16. وظائف الشبكة
سنناقش الآن وظائف الشبكة في PHP، والتي تسمح لنا بتنفيذ برمجة TCP/IP (بروتوكول التحكم في الإرسال/بروتوكول الإنترنت).

16.1. أساسيات برمجة الإنترنت
16.1.1. نظرة عامة
لنفترض وجود اتصال بين جهازين بعيدين، A و B:

عندما يرغب تطبيق AppA الموجود على الجهاز A في التواصل مع تطبيق AppB الموجود على الجهاز B عبر الإنترنت، يجب أن يعرف عدة أمور:
- عنوان IP (بروتوكول الإنترنت) أو اسم الجهاز B؛
- رقم المنفذ الذي يستخدمه التطبيق AppB. في الواقع، قد يستضيف الجهاز B العديد من التطبيقات التي تعمل على الإنترنت. وعندما يتلقى معلومات من الشبكة، يجب أن يعرف التطبيق الذي تستهدفه هذه المعلومات. تصل التطبيقات الموجودة على الجهاز B إلى الشبكة من خلال واجهات تُعرف أيضًا باسم منافذ الاتصال. وترد هذه المعلومات في الحزمة التي يتلقاها الجهاز B حتى يمكن توصيلها إلى التطبيق الصحيح؛
- بروتوكولات الاتصال التي يفهمها الجهاز B. في دراستنا، سنستخدم بروتوكولات TCP-IP فقط؛
- بروتوكول الاتصال الذي يدعمه التطبيق AppB. في الواقع، ستتواصل الجهازان A و B مع بعضهما البعض. وسيتم تغليف ما يتبادلانه ضمن بروتوكولات TCP/IP. ومع ذلك، عندما يتلقى التطبيق AppB، في نهاية السلسلة، المعلومات المرسلة من التطبيق AppA، يجب أن يكون قادرًا على تفسيرها. وهذا مشابه للحالة التي يتواصل فيها شخصان، A و B، عبر الهاتف: حيث يتم نقل محادثتهما عبر الهاتف. يتم ترميز الكلام كسلسلة من الإشارات بواسطة الهاتف A، ويتم إرساله عبر خطوط الهاتف، ويصل إلى الهاتف B ليتم فك ترميزه . ثم يسمع الشخص B الكلام. وهنا يأتي دور مفهوم بروتوكول الاتصال: إذا كان A يتحدث الفرنسية و B لا يفهم تلك اللغة، فلن يتمكن A و B من التواصل بشكل فعال؛
لذلك، يجب أن يتفق التطبيقان المتواصلان على نوع الحوار الذي سيستخدمانه. على سبيل المثال، الحوار مع خدمة FTP يختلف عن الحوار مع خدمة POP: هاتان الخدمتان لا تقبلان نفس الأوامر. لديهما بروتوكول حوار مختلف؛
16.1.2. خصائص بروتوكول TCP
هنا، سنقوم فقط بفحص اتصالات الشبكة التي تستخدم بروتوكول النقل TCP، والذي تتمثل خصائصه الرئيسية فيما يلي:
- تقوم العملية التي ترغب في إرسال البيانات أولاً بإنشاء اتصال مع العملية التي ستستقبل المعلومات التي توشك على إرسالها. يتم إنشاء هذا الاتصال بين منفذ على الجهاز المرسل ومنفذ على الجهاز المستقبل. وبذلك يتم إنشاء مسار افتراضي بين المنفذين، والذي سيتم حجزه حصريًا للعمليتين اللتين قامتا بإنشاء الاتصال؛
- تتبع جميع الحزم المرسلة من قبل العملية المصدر هذا المسار الافتراضي وتصل بالترتيب الذي تم إرسالها به؛
- تبدو المعلومات المرسلة متصلة. ترسل العملية المرسلة المعلومات وفقًا لسرعتها الخاصة. لا يتم إرسال هذه المعلومات بالضرورة على الفور: ينتظر بروتوكول TCP حتى يتوفر لديه ما يكفي لإرساله. يتم تخزينها في بنية تسمى مقطع TCP. بمجرد امتلاء هذا المقطع، يتم إرساله إلى طبقة IP، حيث يتم تغليفه في حزمة IP؛
- يتم ترقيم كل مقطع يرسله بروتوكول TCP. يتحقق بروتوكول TCP المستقبل من أنه يتلقى المقاطع بالتسلسل. لكل مقطع يتم استلامه بشكل صحيح، يرسل إقرارًا بالاستلام إلى المرسل؛
- عندما يتلقى المرسل هذا الإقرار، يقوم بإخطار عملية الإرسال. وبذلك يمكن لعملية الإرسال التأكد من وصول المقطع بأمان؛
- إذا لم يتلق بروتوكول TCP الذي أرسل المقطع إقرارًا بالاستلام بعد فترة زمنية معينة، فإنه يعيد إرسال المقطع المعني، مما يضمن جودة خدمة توصيل المعلومات؛
- الدائرة الافتراضية التي تم إنشاؤها بين عمليتي الاتصال هي دائرة ثنائية الاتجاه: وهذا يعني أن المعلومات يمكن أن تتدفق في كلا الاتجاهين. وبالتالي، يمكن لعملية الوجهة إرسال إقرارات حتى أثناء استمرار عملية المصدر في إرسال المعلومات. وهذا يسمح، على سبيل المثال، لبروتوكول TCP المصدر بإرسال شرائح متعددة دون انتظار إقرار. وإذا أدرك، بعد فترة زمنية معينة، أنه لم يتلق إقرارًا باستلام مقطع معين رقم n، فسيستأنف إرسال المقاطع من تلك النقطة؛
16.1.3. علاقة العميل-الخادم
غالبًا ما يكون الاتصال عبر الإنترنت غير متماثل: تبدأ الآلة A اتصالاً لطلب خدمة من الآلة B، محددةً أنها تريد فتح اتصال مع الخدمة SB1 على الآلة B. وتقبل الآلة B أو ترفض. إذا قبلت، يمكن للجهاز A إرسال طلباته إلى الخدمة SB1. يجب أن تتوافق هذه الطلبات مع بروتوكول الاتصال الذي تفهمه الخدمة SB1. وبذلك يتم إنشاء حوار طلب-استجابة بين الجهاز A، المعروف باسم جهاز العميل، والجهاز B، المعروف باسم جهاز الخادم. سيقوم أحد الشريكين بإغلاق الاتصال.
16.1.4. بنية العميل
ستكون بنية برنامج الشبكة الذي يطلب خدمات تطبيق الخادم كما يلي:
ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
tant que ce n'est pas fini
préparer une demande
l'émettre vers la machine B
attendre et récupérer la réponse
la traiter
fin tant que
finsi
fermer la connexion
16.1.5. بنية الخادم
ستكون بنية البرنامج الذي يقدم الخدمات على النحو التالي:
ouvrir le service sur la machine locale
tant que le service est ouvert
se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que
يعالج برنامج الخادم طلب الاتصال الأولي للعميل بشكل مختلف عن طلباته اللاحقة للحصول على الخدمة. لا يقدم البرنامج الخدمة بنفسه. لو فعل ذلك، لما كان يستمع لطلبات الاتصال أثناء تقديم الخدمة، ولما تم تلبية احتياجات العملاء. لذلك، يتصرف البرنامج بطريقة مختلفة: بمجرد استلام طلب اتصال على منفذ الاستماع وقبوله، يقوم الخادم بإنشاء مهمة مسؤولة عن تقديم الخدمة التي طلبها العميل. يتم تقديم هذه الخدمة على منفذ آخر بجهاز الخادم يُسمى منفذ الخدمة. وهذا يسمح بخدمة عدة عملاء في نفس الوقت.
ستكون مهمة الخدمة بالهيكل التالي:
tant que le service n'a pas été rendu totalement
attendre une demande sur le port de service
lorsqu'il y en a une, élaborer la réponse
transmettre la réponse via le port de service
fin tant que
libérer le port de service
16.2. تعرف على بروتوكولات الاتصال عبر الإنترنت
16.2.1. مقدمة
عندما يتصل عميل بخادم، يتم إنشاء حوار بينهما. وتشكل طبيعة هذا الحوار ما يُعرف ببروتوكول اتصال الخادم. ومن بين بروتوكولات الإنترنت الأكثر شيوعًا ما يلي:
- HTTP: بروتوكول نقل النص التشعبي – البروتوكول المستخدم للتواصل مع خادم الويب (خادم HTTP)؛
- SMTP: بروتوكول نقل البريد البسيط – وهو البروتوكول المستخدم للتواصل مع خادم إرسال البريد الإلكتروني (خادم SMTP)؛
- POP: بروتوكول مكتب البريد – وهو البروتوكول المستخدم للتواصل مع خادم تخزين البريد الإلكتروني (خادم POP). ويستخدم هذا البروتوكول لاسترداد رسائل البريد الإلكتروني المستلمة، وليس لإرسالها؛
- IMAP: بروتوكول الوصول إلى رسائل الإنترنت – وهو البروتوكول المستخدم للتواصل مع خادم تخزين البريد الإلكتروني (خادم IMAP). وقد حل هذا البروتوكول تدريجياً محل بروتوكول POP الأقدم؛
- FTP: بروتوكول نقل الملفات — البروتوكول المستخدم للتواصل مع خادم تخزين الملفات (خادم FTP)؛
جميع هذه البروتوكولات تعتمد على النص: حيث يتبادل العميل والخادم أسطر من النص. إذا كان لديك عميل قادر على:
- إنشاء اتصال بخادم TCP؛
- عرض أسطر النص المرسلة من الخادم على وحدة التحكم؛
- إرسال أسطر النص التي يكتبها المستخدم على لوحة المفاتيح إلى الخادم؛
فإننا نكون قادرين على التواصل مع خادم TCP باستخدام بروتوكول نصي، شريطة أن نعرف قواعد ذلك البروتوكول.
16.2.2. أدوات TCP

في الكود المرتبط بهذا المستند، توجد أداتان مساعدتان للاتصال عبر TCP:
- [RawTcpClient] تسمح لك بالاتصال بالمنفذ P لخادم S؛
- [RawTcpServer] تنشئ خادمًا يستمع إلى العملاء على المنفذ P؛
يتم استدعاء خادم TCP [RawTcpServer] باستخدام صيغة [RawTcpServer port] لإنشاء خدمة TCP على المنفذ [port] للجهاز المحلي (الكمبيوتر الذي تعمل عليه):
- يمكن للخادم خدمة عدة عملاء في وقت واحد؛
- يقوم الخادم بتنفيذ الأوامر التي يكتبها المستخدم على لوحة المفاتيح. وهذه الأوامر هي كما يلي:
- list: يسرد العملاء المتصلين حاليًا بالخادم. يتم عرضهم بالتنسيق [id=x-name=y]. يُستخدم حقل [id] لتعريف العملاء؛
- send x [text]: يرسل نصًا إلى العميل رقم x (id=x). لا يتم إرسال الأقواس المربعة []. وهي مطلوبة في الأمر. وتُستخدم لتمييز النص المرسل إلى العميل بصريًا؛
- close x: يغلق الاتصال مع العميل #x؛
- quit: يغلق جميع الاتصالات ويوقف الخدمة؛
- يتم عرض الأسطر المرسلة من العميل إلى الخادم على وحدة التحكم؛
- يتم تسجيل جميع التبادلات في ملف نصي باسم [machine-portService.txt]، حيث
- [machine] هو اسم الجهاز الذي يعمل عليه الكود؛
- [port] هو منفذ الخدمة الذي يستجيب لطلبات العميل؛
يتم استدعاء عميل TCP [RawTcpClient] باستخدام صيغة [RawTcpClient server port] للاتصال بالمنفذ [port] على الخادم [server]:
- يتم إرسال الأسطر التي يكتبها المستخدم على لوحة المفاتيح إلى الخادم؛
- يتم عرض الأسطر المرسلة من الخادم على وحدة التحكم؛
- يتم تسجيل جميع الاتصالات في ملف نصي باسم [server-port.txt]؛
لنلقِ نظرة على مثال. افتح نافذتين لـ «موجه أوامر Windows» وانتقل إلى مجلد «utilitys» في كل منهما. في إحدى النافذتين، قم بتشغيل خادم [RawTcpServer] على المنفذ 100:

- في [1]، نحن في مجلد utilities؛
- في [2]، نبدأ تشغيل خادم TCP على المنفذ 100؛
- في [3]، ينتظر الخادم عميل TCP؛
- في [4]، ينتظر الخادم أمرًا يدخله المستخدم عبر لوحة المفاتيح؛
في نافذة الأوامر الأخرى، نقوم بتشغيل عميل TCP:

- في [5]، نحن في مجلد الأدوات المساعدة؛
- في [6]، نقوم بتشغيل عميل TCP: نطلب منه الاتصال بالمنفذ 100 على الجهاز المحلي (الجهاز الذي تعمل عليه)؛
- في [7]، نجح العميل في الاتصال بالخادم. يتم عرض تفاصيل العميل: إنه موجود على الجهاز [DESKTOP-528I5CU] (الجهاز المحلي في هذا المثال) ويستخدم المنفذ [50405] للتواصل مع الخادم:
- في [8]، ينتظر العميل أمرًا يدخله المستخدم عبر لوحة المفاتيح؛
لنعد إلى نافذة الخادم. لقد تغير محتواها:

- في [9]، تم اكتشاف عميل. وقد خصص له الخادم الرقم 1. وقد تعرف الخادم بشكل صحيح على العميل البعيد (الجهاز والمنفذ)؛
- في [10]، يعود الخادم إلى انتظار عميل جديد؛
لنعد إلى نافذة العميل ونرسل أمرًا إلى الخادم:

- في [11]، تم إرسال الأمر إلى الخادم؛
لنعد إلى نافذة الخادم. لقد تغير محتواها:

- في [12]، بين قوسين معقوفين، الرسالة التي استقبلها الخادم؛
دعونا نرسل ردًا إلى العميل:

- في [13]، الرد المرسل إلى العميل 1. يتم إرسال النص الموجود بين الأقواس فقط، وليس الأقواس نفسها؛
لنعد إلى نافذة العميل:

- في [14]، الاستجابة التي تلقاها العميل. النص الذي تم استلامه هو النص الموجود بين الأقواس المربعة؛
لنعد إلى نافذة الخادم لنرى الأوامر الأخرى:

- في [15]، نطلب قائمة العملاء؛
- في [16]، الرد؛
- في [17]، نغلق الاتصال مع العميل رقم 1؛
- في [18]، تأكيد الخادم؛
- في [19]، نقوم بإيقاف تشغيل الخادم؛
- في [20]، تأكيد الخادم؛
لنعد إلى نافذة العميل:

- في [21]، اكتشف العميل انتهاء الخدمة؛
تم إنشاء ملفين للسجل، أحدهما للخادم والآخر للعميل:

- في [25]، يسجل الخادم: اسم الملف هو اسم العميل [الجهاز-المنفذ]؛
- في [26]، يسجل العميل: اسم الملف هو اسم الخادم [machine-port]؛
سجلات الخادم هي كما يلي:
سجلات العميل هي كما يلي:
16.3. كيفية العثور على اسم أو عنوان IP لجهاز كمبيوتر على الإنترنت

يتم تحديد هوية أجهزة الكمبيوتر على الإنترنت من خلال عنوان IP (IPv4 أو IPv6) وفي أغلب الأحيان من خلال اسم. ومع ذلك، لا يتم استخدام سوى عنوان IP في النهاية. ولذلك، من الضروري أحيانًا معرفة عنوان IP لجهاز كمبيوتر محدد باسمه.
النص البرمجي [ip-01.php] هو كما يلي:
<?php
// strict adherence to declared function parameter types
declare (strict_types=1);
//
// error management
error_reporting(E_ALL & E_STRICT);
ini_set("display_errors", "on");
//
// constants
$HOTES = array("istia.univ-angers.fr", "www.univ-angers.fr", "www.ibm.com", "localhost", "", "xx");
// IP addresses and $HOTES machine names
for ($i = 0; $i < count($HOTES); $i++) {
getIPandName($HOTES[$i]);
}
// end
print "Terminé\n";
exit;
//------------------------------------------------
function getIPandName(string $nomMachine): void {
//$nomMachine: name of the machine whose address is required IP: name of the machine whose address is required IP: name of the machine whose address is required
//
// nomMachine-->adresse IP
$ip = gethostbyname($nomMachine);
print "---------------\n";
if ($ip !== $nomMachine) {
print "ip[$nomMachine]=$ip\n";
// address IP --> nomMachine
$name = gethostbyaddr($ip);
if ($name !== $ip) {
print "name[$ip]=$name\n";
} else {
print "Erreur, machine[$ip] non trouvée\n";
}
} else {
print "Erreur, machine[$nomMachine] non trouvée\n";
}
}
تعليقات
- السطران 7-8: نطلب من PHP الإبلاغ عن جميع الأخطاء (E_ALL و E_STRICT) وعرضها. يوصى باستخدام هذا الوضع فقط في وضع التطوير لتحسين الكود باستخدام تحذيرات PHP. في وضع الإنتاج، السطر 8، سنضبطه على "off". منذ PHP 5.4، تم تضمين مستوى E_STRICT في E_ALL؛
- السطر 11: قائمة الأجهزة التي نريد الحصول على اسمها وعنوان IP الخاص بها؛
تُستخدم وظائف شبكة PHP في وظيفة getIpandName في السطر 21.
- السطر 25: تسترد الدالة gethostbyname($name) عنوان IP "ip3.ip2.ip1.ip0" الخاص بالجهاز المسمى $name. إذا كان الجهاز $name غير موجود، فإن الدالة تُرجع $name كنتيجة؛
- السطر 30: تسترد الدالة gethostbyaddr($ip) اسم الجهاز المرتبط بعنوان IP $ip بالتنسيق "ip3.ip2.ip1.ip0". إذا كان الجهاز $ip غير موجود، فإن الدالة تُرجع $ip كنتيجة؛
النتائج:
---------------
ip[istia.univ-angers.fr]=193.49.144.41
name[193.49.144.41]=ametys-fo-2.univ-angers.fr
---------------
ip[www.univ-angers.fr]=193.49.144.41
name[193.49.144.41]=ametys-fo-2.univ-angers.fr
---------------
ip[www.ibm.com]=2.18.220.211
name[2.18.220.211]=a2-18-220-211.deploy.static.akamaitechnologies.com
---------------
ip[localhost]=127.0.0.1
name[127.0.0.1]=DESKTOP-528I5CU
---------------
ip[]=192.168.1.38
name[192.168.1.38]=DESKTOP-528I5CU.home
---------------
Erreur, machine[xx] non trouvée
Terminé
16.4. بروتوكول HTTP (بروتوكول نقل النص التشعبي)
16.4.1. مثال 1

عندما يعرض المتصفح عنوان URL، فإنه يعمل كعميل لخادم ويب، أو بعبارة أخرى، خادم HTTP. ويأخذ زمام المبادرة ويبدأ بإرسال عدد من الأوامر إلى الخادم. بالنسبة لهذا المثال الأول:
- سيكون الخادم هو الأداة المساعدة [RawTcpServer]؛
- سيكون العميل هو المتصفح؛
أولاً، نبدأ تشغيل الخادم على المنفذ 100:

ثم، باستخدام متصفح، نطلب عنوان URL [localhost:100]، مما يعني أننا نحدد أن خادم HTTP الذي نستعلم عنه يعمل على المنفذ 100 للجهاز المحلي:

لنعد إلى نافذة الخادم:

- في [3]، العميل الذي اتصل؛
- في [4-7]، سلسلة الأسطر النصية التي أرسلها:
- في [4]: هذا السطر له التنسيق [GET URL HTTP/1.1]. يطلب عنوان URL / ويوجه الخادم لاستخدام بروتوكول HTTP 1.1؛
- في [5]: هذا السطر له التنسيق [Host: server:port]. لا يهم حالة حرف [Host] في الأمر. لاحظ أن العميل يستعلم عن خادم محلي يعمل على المنفذ 100؛
- يحدد الأمر [User-Agent] هوية العميل؛
- يحدد الأمر [Accept] أنواع المستندات التي يقبلها العميل؛
- يحدد الأمر [Accept-Language] اللغة المطلوب أن تكون بها المستندات المطلوبة في حال وجودها بعدة لغات؛
- يحدد الأمر [Connection] وضع الاتصال المطلوب: يشير [keep-alive] إلى أنه يجب الحفاظ على الاتصال حتى اكتمال التبادل؛
- في [7]: ينهي العميل أوامره بسطر فارغ؛
ننهي الاتصال عن طريق إيقاف تشغيل الخادم:

16.4.2. المثال 2
الآن بعد أن عرفنا الأوامر التي يرسلها المتصفح لطلب عنوان URL، سنطلب هذا العنوان باستخدام عميل TCP الخاص بنا [RawTcpClient]. وسيكون خادم Apache الخاص بـ Laragon هو خادم الويب الخاص بنا.
دعونا نطلق Laragon ثم خادم الويب Apache:


الآن، باستخدام متصفح، دعونا نطلب عنوان URL [http://localhost:80]. هنا، نحدد الخادم فقط [localhost:80] دون عنوان URL للوثيقة. في هذه الحالة، يتم طلب عنوان URL /، أي جذر خادم الويب:

- في [1]، عنوان URL المطلوب. كتبنا في البداية [http://localhost:80]، وقام المتصفح (Firefox هنا) ببساطة بتحويله إلى [localhost] لأن بروتوكول [http] يُفهم ضمناً عند عدم تحديد بروتوكول، والمنفذ [80] يُفهم ضمناً عند عدم تحديد منفذ؛
- في [2]، الصفحة الجذرية / لخادم الويب الذي تم الاستعلام عنه؛
الآن، دعونا نلقي نظرة على النص الذي تلقّاه المتصفح:

- انقر بزر الماوس الأيمن على الصفحة واختر الخيار [2]. سترى شفرة المصدر التالية:
<!DOCTYPE HTML>
<HTML>
<head>
<title>Laragon</title>
<link href="https://fonts.googleapis.com/css?family=Karla:400" rel="stylesheet" type="text/css">
<style>
HTML, body {
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
display: table;
font-weight: 100;
font-family: 'Karla';
}
.container {
text-align: center;
display: table-cell;
vertical-align: middle;
}
.content {
text-align: center;
display: inline-block;
}
.title {
font-size: 96px;
}
.opt {
margin-top: 30px;
}
.opt a {
text-decoration: none;
font-size: 150%;
}
a:hover {
color: red;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<div class="title" title="Laragon">Laragon</div>
<div class="info"><br />
Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11<br />
PHP version: 7.2.11 <span><a title="phpinfo()" href="/?q=info">info</a></span><br />
Document Root: C:/myprograms/laragon-lite/www<br />
</div>
<div class="opt">
<div><a title="Getting Started" href="https://laragon.org/docs">Getting Started</a></div>
</div>
</div>
</div>
</body>
</HTML>
الآن دعونا نطلب عنوان URL [http://localhost:80] باستخدام عميل TCP الخاص بنا:
![]()
- في [1]، نتصل بالمنفذ 80 على خادم localhost. هذا هو المكان الذي يعمل فيه خادم الويب Laragon؛
الآن نكتب الأوامر التي اكتشفناها في الفقرة السابقة:

- في [1]، الأمر [GET]. نطلب الدليل الجذر / لخادم الويب؛
- في [2]، الأمر [Host]؛
- هذان هما الأمران الأساسيان الوحيدان. بالنسبة للأوامر الأخرى، سيستخدم خادم الويب القيم الافتراضية؛
- في [3]، السطر الفارغ الذي يجب أن ينهي أوامر العميل؛
- يأتي رد خادم الويب أسفل السطر 3؛
- في [4] وحتى السطر الفارغ [5] توجد رؤوس HTTP لاستجابة الخادم؛
- بعد السطر [5] يأتي مستند HTML المطلوب [6]؛
نكتب [quit] للخروج من العميل وتنزيل ملف السجل [localhost-80.txt]:
- الأسطر 11–79: مستند HTML المستلم. في المثال السابق، تلقى Firefox المستند نفسه؛
لدينا الآن الأساسيات اللازمة لبرمجة عميل TCP يطلب عنوان URL.
16.4.3. المثال 3

البرنامج النصي [http-01.php] هو عميل HTTP تم تكوينه بواسطة ملف JSON [config-http-01.json]. محتويات الملف هي كما يلي:
- السطر 2: اسم الجهاز الذي يستضيف خادم الويب المراد الوصول إليه؛
- السطر 3: المنفذ الذي يعمل عليه خادم الويب هذا؛
- السطر 4: عنوان URL للوثيقة المطلوبة؛
- السطر 5: الجهاز المستهدف بالصيغة machine:port؛
- السطر 6: تعريف عميل HTTP: يمكنك إدخال ما تريد؛
- السطر 7: نوع المستند الذي يقبله العميل، وهو في هذه الحالة نص HTML؛
- السطر 8: اللغة المطلوبة للوثيقة المطلوبة؛
- السطر 9: حرف نهاية السطر للأوامر المرسلة من قبل العميل: قد يختلف هذا اعتمادًا على ما إذا كان الخادم يعمل على جهاز Unix (\n) أو جهاز Windows (\r\n)؛
فيما يلي نص البرنامج النصي [http-01.php]:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
//
// error management
// error_reporting(E_ALL & E_STRICT);
// ini_set("display_errors", "on");
//
// constants
const CONFIG_FILE_NAME = "config-http-01.json";
//
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// otain the HTML text from the URL configuration file
foreach ($config as $site => $protocole) {
// read site index page $ite
$résultat = getURL($site, $protocole);
// result display
print "$résultat\n";
}//for
// end
exit;
//-----------------------------------------------------------------------
function getURL(string $site, array $protocole, $suivi = TRUE): string {
// reads the URL $site["GET"] and stores it in the $site.HTML file
// client/server dialog is based on the $protocole protocol
//
// open a connection on the $site port
$erreurNumber = 0;
$erreur = "";
$connexion = fsockopen($site, $protocole["port"], $erreurNumber, $erreur);
// return if error
if ($connexion === FALSE) {
return "Echec de la connexion au site (" . $site . " ," . $protocole["port"] . " : $erreur";
}
// $connexion represents a bidirectional communication flow
// between the client (this program) and the contacted web server
// this channel is used for the exchange of orders and information
// the dialog protocol is HTTP
//
// creation of the $site.HTML file
$HTML = fopen("output/$site.HTML", "w");
if ($HTML === FALSE) {
// close client/server connection
fclose($connexion);
// error return
return "Erreur lors de la création du fichier $site.HTML";
}
// the client will start the HTTP dialog with the server
if ($suivi) {
print "Client : début de la communication avec le serveur [$site] ----------------------------\n";
}
// depending on the server, client lines must end with \nor \r\n
$endOfLine = $protocole["endOfLine"];
// for simplicity's sake, we don't test for errors in client/server communication
// the customer sends the GET command to request the URL $protocole["GET"]
// syntax GET URL HTTP/1.1
$commande = "GET " . $protocole["GET"] . " HTTP/1.1$endOfLine";
// followed?
if ($suivi) {
print "--> $commande";
}
// send the command to the server
fputs($connexion, $commande);
// issue other headers HTTP
foreach ($protocole as $verb => $value) {
if ($verb !== "GET" && $verb != "port"" && $verb !="endOfLine") {
// we build the
$commande = "$verb: $value$endOfLine";
// followed?
if ($suivi) {
print "--> $commande";
}
// send the command to the server
fputs($connexion, $commande);
}
}
// protocol HTTP headers must end with an empty line
fputs($connexion, $endOfLine);
//
// the server will now respond on channel $connexion. It will send all
// then close the channel. The client therefore reads everything that arrives from $connexion
// until the channel closes
//
// we first read the HTTP headers sent by the server
// they also end with an empty line
if ($suivi) {
print "Réponse du serveur [$site] ----------------------------\n";
}
$fini = FALSE;
while (!$fini && $ligne = fgets($connexion, 1000)) {
// is there an empty line?
$champs = [];
preg_match("/^(.*?)\s+$/", $ligne, $champs);
if ($champs[1] !== "") {
if ($suivi) {
// header HTTP is displayed
print "<-- " . $champs[1] . "\n";
}
} else {
// this was the empty line - HTTP headers are finished
$fini = TRUE;
}
}
// we read the HTML document that will follow the empty line
while ($ligne = fgets($connexion, 1000)) {
// we save the line in the HTML file on the site
fputs($HTML, $ligne);
}
// the server has closed the connection - the client closes it in turn
fclose($connexion);
// close file $HTML
fclose($HTML);
// return
return "Fin de la communication avec le site [$site]. Vérifiez le fichier [$site.HTML]";
}
تعليقات على الكود:
- السطر 14: يُستخدم ملف التكوين لإنشاء قاموس:
- مفاتيح القاموس هي خوادم الويب المطلوب الاستعلام عنها؛
- تحدد القيم بروتوكول HTTP المراد استخدامه؛
- الأسطر 16-21: نقوم بتكرار قائمة خوادم الويب في التكوين؛
- السطر 26: تسترد الدالة getURL($site, $protocol, $log) مستندًا من موقع الويب $site وتحفظه في الملف النصي $site.HTML. بشكل افتراضي، يتم تسجيل تفاعلات العميل/الخادم في وحدة التحكم ($log=TRUE)؛
- السطر 33: تقوم الدالة fsockopen($site,$port,$errNumber,$error) بإنشاء اتصال بخدمة TCP/IP تعمل على المنفذ $port على جهاز $site. إذا فشل الاتصال، فإن [$errNumber] هو رمز الخطأ و[$error] هي رسالة الخطأ المرتبطة به. بمجرد فتح اتصال العميل/الخادم، تتبادل العديد من خدمات TCP/IP أسطر النص. هذا هو الحال هنا مع HTTP (بروتوكول نقل النص التشعبي). يمكن بعد ذلك معالجة دفق البيانات من الخادم إلى العميل كملف نصي يتم قراءته باستخدام [fgets]. وينطبق الأمر نفسه على دفق البيانات من العميل إلى الخادم، والذي يمكن كتابته باستخدام [fputs]؛
- الأسطر 44–50: إنشاء الملف [$site.HTML] الذي سيتم تخزين مستند HTML المستلم فيه؛
- السطر 60: يجب أن يكون الأمر الأول للعميل هو [GET URL HTTP/1.1]؛
- السطر 66: تسمح وظيفة fputs للعميل بإرسال البيانات إلى الخادم. هنا، السطر النصي المرسل له المعنى التالي: "أريد (GET) صفحة [URL] للموقع الإلكتروني الذي أنا متصل به. أنا أستخدم HTTP الإصدار 1.1"؛
- الأسطر 68-79: يتم إرسال أسطر بروتوكول HTTP الأخرى [Host، User-Agent، Accept، Accept-Language]. لا يهم ترتيبها؛
- السطر 81: يتم إرسال سطر فارغ إلى الخادم للإشارة إلى أن العميل قد انتهى من إرسال رؤوس HTTP الخاصة به وهو الآن في انتظار المستند المطلوب؛
- الأسطر 92–106: سيرسل الخادم أولاً سلسلة من رؤوس HTTP التي توفر تفاصيل متنوعة حول المستند المطلوب. تنتهي هذه الرؤوس بسطر فارغ؛
- السطر 93: يتم قراءة سطر أرسله الخادم باستخدام دالة PHP [fgets]؛
- السطر 96: نسترد نص السطر بدون المسافات (المسافات البيضاء، أحرف نهاية السطر) في نهاية السطر؛
- السطر 97: نتحقق مما إذا كنا قد استرددنا السطر الفارغ الذي يشير إلى نهاية رؤوس HTTP المرسلة من الخادم؛
- الأسطر 98–101: إذا كان في وضع [trace]، يتم عرض رأس HTTP المستلم في وحدة التحكم؛
- الأسطر 108–111: يمكن قراءة أسطر النص الخاصة باستجابة الخادم سطراً سطراً باستخدام حلقة while وحفظها في ملف نصي [output/$site.HTML]. عندما يرسل خادم الويب الصفحة المطلوبة بالكامل، فإنه يغلق اتصاله مع العميل. على جانب العميل، سيتم اكتشاف ذلك على أنه نهاية الملف؛
النتائج:
تعرض وحدة التحكم السجلات التالية:
Client : début de la communication avec le serveur [localhost] ----------------------------
--> GET / HTTP/1.1
--> Host: localhost:80
--> User-Agent: client PHP
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [localhost] ----------------------------
<-- HTTP/1.1 200 OK
<-- Date: Thu, 16 May 2019 15:43:18 GMT
<-- Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
<-- X-Powered-By: PHP/7.2.11
<-- Content-Length: 1781
<-- Content-Type: text/HTML; charset=UTF-8
Fin de la communication avec le site [localhost]. Vérifiez le fichier [localhost.HTML]
في مثالنا، يكون الملف [output/localhost.HTML] المستلم كما يلي:
<!DOCTYPE HTML>
<HTML>
<head>
<title>Laragon</title>
<link href="https://fonts.googleapis.com/css?family=Karla:400" rel="stylesheet" type="text/css">
<style>
HTML, body {
height: 100%;
}
body {
margin: 0;
padding: 0;
width: 100%;
display: table;
font-weight: 100;
font-family: 'Karla';
}
.container {
text-align: center;
display: table-cell;
vertical-align: middle;
}
.content {
text-align: center;
display: inline-block;
}
.title {
font-size: 96px;
}
.opt {
margin-top: 30px;
}
.opt a {
text-decoration: none;
font-size: 150%;
}
a:hover {
color: red;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<div class="title" title="Laragon">Laragon</div>
<div class="info"><br />
Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11<br />
PHP version: 7.2.11 <span><a title="phpinfo()" href="/?q=info">info</a></span><br />
Document Root: C:/myprograms/laragon-lite/www<br />
</div>
<div class="opt">
<div><a title="Getting Started" href="https://laragon.org/docs">Getting Started</a></div>
</div>
</div>
</div>
</body>
</HTML>
لقد حصلنا بالفعل على نفس المستند الذي حصلنا عليه باستخدام متصفح Firefox.
16.4.4. المثال 4
في هذا المثال، سنوضح أن عميل HTTP الذي كتبناه غير كافٍ. قم بتعديل ملف التكوين [config-http-01.json] على النحو التالي:
هنا، سنطلب عنوان URL [http://tahe.developpez.com:443/]. المنفذ 443 على الجهاز [tahe.developpez.com] هو منفذ يستخدم لبروتوكول HTTP الآمن المعروف باسم HTTPS. في هذا البروتوكول، يبدأ التفاعل بين العميل والخادم بتبادل المعلومات التي تؤمن الاتصال. لذلك يجب على العميل استخدام بروتوكول [HTTPS] بدلاً من بروتوكول [HTTP]، وهو ما لا يفعله عميلنا.
مع ملف التكوين هذا، يكون إخراج وحدة التحكم كما يلي:
Client : début de la communication avec le serveur [tahe.developpez.com] ----------------------------
--> GET / HTTP/1.1
--> Host: sergetahe.com:443
--> User-Agent: script PHP 7
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [tahe.developpez.com] ----------------------------
<-- HTTP/1.1 400 Bad Request
<-- Date: Fri, 17 May 2019 13:02:26 GMT
<-- Server: Apache/2.4.25 (Debian)
<-- Content-Length: 454
<-- Connection: close
<-- Content-Type: text/HTML; charset=iso-8859-1
Fin de la communication avec le site [tahe.developpez.com]. Vérifiez le fichier [output/tahe.developpez.com.HTML]
- السطر 8: رد الخادم [tahe.developpez.com] بأن طلب العميل غير صحيح؛
محتوى الملف [output/tahe.developpez.com.HTML] هو كما يلي:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<HTML><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
Reason: You're speaking plain HTTP to an SSL-enabled server port.<br />
Instead use the HTTPS scheme to access this URL, please.<br />
</p>
<hr>
<address>Apache/2.4.25 (Debian) Server at 2eurocents.developpez.com Port 443</address>
</body></HTML>
يذكر الخادم بوضوح أننا لم نستخدم البروتوكول الصحيح.
الآن دعونا نستخدم ملف التكوين التالي:
إخراج وحدة التحكم كما يلي:
Client : début de la communication avec le serveur [sergetahe.com] ----------------------------
--> GET /cours-tutoriels-de-programmation/ HTTP/1.1
--> Host: sergetahe.com:80
--> User-Agent: script PHP 7
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [sergetahe.com] ----------------------------
<-- HTTP/1.1 200 OK
<-- Date: Fri, 17 May 2019 13:36:06 GMT
<-- Content-Type: text/HTML; charset=UTF-8
<-- Transfer-Encoding: chunked
<-- Server: Apache
<-- X-Powered-By: PHP/7.0
<-- Vary: Accept-Encoding
<-- Set-Cookie: SERVERID68971=2621207|XN64y|XN64y; path=/
<-- Cache-control: private
<-- X-IPLB-Instance: 17106
Fin de la communication avec le site [sergetahe.com]. Vérifiez le fichier [output/sergetahe.com.HTML]
- تشير السطر 11 إلى أن الخادم يرسل المستند على شكل أجزاء؛
وينتج عن ذلك ظهور أرقام في دفق البيانات المرسلة إلى العميل: حيث يُعلم كل رقم العميل بعدد الأحرف الموجودة في الجزء التالي الذي يرسله الخادم. وإليك كيف يبدو ذلك في الملف [output/sergetahe.com.HTML]:

- يمثل [1] و[2] الحجم السداسي عشري للكتل 1 و2 من المستند؛
يجب ألا يترك عميل HTTP سليم هذه الأرقام في المستند HTML النهائي.
إليك مثال آخر:
يشبه المثال السابق، لكن عنوان URL المطلوب في السطر 4 لا ينتهي بـ /. هذان العنوانان ليسا متطابقين. عند تشغيل عميل HTTP، ينتج عن ذلك إخراج وحدة التحكم التالي:
Client : début de la communication avec le serveur [sergetahe.com] ----------------------------
--> GET /cours-tutoriels-de-programmation HTTP/1.1
--> Host: sergetahe.com:80
--> User-Agent: script PHP 7
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [sergetahe.com] ----------------------------
<-- HTTP/1.1 301 Moved Permanently
<-- Date: Fri, 17 May 2019 13:47:00 GMT
<-- Content-Type: text/HTML; charset=iso-8859-1
<-- Content-Length: 262
<-- Server: Apache
<-- Location: http://sergetahe.com:80/cours-tutoriels-de-programmation/
<-- Set-Cookie: SERVERID68971=2621207|XN67V|XN67V; path=/
<-- Cache-control: private
<-- X-IPLB-Instance: 17095
Fin de la communication avec le site [sergetahe.com]. Vérifiez le fichier [output/sergetahe.com.HTML]
- يشير السطر 8 إلى أن عنوان URL للوثيقة المطلوبة قد تغير. يرد عنوان URL الجديد في السطر 13. لاحظ هذه المرة الحرف / الذي ينهي عنوان URL الجديد؛
يصبح الملف [output/serge.tahe.com.HTML] كما يلي:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<HTML><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="http://sergetahe.com/cours-tutoriels-de-programmation/">here</a>.</p>
</body></HTML>
يجب أن يكون عميل HTTP قادراً على متابعة عمليات إعادة التوجيه. في هذه الحالة، يجب أن يطلب تلقائياً عنوان URL الجديد [http://sergetahe.com/cours-tutoriels-de-programmation/].
16.4.5. المثال 5
أظهرت لنا الأمثلة السابقة أن عميل HTTP الخاص بنا غير كافٍ. سنقدم الآن أداة تسمى [curl] تسمح لك باسترداد مستندات الويب مع التعامل مع التحديات المذكورة: بروتوكول HTTPS، والمستندات المرسلة على شكل أجزاء، وعمليات إعادة التوجيه... تم تثبيت أداة [curl] مع Laragon:

لنفتح محطة Laragon [1]:

في المحطة الطرفية، اكتب الأمر التالي:

- في [1]، نوع وحدة التحكم؛
- في [2]، الدليل الحالي. هذا الدليل خاص: فهو المكان الذي يسترد منه خادم Apache الخاص بـ Laragon المستندات المطلوبة منه. لذلك سنتجنب ازدحام هذا الدليل؛
- في [3]، الأمر الذي تم إدخاله؛
قد ينتج عن الأمر [curl --help] خطأ. السبب الأكثر احتمالاً هو أنك لا تمتلك نوع المحطة الطرفية الصحيح. في هذه الحالة، افتح محطة طرفية أخرى باستخدام الأوامر [4-6]؛
يعرض الأمر [curl --help] جميع خيارات تكوين [curl]. هناك العشرات منها. سنستخدم القليل منها فقط. لطلب عنوان URL، اكتب ببساطة الأمر [curl URL]. سيعرض هذا الأمر المستند المطلوب على وحدة التحكم. إذا كنت تريد أيضًا رؤية تبادلات HTTP بين العميل والخادم، اكتب [curl --verbose URL]. أخيرًا، لحفظ مستند HTML المطلوب في ملف، اكتب [curl --verbose --output file URL].
لتجنب ازدحام مجلد [www] في Laragon، دعونا ننتقل إلى موقع آخر في نظام الملفات:

- في [1]، انتقل إلى مجلد [c:\temp]. إذا لم يكن هذا المجلد موجودًا، يمكنك إنشاؤه أو اختيار مجلد آخر؛
- في [2]، أنشئ مجلدًا باسم [curl]؛
- في [3]، انتقل إليه؛
- في [4]، اعرض محتوياته. إنه فارغ؛
تأكد من تشغيل خادم Laragon Apache، وباستخدام [curl]، اطلب عنوان URL [http://localhost/] باستخدام الأمر [curl –verbose –output localhost.HTML http://localhost/]. ستحصل على النتائج التالية:
c:\Temp\curl
λ curl --verbose --output localhost.HTML http://localhost/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying ::1…
* TCP_NODELAY set
* Connected to localhost (::1) port 80 (#0)
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 17 May 2019 14:32:47 GMT
< Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
< X-Powered-By: PHP/7.2.11
< Content-Length: 1781
< Content-Type: text/HTML; charset=UTF-8
<
{ [1781 bytes data]
100 1781 100 1781 0 0 14248 0 --:--:-- --:--:-- --:--:-- 14248
* Connection #0 to host localhost left intact
- الأسطر 8-12: الأسطر التي أرسلها [curl] إلى خادم [localhost]. تم التعرف على بروتوكول HTTP؛
- الأسطر 13–19: الأسطر المرسلة ردًا على ذلك من قبل الخادم؛
- السطر 13: يشير إلى أن المستند المطلوب قد تم استلامه بنجاح؛
يحتوي ملف [localhost.HTML] على المستند المطلوب. يمكنك التحقق من ذلك عن طريق فتح الملف في محرر نصوص.
الآن دعونا نطلب عنوان URL [https://tahe.developpez.com:443/]. لاسترداد عنوان URL هذا، يجب أن يدعم عميل HTTP بروتوكول HTTPS. وهذا هو الحال مع عميل [curl].
إخراج وحدة التحكم كما يلي:
c:\Temp\curl
λ curl --verbose --output tahe.developpez.com.HTML https://tahe.developpez.com:443/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 87.98.130.52…
* TCP_NODELAY set
* Connected to tahe.developpez.com (87.98.130.52) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: C:\myprograms\laragon-lite\bin\laragon\utils\curl-ca-bundle.crt
CApath: none
} [5 bytes data]
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
} [512 bytes data]
* TLSv1.3 (IN), TLS handshake, Server hello (2):
{ [108 bytes data]
* TLSv1.2 (IN), TLS handshake, Certificate (11):
{ [2558 bytes data]
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
{ [333 bytes data]
* TLSv1.2 (IN), TLS handshake, Server finished (14):
{ [4 bytes data]
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
} [70 bytes data]
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
} [1 bytes data]
* TLSv1.2 (OUT), TLS handshake, Finished (20):
} [16 bytes data]
* TLSv1.2 (IN), TLS handshake, Finished (20):
{ [16 bytes data]
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use http/1.1
* Server certificate:
* subject: CN=*.developpez.com
* start date: Apr 4 08:25:09 2019 GMT
* expire date: Jul 3 08:25:09 2019 GMT
* subjectAltName: host "tahe.developpez.com" matched cert's "*.developpez.com"
* issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
* SSL certificate verify ok.
} [5 bytes data]
> GET / HTTP/1.1
> Host: tahe.developpez.com
> User-Agent: curl/7.63.0
> Accept: */*
>
{ [5 bytes data]
< HTTP/1.1 200 OK
< Date: Fri, 17 May 2019 14:39:41 GMT
< Server: Apache/2.4.25 (Debian)
< X-Powered-By: PHP/5.3.29
< Vary: Accept-Encoding
< Transfer-Encoding: chunked
< Content-Type: text/HTML
<
{ [6 bytes data]
100 96559 0 96559 0 0 163k 0 --:--:-- --:--:-- --:--:-- 163k
* Connection #0 to host tahe.developpez.com left intact
- الأسطر 10–40: تبادل البيانات بين العميل والخادم لتأمين الاتصال: سيتم تشفير هذه البيانات؛
- الأسطر 42-45: رؤوس HTTP المرسلة من العميل [curl] إلى الخادم؛
- السطر 48: تم العثور على المستند المطلوب؛
- السطر 53: يتم إرسال المستند على شكل أجزاء؛
يتعامل [curl] بشكل صحيح مع كل من بروتوكول HTTPS الآمن وحقيقة أن المستند يتم إرساله على شكل أجزاء. يمكن العثور على المستند المرسل هنا في الملف [tahe.developpez.com.HTML].
الآن دعونا نطلب عنوان URL [http://sergetahe.com/cours-tutoriels-de-programmation]. لقد رأينا أنه بالنسبة لعنوان URL هذا، كان هناك إعادة توجيه إلى عنوان URL [http://sergetahe.com/cours-tutoriels-de-programmation/] (مع وجود / في النهاية).
إخراج وحدة التحكم كما يلي:
c:\Temp\curl
λ curl --verbose --output sergetahe.com.HTML --location http://sergetahe.com/cours-tutoriels-de-programmation
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 87.98.154.146…
* TCP_NODELAY set
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation HTTP/1.1
> Host: sergetahe.com
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Date: Fri, 17 May 2019 15:13:03 GMT
< Content-Type: text/HTML; charset=iso-8859-1
< Content-Length: 262
< Server: Apache
< Location: http://sergetahe.com/cours-tutoriels-de-programmation/
< Set-Cookie: SERVERID68971=2621207|XN7Pg|XN7Pg; path=/
< Cache-control: private
< X-IPLB-Instance: 17095
<
* Ignoring the response-body
{ [262 bytes data]
100 262 100 262 0 0 1401 0 --:--:-- --:--:-- --:--:-- 1401
* Connection #0 to host sergetahe.com left intact
* Issue another request to this URL: 'http://sergetahe.com/cours-tutoriels-de-programmation/'
* Found bundle for host sergetahe.com: 0x1c88548 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) with host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
> GET /cours-tutoriels-de-programmation/ HTTP/1.1
> Host: sergetahe.com
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 17 May 2019 15:13:04 GMT
< Content-Type: text/HTML; charset=UTF-8
< Transfer-Encoding: chunked
< Server: Apache
< X-Powered-By: PHP/7.0
< Vary: Accept-Encoding
< Set-Cookie: SERVERID68971=2621207|XN7Pg|XN7Pg; path=/
< Cache-control: private
< X-IPLB-Instance: 17095
<
{ [14205 bytes data]
100 43101 0 43101 0 0 78795 0 --:--:-- --:--:-- --:--:-- 168k
* Connection #0 to host sergetahe.com left intact
- السطر 2: تُستخدم الخيار [--location] للإشارة إلى أننا نريد اتباع عمليات إعادة التوجيه المرسلة من الخادم؛
- السطر 13: يشير الخادم إلى أن عنوان URL للوثيقة المطلوبة قد تغير؛
- السطر 18: يشير إلى عنوان URL الجديد للوثيقة المطلوبة؛
- السطر 27: يرسل [curl] طلبًا جديدًا إلى عنوان URL الجديد؛
- السطر 33: يتم استخدام عنوان URL الجديد؛
- السطر 38: يرد الخادم بأنه عثر على المستند المطلوب؛
- السطر 41: يرسله على شكل أجزاء؛
سيتم العثور على المستند المطلوب في الملف [sergetahe.com.HTML].
16.4.6. المثال 6
يحتوي PHP على ملحق يسمى [libcurl] يتيح لك استخدام إمكانيات أداة [curl] في برنامج PHP. أولاً، تأكد من تمكين هذا الملحق في ملف [php.ini] الموضح في قسم الروابط:

تأكد من إزالة التعليق عن السطر 889 أعلاه.
سنكتب برنامجًا نصيًا [http-02.php] سيستخدم ملف التكوين JSON التالي:
كل إدخال [مفتاح، قيمة] في القاموس له البنية التالية:
- المفتاح: اسم خادم الويب؛
- القيمة هي قاموس يحتوي على المفاتيح التالية:
- timeout: الحد الأقصى لوقت انتظار استجابة الخادم. بعد هذا الوقت، سيقوم العميل بقطع الاتصال؛
- url: عنوان URL للوثيقة المطلوبة؛
رمز البرنامج النصي [http-02.php] هو كما يلي:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
//
// error management
//error_reporting(E_ALL & E_STRICT);
//ini_set("display_errors", "on");
//
// constants
const CONFIG_FILE_NAME = "config-http-02.json";
//
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// get the HTML text from the URL in the configuration file
foreach ($config as $site => $infos) {
// reading URL from site $ite
$résultat = getUrl($site, $infos["url"], $infos["timeout"]);
// result display
print "$résultat\n";
}//for
// end
exit;
//-----------------------------------------------------------------------
function getUrl(string $site, string $url, int $timeout, $suivi = TRUE): string {
// reads the URL $url and stores it in the file output/$site.HTML
//
// follow-up
print "Client : début de la communication avec le serveur [$site] ----------------------------\n";
// Session initialization cURL
$curl = curl_init($url);
if ($curl === FALSE) {
// there has been an error
return "Erreur lors de l'initialisation de la session cURL pour le site [$site]";
}
// curl options
$options = [
// verbose mode
CURLOPT_VERBOSE => true,
// new connection - no cache
CURLOPT_FRESH_CONNECT => true,
// request timeout (in seconds)
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => $timeout,
// do not check the validity of SSL certificates
CURLOPT_SSL_VERIFYPEER => false,
// track redirects
CURLOPT_FOLLOWLOCATION => true,
// retrieve the requested document as a character string
CURLOPT_RETURNTRANSFER => true
];
// curl settings
curl_setopt_array($curl, $options);
// Executing the request
$page_content = curl_exec($curl);
// Close session cURL
curl_close($curl);
// income statement
if ($page_content !== FALSE) {
// save result in $site.HTML
$result = file_put_contents("output/$site.HTML", $page_content);
if ($result === FALSE) {
// error return
return "Erreur lors de la création du fichier [output/$site.HTML]";
}
// successful comeback
return "Fin de la communication avec le serveur [$site]. Vérifiez le fichier [output/$site.HTML]";
} else {
// there has been a communication error
return "Erreur de communication avec le serveur [$site]";
}
}
تعليقات
- السطر 14: نستخدم ملف التكوين لإنشاء القاموس [$config]؛
- الأسطر 17–22: نقوم بتكرار قائمة المواقع الموجودة في التكوين؛
- السطر 19: لكل موقع، نستدعي الدالة [getUrl]، التي ستقوم بتنزيل عنوان URL $infos["url"] مع مهلة انتظار تبلغ $infos["timeout"];
- السطر 34: نبدأ جلسة [curl]. لا تتصل [curl_init] بعد بخادم الويب. وهي تُرجع موردًا [$curl] سيكون معلمة لجميع دوال [curl] اللاحقة؛
- الأسطر 35–38: إذا فشلت تهيئة جلسة [curl]، فإن دالة [curl_init] ترجع القيمة المنطقية FALSE؛
- الأسطر 40–54: يقوم القاموس [$options] بتكوين اتصال [curl] بالخادم؛
- السطر 57: يتم تمرير خيارات الاتصال إلى المورد [$curl]؛
- السطر 59: يتصل بـ URL المطلوب باستخدام الخيارات المحددة. وبسبب الخيار [CURLOPT_RETURNTRANSFER => true]، تُرجع الدالة [curl_exec] المستند المرسل من الخادم كسلسلة. تُرجع الدالة [curl_exec] القيمة FALSE إذا فشل الاتصال؛
- السطر 64: نقوم بتحليل نتيجة [curl_exec]؛
- السطر 66: يتم حفظ الصفحة المستلمة في ملف محلي؛
- السطور 69 و72 و75: يتم إرجاع نتيجة دالة [getUrl]؛
عند تشغيل البرنامج النصي [http-02.php]، يتم عرض إخراج وحدة التحكم التالي:
* Rebuilt URL to: http://sergetahe.com/
Client : début de la communication avec le serveur [sergetahe.com] ----------------------------
* Trying 87.98.154.146…
* TCP_NODELAY set
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET / HTTP/1.1
Host: sergetahe.com
Accept: */*
< HTTP/1.1 302 Found
< Date: Sat, 18 May 2019 08:46:38 GMT
< Content-Type: text/HTML; charset=UTF-8
< Transfer-Encoding: chunked
< Server: Apache
< X-Powered-By: PHP/7.0
< Location: http://sergetahe.com/cours-tutoriels-de-programmation
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 to host sergetahe.com left intact
* Issue another request to this URL: 'http://sergetahe.com/cours-tutoriels-de-programmation'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) with host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation HTTP/1.1
Host: sergetahe.com
Accept: */*
< HTTP/1.1 301 Moved Permanently
< Date: Sat, 18 May 2019 08:46:38 GMT
< Content-Type: text/HTML; charset=iso-8859-1
< Content-Length: 262
< Server: Apache
< Location: http://sergetahe.com/cours-tutoriels-de-programmation/
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< Cache-control: private
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 to host sergetahe.com left intact
* Issue another request to this URL: 'http://sergetahe.com/cours-tutoriels-de-programmation/'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) with host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation/ HTTP/1.1
Host: sergetahe.com
Accept: */*
< HTTP/1.1 200 OK
< Date: Sat, 18 May 2019 08:46:39 GMT
< Content-Type: text/HTML; charset=UTF-8
< Transfer-Encoding: chunked
< Server: Apache
< X-Powered-By: PHP/7.0
< Link: <http://sergetahe.com/cours-tutoriels-de-programmation/wp-json/>; rel="https://api.w.org/"
< Link: <http://sergetahe.com/cours-tutoriels-de-programmation/>; rel=shortlink
< Vary: Accept-Encoding
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< Cache-control: private
< X-IPLB-Instance: 17097
<
Fin de la communication avec le serveur [sergetahe.com]. Vérifiez le fichier [output/sergetahe.com.HTML]
Client : début de la communication avec le serveur [tahe.developpez.com] ----------------------------
* Connection #0 to host sergetahe.com left intact
* Rebuilt URL to: https://tahe.developpez.com/
* Trying 87.98.130.52…
* TCP_NODELAY set
* Connected to tahe.developpez.com (87.98.130.52) port 443 (#0)
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: C:\myprograms\laragon-lite\etc\ssl\cacert.pem
CApath: none
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use http/1.1
* Server certificate:
* subject: CN=*.developpez.com
* start date: Apr 4 08:25:09 2019 GMT
* expire date: Jul 3 08:25:09 2019 GMT
* subjectAltName: host "tahe.developpez.com" matched cert's "*.developpez.com"
* issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
* SSL certificate verify ok.
> GET / HTTP/1.1
Host: tahe.developpez.com
Accept: */*
< HTTP/1.1 200 OK
< Date: Sat, 18 May 2019 08:46:42 GMT
< Server: Apache/2.4.25 (Debian)
< X-Powered-By: PHP/5.3.29
< Vary: Accept-Encoding
< Transfer-Encoding: chunked
< Content-Type: text/HTML
<
Fin de la communication avec le serveur [tahe.developpez.com]. Vérifiez le fichier [output/tahe.developpez.com.HTML]
Client : début de la communication avec le serveur [www.polytech-angers.fr] ----------------------------
* Connection #0 to host tahe.developpez.com left intact
* Rebuilt URL to: http://www.polytech-angers.fr/
* Trying 193.49.144.41…
* TCP_NODELAY set
* Connected to www.polytech-angers.fr (193.49.144.41) port 80 (#0)
> GET / HTTP/1.1
Host: www.polytech-angers.fr
Accept: */*
< HTTP/1.1 301 Moved Permanently
< Date: Sat, 18 May 2019 08:46:45 GMT
< Server: Apache/2.4.29 (Ubuntu)
< Location: http://www.polytech-angers.fr/fr/index.HTML
< Cache-Control: max-age=1
< Expires: Sat, 18 May 2019 08:46:46 GMT
< Content-Length: 339
< Content-Type: text/HTML; charset=iso-8859-1
<
* Ignoring the response-body
* Connection #0 to host www.polytech-angers.fr left intact
* Issue another request to this URL: 'http://www.polytech-angers.fr/fr/index.HTML'
* Found bundle for host www.polytech-angers.fr: 0x1fee4ebe390 [can pipeline]
* Re-using existing connection! (#0) with host www.polytech-angers.fr
* Connected to www.polytech-angers.fr (193.49.144.41) port 80 (#0)
> GET /fr/index.HTML HTTP/1.1
Host: www.polytech-angers.fr
Accept: */*
< HTTP/1.1 200
< Date: Sat, 18 May 2019 08:46:46 GMT
< Server: Apache/2.4.29 (Ubuntu)
< X-Cocoon-Version: 2.1.13-dev
< Accept-Ranges: bytes
< Last-Modified: Sat, 18 May 2019 08:01:36 GMT
< Content-Type: text/HTML; charset=UTF-8
< Content-Length: 47372
< Vary: Accept-Encoding
< Cache-Control: max-age=1
< Expires: Sat, 18 May 2019 08:46:47 GMT
< Content-Language: fr
<
* Connection #0 to host www.polytech-angers.fr left intact
Fin de la communication avec le serveur [www.polytech-angers.fr]. Vérifiez le fichier [output/www.polytech-angers.fr.HTML]
Client : début de la communication avec le serveur [localhost] ----------------------------
* Rebuilt URL to: http://localhost/
* Trying ::1…
* TCP_NODELAY set
* Connected to localhost (::1) port 80 (#0)
> GET / HTTP/1.1
Host: localhost
Accept: */*
< HTTP/1.1 200 OK
< Date: Sat, 18 May 2019 08:46:47 GMT
< Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
< X-Powered-By: PHP/7.2.11
< Content-Length: 1781
< Content-Type: text/HTML; charset=UTF-8
<
* Connection #0 to host localhost left intact
Fin de la communication avec le serveur [localhost]. Vérifiez le fichier [output/localhost.HTML]
تعليقات
- نحصل على نفس التبادلات كما هو الحال مع أداة [curl]؛
- باللون الأخضر، سجلات البرنامج النصي؛
- باللون الأزرق، الأوامر المرسلة إلى الخادم؛
- باللون الأصفر، الأوامر التي يستقبلها العميل كرد؛
16.4.7. الخلاصة
في هذا القسم، استكشفنا بروتوكول HTTP وكتبنا برنامجًا نصيًا [http-02.php] قادرًا على تنزيل عنوان URL من الويب.
16.5. بروتوكول SMTP (بروتوكول نقل البريد البسيط)
16.5.1. مقدمة

في هذا الفصل:
- سيكون [الخادم B] خادم SMTP محلي سنقوم بتثبيته؛
- [العميل A] سيكون عميل SMTP بأشكال مختلفة:
- عميل [RawTcpClient] لاستكشاف بروتوكول SMTP؛
- نص برمجي PHP يحاكي بروتوكول SMTP لعميل [RawTcpClient]؛
- نص برمجي PHP يستخدم مكتبة [SwiftMailServer] لإرسال جميع أنواع رسائل البريد الإلكتروني؛
16.5.2. إنشاء عنوان [Gmail]
لإجراء اختبارات SMTP الخاصة بنا، سنحتاج إلى عنوان بريد إلكتروني لإرسال الرسائل إليه. للقيام بذلك، سننشئ عنوانًا على Gmail:

- في [5]، نقوم بإنشاء المستخدم [php7parlexemple] (اختر اسمًا آخر)؛
- في [6]، ستكون كلمة المرور [PHP7parlexemple] (اختر اسمًا آخر)؛
- في [7]، نؤكد هذه المعلومات؛

- املأ الحقول [9-10] ثم قم بالتأكيد (11)؛
- اقبل شروط خدمة Google (12-13) ثم قم بالتأكيد (14)؛

- في [15]، صندوق الوارد الخاص بالمستخدم [PHP7] (16)؛
- في [17]، صندوق الوارد الخاص بهذا المستخدم فارغ؛
- في [18-19]، قم بتسجيل الدخول إلى حساب Google الخاص بالمستخدم [php7parlexemple@gmail.com]. سنقوم بتكوين أمان الحساب؛

- في [21]، اسمح للتطبيقات غير التابعة لـ Google بالوصول إلى الحساب [php7parlexemple]. إذا لم تقم بذلك، فلن يتمكن خادم البريد المحلي لدينا [hMailServer] من التواصل مع خادم SMTP الخاص بـ Gmail؛

16.5.3. إعداد خادم SMTP
لأغراض الاختبارات التي سنجريها، سنقوم بتثبيت خادم البريد [hMailServer]، الذي يعمل كخادم SMTP لإرسال رسائل البريد الإلكتروني، وخادم POP3 (بروتوكول مكتب البريد) لاسترداد رسائل البريد الإلكتروني المخزنة على الخادم، وخادم IMAP (بروتوكول الوصول إلى رسائل الإنترنت)، الذي يسمح لك أيضًا باسترداد رسائل البريد الإلكتروني المخزنة على الخادم ولكنه يوفر إمكانيات إضافية. وعلى وجه الخصوص، يسمح لك بإدارة تخزين البريد الإلكتروني على الخادم.
خادم البريد [hMailServer] متاح على الرابط [https://www.hmailserver.com/] (مايو 2019).

أثناء التثبيت، سيُطلب منك إدخال بعض المعلومات:

- في [1-2]، حدد كل من خادم البريد والأدوات اللازمة لإدارته؛
- أثناء التثبيت، سيُطلب منك إدخال كلمة مرور المسؤول: قم بتدوينها، حيث ستحتاج إليها؛
يتم تثبيت [hMailServer] كخدمة Windows يتم تشغيلها تلقائيًا عند بدء تشغيل الكمبيوتر. يفضل اختيار التشغيل اليدوي:
- في [3]، اكتب [services] في مربع البحث على شريط المهام؛

- في [4-8]، اضبط الخدمة على الوضع [يدوي] (6)، ثم قم بتشغيلها (7)؛
بمجرد بدء التشغيل، يجب تكوين [hMailServer]. تم تثبيت الخادم مع برنامج إدارة [hMailServer Administrator]:

- في [2]، في حقل الإدخال بشريط الحالة، اكتب [hmailserver]؛
- في [3]، قم بتشغيل برنامج الإدارة؛
- في [4]، قم بتوصيل برنامج الإدارة بخادم [hMailServer]؛
- في [5]، اكتب كلمة المرور التي أدخلتها أثناء تثبيت [hMailServer]؛

سنقوم بإنشاء حساب مستخدم:
- انقر بزر الماوس الأيمن على [Accounts] (7) ثم (8) لإضافة مستخدم جديد؛
- في علامة التبويب [General] (9)، نحدد مستخدمًا باسم [guest] (10) بكلمة مرور [guest] (11). سيكون لهذا المستخدم عنوان البريد الإلكتروني [guest@localhost] (10)؛
- في [12]، يتم تمكين المستخدم [guest]؛


- في [15]، نقوم بتكوين بروتوكول SMTP لخادم البريد؛
- في [16]، نقوم بتكوين تسليم البريد الإلكتروني؛
- في [17]، التكوين لتسليم البريد الإلكتروني إلى الجهاز المضيف (localhost)؛
- في [18]، اسم الجهاز المحلي (localhost). يتيح لك البرنامج النصي الموجود في قسم الروابط الحصول على هذا الاسم؛
- في [19]، نقوم بتكوين خادم ترحيل SMTP: هذا هو الخادم الذي سيتولى توزيع رسائل البريد الإلكتروني غير الموجهة إلى الجهاز المحلي (localhost)؛
- في [20]، خادم SMTP الخاص بـ Gmail. نستخدم Gmail لأننا أنشأنا حسابًا هناك في قسم الروابط؛
- في [21]، منفذ SMTP لـ Gmail؛
- في [22]، خدمة SMTP لـ Gmail هي خدمة آمنة: تحتاج إلى حساب Gmail للوصول إليها؛
- في [23]، المستخدم [php7parlexemple] الذي تم إنشاؤه في قسم "الرابط"؛
- في [24]، كلمة مرور هذا المستخدم: [PHP7parlexemple]، التي تم إنشاؤها في قسم "الرابط"؛
- في [25]، حدد نوع بروتوكول الأمان الذي يستخدمه Gmail؛

- في [27]، منفذ خدمة SMTP؛
- في [28]، لا تتطلب هذه الخدمة مصادقة؛
- في [30]، أدخل رسالة الترحيب التي سيرسلها خادم SMTP إلى عملائه؛
16.5.4. بروتوكول SMTP

سنستكشف بروتوكول SMTP باستخدام البيئة التالية:
- سيكون العميل A هو عميل TCP العام [RawTcpClient]؛
- سيكون الخادم B هو خادم البريد [hMailServer]؛
- سيطلب العميل A من الخادم B تسليم رسالة بريد إلكتروني إلى المستخدم [php7parlexemple@gmail.com]؛
- سنقوم بالتحقق من أن هذا المستخدم قد تلقى بالفعل رسالة البريد الإلكتروني المرسلة؛
نقوم بتشغيل العميل على النحو التالي:
![]()
- في [1]، نتصل بالمنفذ 25 على الجهاز المحلي، حيث تعمل خدمة SMTP لـ [hMailServer]. تشير الحجة [--quit bye] إلى أن المستخدم سيخرج من البرنامج عن طريق كتابة الأمر [bye]. بدون هذه الحجة، يكون الأمر لإنهاء البرنامج هو [quit]. ومع ذلك، فإن [quit] هو أيضًا أمر بروتوكول SMTP. لذلك يجب علينا تجنب هذا الغموض؛
- في [2]، تم توصيل العميل بنجاح؛
- في [3]، ينتظر العميل الأوامر التي يتم إدخالها من لوحة المفاتيح؛
- في [4]، يرسل الخادم رسالة ترحيب إلى العميل؛

- في [5]، يرسل العميل الأمر [EHLO client-machine-name]. يرد الخادم بسلسلة من الرسائل على شكل [250-xx] (6). يشير الرمز [250] إلى أن الأمر الذي أرسله العميل كان ناجحًا؛
- في [7]، يحدد العميل مرسل الرسالة، وهو هنا [guest@localhost]. يجب أن يكون هذا المستخدم موجودًا على خادم البريد [hMailServer]. وهذا هو الحال هنا لأننا أنشأنا هذا المستخدم مسبقًا؛
- في [8]، رد الخادم؛
- في [9]، يتم تحديد مستلم الرسالة، وهو هنا مستخدم Gmail [php7parlexemple@gmail.com]؛
- في [10]، رد الخادم؛
- في [11]، يُخبر الأمر [DATA] الخادم بأن العميل على وشك إرسال محتوى الرسالة؛
- في [12]، استجابة الخادم؛
- في [13-16]، يجب على العميل إرسال قائمة من الأسطر النصية تنتهي بسطر يحتوي على نقطة واحدة فقط. قد تحتوي الرسالة على أسطر [Subject:, From:, To:] (13) لتحديد موضوع الرسالة والمرسل والمستلم، على التوالي؛
- في [14]، يجب أن يتبع الرؤوس السابقة سطر فارغ؛
- في [15]، نص الرسالة؛
- في [16]، السطر الذي يحتوي على نقطة واحدة فقط، والذي يشير إلى نهاية الرسالة؛
- في [17]، بمجرد أن يتلقى الخادم السطر الذي يحتوي على نقطة واحدة فقط، يضع الرسالة في قائمة الانتظار؛
- في [18]، يُعلم العميل الخادم بأنه قد انتهى؛
- في [19]، رد الخادم؛
- في [20]، نرى أن الخادم قد أغلق الاتصال بالعميل؛
والآن دعونا نتأكد من أن المستخدم [php7parlexemple@gmail.com] قد تلقى الرسالة بالفعل:

- في [2]، نرى أن المستخدم [php7parlexemple@gmail.com] قد تلقى الرسالة بالفعل؛



- في [7]، نرى مرسل البريد الإلكتروني. ونلاحظ أنه ليس [guest@localhost]. ويرجع ذلك إلى أن خادم الترحيل المحدد في تكوين [hMailServer] هو الذي قام بتسليم الرسالة. ومع ذلك، فإن خادم الترحيل هذا هو [smtp.gmail.com]، المرتبط ببيانات اعتماد مستخدم Gmail [php7parlexemple@gmail.com]. ستظهر أي رسالة بريد إلكتروني قادمة من [hMailServer] على أنها قادمة من المستخدم [php7parlexemple@gmail.com]. هذا ليس ما أردناه هنا، ولكن إذا لم نستخدم خادم الترحيل هذا، فإن خدمة SMTP في Gmail ترفض رسائل البريد الإلكتروني المرسلة من [hMailServer] لأن SMTP في Gmail تتطلب مصادقة لا يوفرها [hMailServer]. من المحتمل أن تكون هناك طريقة للتغلب على هذه المشكلة، لكنني لم أجدها؛
- في [8]، نرى أن البريد الإلكتروني تم استلامه من الجهاز [DESKTOP-528I5CU] الذي يستضيف خادم البريد [hMailServer]؛
- في [9]، مرسل الرسالة. يمكننا أن نرى أنه ليس [guest@localhost]؛
- في [10]، المرسل الأصلي للرسالة. هذه المرة هو بالفعل [guest@localhost]؛
- في [11]، الموضوع؛
- في [12]، المستلم؛
- في [13]، الرسالة؛
أخيرًا، نجح [RawTcpClient] في إرسال الرسالة على الرغم من أننا واجهنا مشكلة مع المرسل. لدينا الآن الأساسيات اللازمة لإنشاء عميل SMTP مكتوب بلغة PHP.
16.5.5. عميل SMTP أساسي مكتوب بلغة PHP
سنقوم بتنفيذ ما تعلمناه سابقًا عن بروتوكول SMTP بلغة PHP.

يتم تكوين البرنامج النصي [smtp-01.php] بواسطة ملف JSON التالي [config-smtp-01.json]:
{
"mail to localhost via localhost": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "guest@localhost",
"subject": "to localhost via localhost",
"message": "ligne 1\nligne 2\nligne 3"
},
"mail to gmail via localhost": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "php7parlexemple@gmail.com",
"subject": "to gmail via localhost",
"message": "ligne 1\nligne 2\nligne 3"
},
"mail to gmail via gmail": {
"smtp-server": "smtp.gmail.com",
"smtp-port": "587",
"from": "guest@localhost",
"to": "php7parlexemple@gmail.com",
"subject": "to gmail via gmail",
"message": "ligne 1\nligne 2\nligne 3"
}
}
[config-smtp-01.json] عبارة عن مصفوفة حيث كل عنصر فيها عبارة عن قاموس من النوع [name=>info]. قيمة [info] هي في حد ذاتها قاموس يحتوي على المفاتيح والقيم التالية:
- [smtp-server]: اسم خادم SMTP المراد استخدامه؛
- [smtp-port]: رقم منفذ خدمة SMTP؛
- [from]: مرسل الرسالة؛
- [to]: مستلم الرسالة؛
- [subject]: موضوع الرسالة؛
- [message]: الرسالة المراد إرسالها؛
- يستخدم العنصر الأول خادم SMTP [localhost] لإرسال بريد إلكتروني إلى مستخدم على [localhost]؛
- يستخدم العنصر الثاني خادم SMTP [localhost] لإرسال بريد إلكتروني إلى مستخدم على [Gmail]؛
- يستخدم العنصر الثالث خادم SMTP [Gmail] لإرسال بريد إلكتروني إلى مستخدم على [Gmail]؛
فيما يلي كود [smtp-01.php] لعميل SMTP:
<?php
// client SMTP (SendMail Transfer Protocol) for sending a message
// communication protocol SMTP client-server
// -> client connects to smtp server port 25
// <- server sends him a welcome message
// -> customer sends command EHLO machine name
// <- server responds OK or not
// -> customer sends order MAIL FROM: <sender>
// <- server responds OK or not
// -> customer sends command RCPT TO: <recipient>
// <- server responds OK or not
// -> customer sends DATA command
// <- server responds OK or not
// -> client sends all the lines of its message and ends with a line containing the
// single character .
// <- server responds OK or not
// -> customer sends QUIT command
// <- server responds OK or not
// server responses have the form xxx text where xxx is a 3-digit number. All
// number xxx >=500 indicates an error.
// The answer may consist of several lines all beginning with xxx except the last one
// of the form xxx(space)
// text lines exchanged must end with the characters RC(#13) and LF(#10)
//
// client SMTP (SendMail Transfer Protocol) for sending a message
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// strict adherence to declared types of function parameters
declare (strict_types=1);
//
// mail settings
const CONFIG_FILE_NAME = "config-smtp-01.json";
// we retrieve the configuration
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// mail dispatch
foreach ($mails as $name => $infos) {
// follow-up
print "Envoi du mail [$name]\n";
// mail dispatch
$résultat = sendmail($name, $infos, TRUE);
// result display
print "$résultat\n";
}//for
// end
exit;
//sendmail
//-----------------------------------------------------------------------
function sendmail(string $name, array $infos, bool $verbose = TRUE): string {
// envoie message[$name,$infos]. If $verbose=TRUE , tracks client-server exchanges
// retrieve the customer's name
$client = gethostbyaddr(gethostbyname(""));
// open a connection with the SMTP server
$connexion = fsockopen($infos["smtp-server"], (int) $infos["smtp-port"]);
// return if error
if ($connexion === FALSE) {
return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
}
// $connexion represents a bidirectional communication flow
// between the client (this program) and the smtp server contacted
// this channel is used for the exchange of orders and information
// after connection, the server sends a welcome message which is read as follows
$erreur = sendCommand($connexion, "", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde EHLO
$erreur = sendCommand($connexion, "EHLO $client", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde MAIL FROM:
$erreur = sendCommand($connexion, sprintf("MAIL FROM: <%s>", $infos["from"]), $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde RCPT TO:
$erreur = sendCommand($connexion, sprintf("RCPT TO: <%s>", $infos["to"]), $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde DATA
$erreur = sendCommand($connexion, "DATA", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// prepare message to send
// it must contain the lines
// From: expéditeur
// To: recipient
// Subject:
// blank line
// Message
// .
$data = sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s\r\n.\r\n", $infos["from"], $infos["to"], $infos["subject"], $infos["message"]);
$erreur = sendCommand($connexion, $data, $verbose, FALSE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde quit
$erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// end
fclose($connexion);
return "Message envoyé";
}
// --------------------------------------------------------------------------
function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF): string {
// sends $commande to the $connexion channel
// verbose mode if $verbose=1
// if $withRCLF=1, adds sequence RCLF to exchange
// data
if ($withRCLF) {
$RCLF = "\r\n";
} else {
$RCLF = "";
}
// send cmde if $commande not empty
if ($commande!=="") {
fputs($connexion, "$commande$RCLF");
// possible echo
if ($verbose) {
affiche($commande, 1);
}
}//if
// reading response
$réponse = fgets($connexion, 1000);
// possible echo
if ($verbose) {
affiche($réponse, 2);
}
// error code recovery
$codeErreur = (int) substr($réponse, 0, 3);
// last line of the answer?
while (substr($réponse, 3, 1) === "-") {
// reading response
$réponse = fgets($connexion, 1000);
// possible echo
if ($verbose) {
affiche($réponse, 2);
}
}//while
// answer completed
// error returned by the server?
if ($codeErreur >= 500) {
return substr($réponse, 4);
}
// error-free return
return "";
}
// --------------------------------------------------------------------------
function affiche($échange, $sens) {
// displays $échange on screen
// if $sens=1 displays -->$echange
// if $sens=2 displays <-- $échange without the last 2 characters RCLF
switch ($sens) {
case 1:
print "--> [$échange]\n";
break;
case 2:
$L = strlen($échange);
print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
break;
}//switch
}
تعليقات
- السطر 39: تتم معالجة ملف التكوين؛
- السطر 42: نقوم بتكرار عناصر مصفوفة [mails]. كل عنصر عبارة عن قاموس [name=>infos] حيث [name] هو أي اسم و[infos] هو قاموس يحتوي على المعلومات اللازمة لإرسال بريد إلكتروني؛
- السطر 46: يتم إرسال البريد الإلكتروني باستخدام الدالة [sendmail]، التي تأخذ ثلاثة معلمات:
- $name: الاسم الممنوح لهذه الرسالة الإلكترونية؛
- $infos: القاموس الذي يحتوي على المعلومات اللازمة لإرسال البريد الإلكتروني؛
- verbose: قيمة منطقية تشير إلى ما إذا كان يجب تسجيل تبادلات العميل/الخادم على وحدة التحكم؛
- السطر 46: تُرجع الدالة [sendmail] رسالة خطأ فارغة إذا لم يحدث أي خطأ؛
- السطر 56: ترسل دالة [sendmail] الأوامر المختلفة التي يجب على عميل SMTP إرسالها:
- الأسطر 77–84: الأمر EHLO؛
- الأسطر 85–92: الأمر MAIL FROM:؛
- الأسطر 93–100: الأمر RCPT TO:؛
- الأسطر 101–108: الأمر DATA؛
- الأسطر 117–124: إرسال الرسالة (From، To، Subject، text)؛
- الأسطر 125–132: الأمر QUIT؛
- السطر 140: وظيفة [sendCommand] مسؤولة عن إرسال أوامر العميل إلى خادم SMTP. وهي تقبل أربعة معلمات:
- [$connection]: الاتصال الذي يربط العميل بالخادم؛
- [$command]: الأمر المراد إرساله؛
- [$verbose]: إذا كانت القيمة TRUE، يتم تسجيل تبادلات العميل/الخادم في وحدة التحكم؛
- [$withRCLF]: إذا كانت القيمة TRUE، يتم إرسال الأمر منتهياً بتسلسل \r\n. وهذا مطلوب لجميع أوامر بروتوكول SMTP، ولكن [sendCommand] تُستخدم أيضًا لإرسال الرسالة. في هذه الحالة، لا يُضاف تسلسل \r\n؛
- الأسطر 150–157: يتم إرسال الأمر إلى الخادم؛
- الأسطر 158–163: قراءة السطر الأول من الرد. قد يتكون الرد من عدة أسطر. كل سطر له الشكل XXX-YYY، حيث XXX هو رمز رقمي، باستثناء السطر الأخير من الرد، الذي له الشكل XXX YYY (بدون الواصلة)؛
- الأسطر 167–174: قراءة جميع أسطر الرد؛
- السطر 177: إذا كان الرمز الرقمي XXX أكبر من 500، فهذا يعني أن الخادم قد أرجع خطأً؛
النتائج
يؤدي تنفيذ البرنامج النصي إلى إخراج وحدة التحكم التالي:
Envoi du mail [mail to localhost via localhost]
<-- [220 Bienvenue sur sergetahe@localhost]
--> [EHLO DESKTOP-528I5CU.home]
<-- [250-DESKTOP-528I5CU]
<-- [250-SIZE 20480000]
<-- [250-AUTH LOGIN]
<-- [250 HELP]
--> [MAIL FROM: <guest@localhost>]
<-- [250 OK]
--> [RCPT TO: <guest@localhost>]
<-- [250 OK]
--> [DATA]
<-- [354 OK, send.]
--> [From: guest@localhost
To: guest@localhost
Subject: to localhost via localhost
ligne 1
ligne 2
ligne 3
.
]
<-- [250 Queued (0.016 seconds)]
--> [QUIT]
<-- [221 goodbye]
Message envoyé
Envoi du mail [mail to gmail via localhost]
<-- [220 Bienvenue sur sergetahe@localhost]
--> [EHLO DESKTOP-528I5CU.home]
<-- [250-DESKTOP-528I5CU]
<-- [250-SIZE 20480000]
<-- [250-AUTH LOGIN]
<-- [250 HELP]
--> [MAIL FROM: <guest@localhost>]
<-- [250 OK]
--> [RCPT TO: <php7parlexemple@gmail.com>]
<-- [250 OK]
--> [DATA]
<-- [354 OK, send.]
--> [From: guest@localhost
To: php7parlexemple@gmail.com
Subject: to gmail via localhost
ligne 1
ligne 2
ligne 3
.
]
<-- [250 Queued (0.000 seconds)]
--> [QUIT]
<-- [221 goodbye]
Message envoyé
Envoi du mail [mail to gmail via gmail]
<-- [220 smtp.gmail.com ESMTP d9sm21623375wro.26 - gsmtp]
--> [EHLO DESKTOP-528I5CU.home]
<-- [250-smtp.gmail.com at your service, [90.93.230.110]]
<-- [250-SIZE 35882577]
<-- [250-8BITMIME]
<-- [250-STARTTLS]
<-- [250-ENHANCEDSTATUSCODES]
<-- [250-PIPELINING]
<-- [250-CHUNKING]
<-- [250 SMTPUTF8]
--> [MAIL FROM: <guest@localhost>]
<-- [530 5.7.0 Must issue a STARTTLS command first. d9sm21623375wro.26 - gsmtp]
5.7.0 Must issue a STARTTLS command first. d9sm21623375wro.26 - gsmtp
Done.
- الأسطر 1-26: استخدام خادم SMTP [hMailServer] لإرسال بريد إلكتروني إلى [guest@localhost] يعمل بشكل جيد؛
- الأسطر 27-52: استخدام خادم SMTP [hMailServer] لإرسال بريد إلكتروني إلى [php7parlexemple@gmail.com] يعمل بشكل جيد؛
- الأسطر 53-65: استخدام خادم SMTP [Gmail] لإرسال بريد إلكتروني إلى [php7parlexemple@gmail.com] لا يعمل بشكل جيد: في السطر 65، يعرض خادم SMTP رمز الخطأ 530 مع رسالة الخطأ. يشير هذا إلى أن عميل SMTP يجب أن يقوم أولاً بالمصادقة عبر اتصال آمن. لم يقم عميلنا بذلك، وبالتالي تم رفضه؛
16.5.6. عميل SMTP ثانٍ مكتوب باستخدام مكتبة [SwiftMailer]
يوجد عيبان على الأقل في العميل السابق:
- لا يمكنه استخدام اتصال آمن إذا كان الخادم يتطلب ذلك؛
- لا يمكنه إرفاق ملفات بالرسالة؛
في البرنامج النصي الجديد، سنستخدم مكتبة [SwiftMailer] [https://swiftmailer.symfony.com/] (مايو 2019). توجد إجراءات تثبيت [SwiftMailer] على الرابط [https://swiftmailer.symfony.com/docs/introduction.HTML] (مايو 2019).
أولاً، قم بتشغيل Laragon:

- في [1]، افتح محطة طرفية؛

- في [3]، تأكد من أنك في المجلد [<laragon>/www]، حيث <laragon> هو مجلد تثبيت Laragon؛
- في [3]، اكتب الأمر الموضح (مايو 2019). تحقق من الأمر الدقيق على الرابط [https://swiftmailer.symfony.com/docs/introduction.HTML]؛
- في [4]، يشير إلى أنه لم يتم إجراء أي تثبيت أو تحديث. وذلك لأن المكتبة كانت مثبتة بالفعل على هذا الجهاز؛
- في [5]، دليل التثبيت لـ [swiftmailer] [6]؛
- في [7]، ملف سنحتاجه في البرنامج النصي الخاص بنا؛
بمجرد الانتهاء من ذلك، تحقق من أن المجلد [<laragon>/www/vendor] [5] مدرج في [مسار التضمين] في NetBeans (انظر القسم المرتبط).
أخيرًا، تتطلب مكتبة [SwiftMailer] تمكين ملحق PHP [mbstring]. للقيام بذلك، تحقق من ملف [php.ini] (انظر القسم المرتبط):

سيستخدم البرنامج النصي [smtp-02.php] ملف التكوين JSON التالي [config-smtp-02.json]:
توجد نفس الحقول الموجودة في ملف [config-smtp-01.json]، مع حقلين إضافيين:
- [tls]: إذا تم تعيينه على TRUE، فهذا يشير إلى أنه يجب استخدام اتصال آمن مع خادم SMTP. إذا تم تعيين [tls] على TRUE، فيجب إضافة حقلين إضافيين:
- [user]: اسم المستخدم المستخدم لتوثيق الاتصال؛
- [password]: كلمة المرور الخاصة به؛
في مثالنا، استخدمنا بيانات اعتماد المستخدم [php7parlexemple@gmail.com] للاتصال بخادم Gmail. استخدم بيانات اعتمادك الخاصة؛
- [المرفقات]: تحدد أسماء الملفات المراد إرفاقها بالبريد الإلكتروني؛
فيما يلي كود البرنامج النصي [smtp-02.php]:
<?php
// client SMTP (SendMail Transfer Protocol) for sending a message
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
//
// mail settings
const CONFIG_FILE_NAME = "config-smtp-02.json";
// we retrieve the configuration
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// mail dispatch
foreach ($mails as $name => $infos) {
// follow-up
print "Envoi du mail [$name]\n";
// mail dispatch
$résultat = sendmail($name, $infos);
// result display
print "$résultat\n";
}//for
// end
exit;
//-----------------------------------------------------------------------
function sendmail($name, $infos) {
// sends $infos[message] to smtp server $infos[smtp-server] on port $infos[smt-port]
// if $infos[tls] is true, support TLS will be used
// the mail is sent from $infos[from]
// for the recipient $infos['to']
// Document $info[attachment] is attached to the message
// message has subject $infos[subject]
//
// message in HTML format
$messageHTML = str_replace("\n", "<br/>", $infos["message"]);
try {
// message creation
$message = (new \Swift_Message())
// message subject
->setSubject($infos["subject"])
// sender
->setFrom($infos["from"])
// recipients with a dictionary (setTo/setCc/setBcc)
->setTo($infos["to"])
// message text
->setBody($infos["message"])
// html variant
->addPart("<b>$messageHTML</b>", 'text/html')
;
// attachments
foreach ($infos["attachments"] as $attachment) {
// path of attachment
$fileName = __DIR__ . $attachment;
// check that the file exists
if (file_exists($fileName)) {
// attach the document to the message
$message->attach(\Swift_Attachment::fromPath($fileName));
} else {
// error
print "L'attachement [$fileName] n'existe pas\n";
}
}
// protocol TLS ?
if ($infos["tls"] === "TRUE") {
// TLS
$transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"], 'tls'))
->setUsername($infos["user"])
->setPassword($infos["password"]);
} else {
// no TLS
$transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"]));
}
// the shipment manager
$mailer = new \Swift_Mailer($transport);
// sending the message
$result = $mailer->send($message);
// end
return "Message [$name] envoyé";
} catch (\Throwable $ex) {
// error
return "Erreur lors de l'envoi du message [$name] : " . $ex->getMessage();
}
}
تعليقات
- السطر 10: نقوم بتحميل ملف [autoload.php] الموجود في المجلد [<laragon>/www/vendor]، حيث يمثل <laragon> مجلد تثبيت Laragon. سيقوم هذا الملف تلقائيًا بتحميل ملفات تعريف فئات SwiftMailer عند استخدام تلك الفئات لأول مرة. وهذا يوفر علينا الحاجة إلى تضمين عدد من عبارات [require] يساوي عدد فئات وواجهات SwiftMailer التي سنستخدمها؛
- السطر 32: الدالة الجديدة [sendmail]، التي تحتوي على معلمتين:
- [$name]، والتي تُستخدم للتمييز بين الرسائل؛
- [$infos]: المعلومات اللازمة لإرسال الرسالة إلى مستلمها؛
- السطر 42: سيكون لدينا نسختان من الرسالة: واحدة بنص عادي والأخرى بتنسيق HTML. هنا، نستبدل فواصل الأسطر برمز HTML <br/>؛
- الأسطر 45-69: نحدد الرسالة باستخدام فئة [\SwiftMessage]؛
- السطر 47: تُستخدم طريقة [SwiftMessage→setSubject] لتعيين موضوع الرسالة؛
- السطر 49: تُستخدم طريقة [SwiftMessage→setFrom] لتعيين مرسل الرسالة؛
- السطر 51: تُستخدم طريقة [SwiftMessage→setTo] لتعيين مستلم الرسالة؛
- السطر 53: تُستخدم طريقة [SwiftMessage→setBody] لتعيين نص الرسالة؛
- السطر 55: تُستخدم طريقة [SwiftMessage→addPart] لتعيين إصدارات مختلفة من الرسالة، وفي هذه الحالة تكون الرسالة بتنسيق HTML. عندما تحتوي الرسالة على متغيرات، تعرض برامج البريد الإلكتروني المتغير المفضل لدى المستخدم؛
- الأسطر 58–69: تسمح لك طريقة [SwiftMessage→addAttachment] (64) بإرفاق ملف بالرسالة؛
- الأسطر 70–79: بمجرد تحديد الرسالة المراد إرسالها، يجب تحديد كيفية إرسالها. يتم تحديد وضع نقل الرسالة بواسطة فئة [\Swift_SmtpTransport]. يجب توفير معلومتين على الأقل: اسم خادم SMTP ومنفذه. وهناك معلومة ثالثة: هل يتطلب خادم SMTP مصادقة آمنة؟
- الأسطر 73-75: مثيل [\Swift_SmtpTransport] لاتصال آمن بخادم SMTP؛
- السطر 78: مثيل [\Swift_SmtpTransport] لاتصال غير آمن بخادم SMTP؛
- السطر 81: ترسل فئة [\SwiftMailer] الرسائل. يجب تمرير وضع النقل المختار إليها؛
- السطر 83: يتم إرسال رسالة [\SwiftMessage] عبر [\Swift_SmtpTransport] المحدد. تُرجع طريقة [SwiftMailer→send] القيمة FALSE إذا تعذر إرسال الرسالة؛
- الأسطر 86–89: ترمي مكتبة [SwiftMailer] استثناءً بمجرد حدوث خطأ ما؛
ملاحظة: لاحظ أن مساحة الاسم للفئات في مكتبة [SwiftMailer] هي الجذر \. وقد أشرنا صراحةً إلى الفئات [\SwiftMessage، \Swift_SmtpTransport، \SwiftMailer] لتذكيرك بذلك؛
النتائج
عند تشغيل البرنامج النصي [smtp-02.php]، يتم عرض الناتج التالي على وحدة التحكم:
إذا تحققنا من حساب Gmail الخاص بالمستخدم [php7parlexemple]، فسنرى ما يلي:

- في [1]، الموضوع؛
- في [2]، المرسل؛
- في [3]، المستلم؛
- في [4]، الرسالة؛
- في [5-10]، المرفقات؛
إذا طلبت عرض الرسالة الأصلية، فستحصل على المستند التالي:
Return-Path: <php7parlexemple@gmail.com>
Received: from [127.0.0.1] (lfbn-1-11924-110.w90-93.abo.wanadoo.fr. [90.93.230.110])
by smtp.gmail.com with ESMTPSA id e14sm7773816wma.41.2019.05.26.03.11.53
for <php7parlexemple@gmail.com>
(version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
Sun, 26 May 2019 03:11:54 -0700 (PDT)
Message-ID: <e613c47a421a66e2cf7f8e319616ec49@swift.generated>
Date: Sun, 26 May 2019 10:11:53 +0000
Subject: test-gmail-via-gmail
From: php7parlexemple@gmail.com
To: php7parlexemple@gmail.com
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: multipart/alternative; boundary="_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_"
--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
ligne 1
ligne 2
ligne 3
--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_
Content-Type: text/HTML; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<b>ligne 1<br/>ligne 2<br/>ligne 3</b>
--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_--
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document; name="Hello from SwiftMailer.docx"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.docx"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: application/pdf; name="Hello from SwiftMailer.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.pdf"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: application/vnd.oasis.opendocument.text; name="Hello from SwiftMailer.odt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.odt"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: image/png; name="Cours-Tutoriels-Serge-Tahé-1568x268.png"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Cours-Tutoriels-Serge-Tahé-1568x268.png"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: message/rfc822; name=test-localhost.eml
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=test-localhost.eml
Return-Path: guest@localhost
Received: from [127.0.0.1] (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Sat, 25 May 2019 09:48:23 +0200
Message-ID: <620f4628882b011feebe4faa30b45092@swift.generated>
Date: Sat, 25 May 2019 07:48:22 +0000
Subject: test-localhost
From: guest@localhost
To: guest@localhost
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_"
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: multipart/alternative; boundary="_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_"
--_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
j'ai =C3=A9t=C3=A9 invit=C3=A9 =C3=A0 d=C3=A9je=C3=BBner
--_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_
Content-Type: text/HTML; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<b>j'ai =C3=A9t=C3=A9 invit=C3=A9 =C3=A0 d=C3=A9je=C3=BBner</b>
--_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_--
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document; name="Hello from SwiftMailer.docx"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.docx"
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: application/pdf; name="Hello from SwiftMailer.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.pdf"
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: application/vnd.oasis.opendocument.text; name="Hello from SwiftMailer.odt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.odt"
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: image/png; name="Cours-Tutoriels-Serge-Tahé-1568x268.png"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Cours-Tutoriels-Serge-Tahé-1568x268.png"
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_--
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_--
- السطر 9: الموضوع؛
- السطر 10: المرسل؛
- السطر 11: المستلم؛
- السطر 13: تحتوي الرسالة على عدة أقسام محددة بعلامات [--_=_swift_xx]؛
- الأسطر 19–24: الرسالة بنص عادي؛
- الأسطر 27–30: الرسالة بتنسيق HTML؛
- الأسطر 34-36: الملف المرفق [Hello from SwiftMailer.docx]؛
- الأسطر 40-42: الملف المرفق [Hello from SwiftMailer.pdf]؛
- الأسطر 46–48: الملف المرفق [Hello from SwiftMailer.odt]؛
- الأسطر 58-60: الملف المرفق [Cours-Tutoriels-Serge-Tahé-1568x268.png]؛
- الأسطر 58–60: الملف المرفق [test-localhost.eml]؛
- الأسطر 62–114: الملف المرفق [test-localhost.eml] هو في حد ذاته رسالة يُعرض محتواها في الأسطر 62–114. لاحظ أن هذه الرسالة نفسها تحتوي على مرفقات؛
16.6. بروتوكولا POP3 (بروتوكول مكتب البريد) و IMAP (بروتوكول الوصول إلى رسائل الإنترنت)
16.6.1. مقدمة
يوجد بروتوكولان لقراءة رسائل البريد الإلكتروني المخزنة على خادم البريد:
- بروتوكول POP3 (بروتوكول مكتب البريد)، وهو أول بروتوكول تاريخيًا ولكنه نادرًا ما يُستخدم اليوم؛
- بروتوكول IMAP (بروتوكول الوصول إلى رسائل الإنترنت)، وهو أحدث من بروتوكول POP3 والأكثر استخدامًا حاليًا؛
لاستكشاف بروتوكول POP3، سنستخدم البنية التالية:

- [الخادم B] سيكون خادم POP3/IMAP محلي، يتم تنفيذه بواسطة خادم البريد [hMailServer]؛
- [العميل A] سيكون عميل POP3/IMAP بأشكال مختلفة:
- عميل [RawTcpClient] لاستكشاف بروتوكول POP3؛
- برنامج نصي PHP يحاكي بروتوكول POP3 الخاص بعميل [RawTcpClient]؛
- نص برمجي PHP يستخدم مكتبة PHP IMAP، مما يسمح بتنفيذ كل من عملاء IMAP و POP3؛
16.6.2. استكشاف بروتوكول POP3
أولاً، نستخدم البرنامج النصي [smtp-01.php] لإرسال بريد إلكتروني إلى المستخدم [guest@localhost]. إذا كنت قد أجريت الاختبارات المرتبطة بالبرنامج النصي، فمن المفترض أن يكون هذا المستخدم قد تلقى رسائل بريد إلكتروني، لكننا لم نتمكن من التحقق من ذلك. لإرسال بريد إلكتروني جديد إليهم، استخدم ملف التكوين التالي [config-smtp-01.json]، على سبيل المثال:
الآن دعونا نرى كيف يمكننا قراءة صندوق بريد المستخدم [guest@localhost] باستخدام عميل [RawTcpClient]:
C:\Data\st-2019\dev\php7\php5-exemples\exemples\inet\utilitaires>RawTcpClient --quit bye localhost 110
Client [DESKTOP-528I5CU:55593] connecté au serveur [localhost-110]
Tapez vos commandes (bye pour arrêter) :
<-- [+OK Bienvenue sur sergetahe@localhost]
USER guest@localhost
<-- [+OK Send your password]
PASS guest
<-- [+OK Mailbox locked and ready]
LIST
<-- [+OK 2 messages (610 octets)]
<-- [1 305]
<-- [2 305]
<-- [.]
RETR 1
<-- [+OK 305 octets]
<-- [Return-Path: guest@localhost]
<-- [Received: from DESKTOP-528I5CU.home (localhost [127.0.0.1])]
<-- [ by DESKTOP-528I5CU with ESMTP]
<-- [ ; Tue, 21 May 2019 12:59:11 +0200]
<-- [Message-ID: <1356373A-33C9-4F31-BA43-2B119E128CE3@DESKTOP-528I5CU>]
<-- [From: guest@localhost]
<-- [To: guest@localhost]
<-- [Subject: to localhost via localhost]
<-- []
<-- [ligne 1]
<-- [ligne 2]
<-- [ligne 3]
<-- [.]
DELE 1
<-- [+OK msg deleted]
LIST
<-- [+OK 1 messages (305 octets)]
<-- [2 305]
<-- [.]
DELE 2
<-- [+OK msg deleted]
LIST
<-- [+OK 0 messages (0 octets)]
<-- [.]
QUIT
<-- [+OK POP3 server saying goodbye…]
Perte de la connexion avec le serveur…
- السطر 1: يستخدم خادم POP3 عادةً المنفذ 110. وهذا هو الحال هنا؛
- السطر 5: يُستخدم الأمر [USER] لتحديد المستخدم الذي تريد قراءة صندوق بريده؛
- السطر 7: يُستخدم الأمر [PASS] لتحديد كلمة المرور؛
- السطر 9: يطلب الأمر [LIST] قائمة بالرسائل الموجودة في صندوق بريد المستخدم؛
- السطر 14: يطلب الأمر [RETR] الرسالة المحددة بالرقم؛
- السطر 29: يقوم الأمر [DELE] بحذف الرسالة التي تم تحديد رقمها؛
- السطر 40: الأمر [QUIT] يُعلم الخادم بأنك قد انتهيت؛
يمكن أن تأخذ استجابة الخادم عدة أشكال:
- سطر واحد يبدأ بـ [+OK] للإشارة إلى نجاح الأمر السابق للعميل؛
- سطر واحد يبدأ بـ [-ERR] للإشارة إلى فشل الأمر السابق للعميل؛
- أسطر متعددة حيث:
- يبدأ السطر الأول بـ [+OK]؛
- يتكون السطر الأخير من نقطة واحدة؛
16.6.3. نص برمجي أساسي لتنفيذ بروتوكول POP3

نظرًا لأن بروتوكول POP3 له نفس بنية بروتوكول SMTP، فإن البرنامج النصي [pop3-01.php] هو نسخة معدلة من البرنامج النصي [smtp-01.php]. وسيكون له ملف التكوين التالي [config-pop3-01.json]:
- السطران 3-4: خادم POP3 الذي يتم الاستعلام عنه هو الخادم المحلي [hMailServer]؛
- السطران 5-6: نريد قراءة صندوق بريد المستخدم [guest@localhost]؛
- السطر 7: سنقرأ ما يصل إلى 5 رسائل بريد إلكتروني؛
النص البرمجي [pop3-01.php] هو كما يلي:
<?php
// client POP3 (Post Office Protocol) for reading mailbox messages
// POP3 client-server communication protocol
// -> client connects to smtp server port 110
// <- server sends him a welcome message
// -> customer sends command USER user
// <- server responds OK or not
// -> customer sends PASS mot_de_passe order
// <- server responds OK or not
// -> customer sends LIST command
// <- server responds OK or not
// -> customer sends command RETR n° for each email
// <- server responds OK or not. If OK sends the requested mail content
// -> server sends all the mail lines and ends with a line containing the
// single character .
// -> customer sends command DELE n° to delete an e-mail
// <- server responds OK or not
// // -> client sends QUIT command to end dialog with server
// <- server responds OK or not
// server responses have the form +OK text where -ERR text
// The answer may consist of several lines. In this case, the last line consists of a single dot
// text lines exchanged must end with the characters RC(#13) and LF(#10)
//
// POP3 client (SendMail Transfer Protocol) for reading e-mails
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// strict adherence to declared types of function parameters
declare (strict_types=1);
//
// mail settings
const CONFIG_FILE_NAME = "config-pop3-01.json";
// we retrieve the configuration
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// letterbox reading
foreach ($mailboxes as $name => $infos) {
// follow-up
print "Lecture de la boîte à lettres [$name]\n";
// letterbox reading
$résultat = readmail($name, $infos, TRUE);
// result display
print "$résultat\n";
}//for
// end
exit;
//readmail
//-----------------------------------------------------------------------
function readmail(string $name, array $infos, bool $verbose = TRUE): string {
// reads the contents of the mailbox [$name]
// import all messages
// each message is deleted afterb being read
// If $verbose=1, tracks client-server exchanges
//
// open a connection with the SMTP server
$connexion = fsockopen($infos["server"], (int) $infos["port"]);
// return if error
if ($connexion === FALSE) {
return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
}
// $connexion represents a bidirectional communication flow
// between the client (this program) and the pop3 server contacted
// this channel is used for the exchange of orders and information
// after connection, the server sends a welcome message which is read as follows
$erreur = sendCommand($connexion, "", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde USER
$erreur = sendCommand($connexion, "USER {$infos["user"]}", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde PASS
$erreur = sendCommand($connexion, "PASS {$infos["password"]}", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde LIST
$premièreLigne = "";
$erreur = sendCommand($connexion, "LIST", $verbose, TRUE, $premièreLigne);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// analyze 1st line to determine number of messages
$champs = [];
preg_match("/^\+OK (\d+)/", $premièreLigne, $champs);
$nbMessages = (int) $champs[1];
// we loop on the messages
$iMessage = 0;
while ($iMessage < $nbMessages && $iMessage < $infos["maxmails"]) {
// cmde RETR
$erreur = sendCommand($connexion, "RETR " . ($iMessage + 1), $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde DELE
$erreur = sendCommand($connexion, "DELE " . ($iMessage + 1), $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// next msg
$iMessage++;
}
// cmde QUIT
$erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// end
fclose($connexion);
return "Terminé";
}
// --------------------------------------------------------------------------
function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF, string &$premièreLigne = ""): string {
// sends $commande to the $connexion channel
// verbose mode if $verbose=1
// if $withRCLF=1, adds sequence RCLF to exchange
// puts the 1st line of the answer in [$premièreLigne]
// ]
// data
if ($withRCLF) {
$RCLF = "\r\n";
} else {
$RCLF = "";
}
// send cmde if $commande not empty
if ($commande !== "") {
fputs($connexion, "$commande$RCLF");
// possible echo
if ($verbose) {
affiche($commande, 1);
}
}//if
// reading response
$réponse = fgets($connexion, 1000);
// memorize the 1st line
$premièreLigne = $réponse;
// possible echo
if ($verbose) {
affiche($réponse, 2);
}
// error code recovery
$codeErreur = substr($réponse, 0, 1);
if ($codeErreur === "-") {
// there has been an error
return substr($réponse, 5);
}
// special cases of cmdes RETR and LIST with multi-line responses
$commande = substr(strtolower($commande), 0, 4);
if ($commande === "list" || $commande === "retr") {
// last line of the answer?
$champs = [];
$match = preg_match("/^\.\s+$/", $réponse, $champs);
while (!$match) {
// reading response
$réponse = fgets($connexion, 1000);
// possible echo
if ($verbose) {
affiche($réponse, 2);
}
// response analysis
$champs = [];
$match = preg_match("/^\.\s+$/", $réponse, $champs);
}//while
}
// error-free return
return "";
}
// --------------------------------------------------------------------------
function affiche($échange, $sens) {
// displays $échange on screen
// if $sens=1 displays -->$echange
// if $sens=2 displays <-- $échange without last 2 characters RCLF
switch ($sens) {
case 1:
print "--> [$échange]\n";
break;
case 2:
$L = strlen($échange);
print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
break;
}//switch
}
تعليقات
كما ذكرنا، [pop3-01.php] هو نسخة معدلة من البرنامج النصي [smtp-01.php] الذي ناقشناه سابقًا. سنكتفي بالتعليق على الاختلافات الرئيسية:
- السطر 55: وظيفة [readmail] مسؤولة عن قراءة رسائل البريد الإلكتروني من صندوق البريد. يتم تخزين بيانات اعتماد تسجيل الدخول لهذا الصندوق في قاموس [$infos]؛
- الأسطر 61–66: إنشاء اتصال بخادم POP3؛
- الأسطر 71–77: قراءة رسالة الترحيب المرسلة من الخادم؛
- الأسطر 78–85: يتم إرسال الأمر [USER] لتحديد المستخدم الذي نريد رسائل بريده الإلكتروني؛
- الأسطر 86–93: إرسال الأمر [PASS] لتوفير كلمة مرور المستخدم؛
- الأسطر 94-102: إرسال الأمر [LIST] لتحديد عدد رسائل البريد الإلكتروني الموجودة في صندوق بريد هذا المستخدم.
- السطر 96: إضافة المعلمة [$firstLine] إلى معلمات دالة [readmail]. في السطر الأول من رد الخادم على الأمر LIST، يشير الخادم إلى عدد الرسائل الموجودة في صندوق البريد؛
- الأسطر 104–106: استرجاع عدد الرسائل من السطر الأول من الرد؛
- الأسطر 109–128: نقوم بتكرار كل رسالة. لكل رسالة، نصدر أمرين:
- RETR i: لاسترداد الرسالة رقم i (الأسطر 111–117)؛
- DELE i: لحذفها بمجرد قراءتها (الأسطر 118–125)؛
- الأسطر 129–136: يتم إرسال الأمر [QUIT] لإعلام الخادم بأننا انتهينا؛
- الأسطر 178–194: بالنسبة لأوامر [LIST] و [RETR]، يمتد رد الخادم على عدة أسطر، حيث يتكون السطر الأخير من نقطة واحدة؛
النتائج
عند التنفيذ، يتم الحصول على النتائج التالية:
Lecture de la boîte à lettres [localhost:110]
<-- [+OK Bienvenue sur sergetahe@localhost]
--> [USER guest@localhost]
<-- [+OK Send your password]
--> [PASS guest]
<-- [+OK Mailbox locked and ready]
--> [LIST]
<-- [+OK 1 messages (305 octets)]
<-- [1 305]
<-- [.]
--> [RETR 1]
<-- [+OK 305 octets]
<-- [Return-Path: guest@localhost]
<-- [Received: from DESKTOP-528I5CU.home (localhost [127.0.0.1])]
<-- [ by DESKTOP-528I5CU with ESMTP]
<-- [ ; Tue, 21 May 2019 14:25:39 +0200]
<-- [Message-ID: <5F912826-F9C4-41B6-BDA7-4A29537781C9@DESKTOP-528I5CU>]
<-- [From: guest@localhost]
<-- [To: guest@localhost]
<-- [Subject: to localhost via localhost]
<-- []
<-- [ligne ]
<-- [ligne ]
<-- [ligne 3]
<-- [.]
--> [DELE 1]
<-- [+OK msg deleted]
--> [QUIT]
<-- [+OK POP3 server saying goodbye…]
Terminé
Done.
لدينا هنا عميل POP3 أساسي يفتقر إلى بعض القدرات:
- القدرة على التواصل مع خادم POP3 آمن؛
- القدرة على قراءة المرفقات في الرسالة؛
سنقوم بتنفيذ الميزة الأولى باستخدام وظائف [imap] في PHP.
16.6.4. عميل POP3/IMAP تم تنفيذه باستخدام وظائف [imap] في PHP
أولاً، نحتاج إلى التحقق من توفر وظائف [imap] في إصدار PHP الذي نستخدمه. نفتح ملف [php.ini] الموصوف في القسم المرتبط ونبحث عن الأسطر التي تذكر [imap]:

السطر 895: تحقق من تمكين ملحق [imap].
سيستخدم البرنامج النصي [imap-01.php] ملف JSON التالي [config-imap-01.json]:
يحدد ملف [config-imap-01.json] مجموعة من خوادم IMAP/POP3 التي يجب الاتصال بها. كل عنصر عبارة عن بنية [مفتاح:قيمة]، حيث:
- [key]: هو الخادم المراد الاتصال به. لدينا خادمان هنا:
- [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]: يشير إلى خادم [imap.gmail.com] الذي يستمع على المنفذ 993. بروتوكول العميل/الخادم هو IMAP. تشير المعلمة /ssl إلى أن الاتصال بين العميل والخادم آمن. المعلمة /novalidate-cert توجه العميل بعدم التحقق من شهادة الأمان التي سيرسلها الخادم. أخيرًا، يدير خادم IMAP مجموعة من صناديق البريد لمستخدم واحد. من خلال تحديد INBOX في عنوان URL لخادم IMAP، نشير إلى أننا مهتمون بصندوق البريد المسمى INBOX، والذي عادةً ما تصل إليه الرسائل الجديدة؛
- [{localhost:110/pop3}INBOX]: يشير إلى خادم [localhost] الذي يستمع على المنفذ 110. بروتوكول العميل/الخادم هنا هو POP3؛
- [value]: هو قاموس يحدد النقاط التالية:
- [imap-server]: اسم خادم IMAP أو POP3؛
- [imap-port]: منفذ خادم IMAP أو POP3؛
- [user]: المالك الذي تريد قراءة صندوق بريده؛
- [password]: كلمة المرور الخاصة بهم؛
- [output-dir]: المجلد الذي يجب حفظ الرسائل فيه؛
- [prefix]: بادئة اسم الملف للرسائل، والتي ستكون على شكل prefixN، حيث N هو رقم الرسالة؛
- [pop3]: قيمة منطقية تُعيَّن إلى TRUE للإشارة إلى أن البروتوكول المستخدم هو POP3. في هذه الحالة، بعد قراءة الرسالة، سيتم حذفها. هذه هي الطريقة التي تعمل بها خوادم POP3 عادةً: لا يتم الاحتفاظ بالرسالة المقروءة على الخادم؛
النص البرمجي [imap-01.php] هو كما يلي:
<?php
// IMAP (Internet Message Access Protocol) client for reading e-mails
//
// strict adherence to declared types of function parameters
declare (strict_types=1);
// error management
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
//
// mail reading parameters
const CONFIG_FILE_NAME = "config-imap-01.json";
// we retrieve the configuration
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// reading mailboxes
foreach ($mailboxes as $name => $infos) {
// follow-up
print "------------Lecture de la boîte à lettres [$name]\n";
// reading the mailbox
readmailbox($name, $infos);
}
// end
exit;
//-----------------------------------------------------------------------
function readmailbox(string $name, array $infos): void {
// Connection attempt
$imapResource = imap_open($name, $infos["user"], $infos["password"]);
// Test on the return of the imap_open() function
if (!$imapResource) {
// Failure
print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
} else {
// Connection established
print "Connexion établie avec le serveur [$name].\n";
// total messages in mailbox
$nbmsg = imap_num_msg($imapResource);
print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
// unread messages in current mailbox
if ($nbmsg > 0) {
print "Récupération de la liste des messages non lus de la boîte à lettres [$name]\n";
$msgNumbers = imap_search($imapResource, 'UNSEEN');
if ($msgNumbers === FALSE) {
print "Il n'y a pas de nouveaux messages dans la boîte à lettres [$name]\n";
} else {
foreach ($msgNumbers as $msgNumber) {
// we retrieve information on message n° $msgNumber
$infosMail = imap_headerinfo($imapResource, $msgNumber);
if ($infosMail === FALSE) {
print "Statut du message n° [$msgNumber] de la boîte à lettres [$name] non récupéré : " . imap_last_error() . "\n";
} else {
print "Statut du message n° [$msgNumber] de la boîte à lettres [$name]\n";
print_r($infosMail);
}
// we retrieve the body of message n° $msgNumber
getMailBody($imapResource, $msgNumber, $infos);
// if the protocol is POP3, we delete the message
$pop3 = $infos["pop3"];
if ($pop3 !== NULL) {
// delete the message in two steps
imap_delete($imapResource, $msgNumber);
imap_expunge($imapResource);
}
}
}
}
}
// closing the connection
$imapClose = imap_close($imapResource);
if (!$imapClose) {
// Failure
print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
} else {
// success
print "Fermeture de la connexion réussie.\n";
}
}
function getMailBody($imapResource, int $msgNumber, array $infos): void {
// we retrieve the body of message n° $msgNumber
$corpsMail = imap_body($imapResource, $msgNumber);
print "Enregistrement du message dans le fichier {$infos["output-dir"]}/{$infos["prefix"]}$msgNumber\n";
// create the folder if necessary
if (!file_exists($infos["output-dir"])) {
mkdir($infos["output-dir"]);
}
// record the message
if (!file_put_contents($infos["output-dir"] . "/" . $infos["prefix"] . $msgNumber, $corpsMail)) {
print "Echec de l'enregistrement\n";
}
}
تعليقات
- الأسطر 19–24: تقوم بالتكرار عبر جميع الخوادم الموجودة في ملف التكوين؛
- السطر 32: تقوم وظيفة [readmailbox] بقراءة صندوق البريد المحدد في [$name]؛
- السطر 32: يفتح اتصال IMAP؛
- المعلمة الأولى هي عنوان URL IMAP لصندوق البريد المراد قراءته؛
- المعلمة الثانية هي اسم المستخدم لمالك صندوق البريد؛
- المعلمة الثالثة هي كلمة المرور الخاصة بهم؛
تقوم الدالة [imap_open] بتأمين الاتصال إذا كان عنوان URL IMAP لصندوق البريد يتضمن المعلمة /ssl؛
- السطر 41: تعرض الدالة [imap_num_msg] العدد الإجمالي للرسائل الموجودة في صندوق البريد؛
- السطر 46: تسمح لك الدالة [imap_search] بالبحث عن رسائل محددة. هنا، نبحث عن الرسائل التي لم تتم قراءتها بعد (UNSEEN). المعلمة الثانية هي معيار الاختيار. هناك حوالي عشرين معيارًا. تُرجع الدالة [imap_search] مصفوفة من معرّفات الرسائل. يمكن أن تتخذ هذه المصفوفة شكلين: أرقام تسلسلية أو معرّفات فريدة للرسائل (UID). بشكل افتراضي، تُرجع الدالة [imap_search] مصفوفة من الأرقام التسلسلية. إذا أضفنا المعلمة الثالثة [SE_UID]، فسنحصل على معرّفات الرسائل الفريدة (UID)؛
- السطر 47: تُرجع الدالة [imap_search] القيمة المنطقية FALSE إذا لم تعثر على أي رسائل؛
- السطر 50: نقوم بالتكرار عبر جميع الرسائل غير المقروءة؛
- السطر 52: تحتوي الرسالة على رؤوس يمكن استردادها باستخدام دالة [imap_headerinfo]. عادةً ما تكون المعلمة الثانية لها هي رقم تسلسل الرسالة. إذا كنت تريد استخدام معرف الرسالة الفريد (UID)، فاضبط المعلمة الثالثة على [FT_UID]؛
- السطر 53: تُرجع الدالة [imap_headerinfo] القيمة المنطقية FALSE إذا لم تتمكن من إكمال مهمتها. وإلا، فإنها تُرجع كائنًا مركبًا نعرضه باستخدام الدالة [print_r]، السطر 57؛
- السطر 60: بعد استرداد الرؤوس، نقوم الآن باسترداد نص الرسالة باستخدام الدالة [imap_body]. ترجع هذه الدالة NULL إذا لم تتمكن من إكمال مهمتها؛
- الأسطر 84–87: نقوم بحفظ نص الرسالة في ملف محلي؛
- الأسطر 63–68: إذا كان البروتوكول المستخدم هو POP3، فإننا نحذف الرسالة التي تمت قراءتها للتو:
- تقوم الدالة [imap_delete] بوضع علامة "للحذف" على الرسالة ولكنها لا تحذفها؛
- تقوم الدالة [imap_expunge] بحذف جميع الرسائل التي تم وضع علامة "للحذف" عليها فعليًا؛
- السطر 74: نغلق الاتصال بخادم IMAP. للقيام بذلك، نستخدم الدالة [imap_close]؛
- السطر 86: تسترد الدالة [imap_body] نص الرسالة المحددة بواسطة معرفها؛
دعونا نقوم بتشغيل البرنامج النصي [smtp-02.json] بحيث يكون لدى مستخدم Gmail [php7parlexemple] ومستخدم [localhost] [guest] رسائل جديدة. بمجرد الانتهاء من ذلك، دعونا نقوم بتشغيل البرنامج النصي [imap-01.php] لقراءة صناديق بريدهما.
إخراج وحدة التحكم كما يلي:
------------Lecture de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Connexion établie avec le serveur [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX].
Il y a [27] messages dans la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Récupération de la liste des messages non lus de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Statut du message n° [26] de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
stdClass Object
(
[date] => Wed, 22 May 2019 10:08:24 +0000
[Date] => Wed, 22 May 2019 10:08:24 +0000
[subject] => test-gmail-via-gmail
[Subject] => test-gmail-via-gmail
[message_id] => <d8405cac62d57bd9c531ea79c146c72d@swift.generated>
[toaddress] => php7parlexemple@gmail.com
[to] => Array
(
[0] => stdClass Object
(
[mailbox] => php7parlexemple
[host] => gmail.com
)
)
[fromaddress] => php7parlexemple@gmail.com
[from] => Array
(
[0] => stdClass Object
(
[mailbox] => php7parlexemple
[host] => gmail.com
)
)
[reply_toaddress] => php7parlexemple@gmail.com
[reply_to] => Array
(
[0] => stdClass Object
(
[mailbox] => php7parlexemple
[host] => gmail.com
)
)
[senderaddress] => php7parlexemple@gmail.com
[sender] => Array
(
[0] => stdClass Object
(
[mailbox] => php7parlexemple
[host] => gmail.com
)
)
[Recent] =>
[Unseen] => U
[Flagged] =>
[Answered] =>
[Deleted] =>
[Draft] =>
[Msgno] => 26
[MailDate] => 22-May-2019 10:08:29 +0000
[Size] => 19086
[udate] => 1558519709
)
Enregistrement du message dans le fichier output/gmail-imap/message-26
Statut du message n° [27] de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
stdClass Object
(
…
)
Enregistrement du message dans le fichier output/gmail-imap/message-27
Fermeture de la connexion réussie.
------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [1] messages dans la boîte à lettres [{localhost:110/pop3}]
Récupération de la liste des messages non lus de la boîte à lettres [{localhost:110/pop3}]
Statut du message n° [1] de la boîte à lettres [{localhost:110/pop3}]
stdClass Object
(
…
)
Enregistrement du message dans le fichier output/localhost-pop3/message-1
Fermeture de la connexion réussie.
Done.
إذا أعدنا تشغيل البرنامج النصي [imap-01.php] فور ظهور هذه النتائج، فستكون النتائج كما يلي:
------------Lecture de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Connexion établie avec le serveur [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX].
Il y a [27] messages dans la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Récupération de la liste des messages non lus de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Il n'y a pas de nouveaux messages dans la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Fermeture de la connexion réussie.
------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [0] messages dans la boîte à lettres [{localhost:110/pop3}]
Fermeture de la connexion réussie.
- السطر 3: لا يزال عدد الرسائل في صندوق بريد Gmail كما هو، ولكن لم تعد هناك رسائل جديدة غير مقروءة (السطر 5). وهذا يدل على أن التنفيذ السابق قد غيّر حالة الرسائل المقروءة من "غير مقروءة" إلى "مقروءة"؛
- السطر 9: لم تعد هناك رسائل في صندوق بريد المستخدم [guest@localhost]. ويرجع ذلك إلى أنه في التشغيل السابق، تم حذف الرسائل التي تمت قراءتها على [localhost] لاحقًا؛
تم حفظ الرسائل محليًا:

إذا نظرنا، على سبيل المثال، إلى محتوى الرسالة رقم 26 في Gmail، نرى ما يلي:
--_=_swift_1558519704_f31b373d6e416dc88eb4db0e45fb3a95_=_
Content-Type: multipart/alternative;
boundary="_=_swift_1558519706_9bffb48891232e50ab645383ca62242d_=_"
--_=_swift_1558519706_9bffb48891232e50ab645383ca62242d_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
ligne 1
ligne 2
ligne 3
--_=_swift_1558519706_9bffb48891232e50ab645383ca62242d_=_
Content-Type: text/HTML; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<b>ligne 1<br/>ligne 2<br/>ligne 3</b>
--_=_swift_1558519706_9bffb48891232e50ab645383ca62242d_=_--
--_=_swift_1558519704_f31b373d6e416dc88eb4db0e45fb3a95_=_
Content-Type: application/pdf; name=Hello.pdf
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=Hello.pdf
JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl
Y29kZT4+CnN0cmVhbQp4nHWPuQoCQQyG+3mK1MKMyThHFoaAq7uF3cKAhdh5gIXgNr6+swcWshII
……………………………….…
OTQwODU4RDUzRDVENjU0QzJCNTM3Mjc+IF0KL0RvY0NoZWNrc3VtIC9DMjU3MUY1MUNDRjgwQ0Ex
ODU0OUI0RTQ4NDkwMDM3OAo+PgpzdGFydHhyZWYKMTIzMjYKJSVFT0YK
--_=_swift_1558519704_f31b373d6e416dc88eb4db0e45fb3a95_=_--
- الأسطر 11–13: الرسالة النصية العادية؛
- السطر 19: رسالة HTML؛
- السطر 25: المرفق؛
دعونا نحاول تحسين هذا البرنامج النصي بحيث يتم تخزين أنواع الرسائل المختلفة والمرفقات في ملفات منفصلة.
16.6.5. عميل POP3/IMAP المحسّن
في البرنامج النصي [imap-01.php]، نعرض نص الرسالة #i كملف نصي يحتوي على أنواع الرسائل المختلفة والمحتوى المشفر للمرفقات المتنوعة. من الممكن الحصول على بنية الرسالة لتحديد هذه الأجزاء المختلفة. في البرنامج النصي [imap-02.php]، نقوم بتعديل الدالة [getMailBody] على النحو التالي:
function getMailBody($imapResource, int $msgNumber, array $infos): void {
// we retrieve the message structure
$structure=imap_fetchstructure($imapResource, $msgNumber);
// we display it
print_r($structure);
}
- السطر 3: نطلب بنية الرسالة؛
- السطر 5: نعرضها؛
الهدف هو فهم المعلومات الواردة في بنية الرسالة لمعرفة كيف يمكننا استخراج أجزائها المختلفة. في مثالنا، يتم إرسال الرسالة بواسطة البرنامج النصي [smtp-02.php] باستخدام التكوين التالي [config-smtp-02.json]:
لذلك، يتم إرسال رسالة تحتوي على خمسة مرفقات إلى [guest@localhost] (الأسطر 11–15). يتم تنفيذ البرنامج النصي [imap-02.php] باستخدام التكوين التالي [config-imap-01.json]:
وبالتالي، يتم استخدام صندوق البريد الخاص بـ [guest@localhost] (السطر 5). ثم يعرض البرنامج النصي [imap-02.php] بنية الرسالة التي أرسلها [smtp-02.php]. وتظهر هذه البنية على وحدة التحكم على النحو التالي:
stdClass Object
(
[type] => 1
[encoding] => 0
[ifsubtype] => 1
[subtype] => MIXED
[ifdescription] => 0
[ifid] => 0
[bytes] => 253599
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => BOUNDARY
[value] => _=_swift_1558872295_5bc8ee2ca8b3723c0b39ca8bbfbebdeb_=_
)
)
[parts] => Array
(
[0] => stdClass Object
(
[type] => 1
[encoding] => 0
[ifsubtype] => 1
[subtype] => ALTERNATIVE
[ifdescription] => 0
[ifid] => 0
[bytes] => 429
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => BOUNDARY
[value] => _=_swift_1558872296_1e51aae79dfca4e7e0af112489fe8734_=_
)
)
[parts] => Array
(
[0] => stdClass Object
(
[type] => 0
[encoding] => 4
[ifsubtype] => 1
[subtype] => PLAIN
[ifdescription] => 0
[ifid] => 0
[lines] => 3
[bytes] => 27
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => CHARSET
[value] => utf-8
)
)
)
[1] => stdClass Object
(
[type] => 0
[encoding] => 4
[ifsubtype] => 1
[subtype] => HTML
[ifdescription] => 0
[ifid] => 0
[lines] => 1
[bytes] => 40
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => CHARSET
[value] => utf-8
)
)
)
)
)
[1] => stdClass Object
(
[type] => 3
[encoding] => 3
[ifsubtype] => 1
[subtype] => VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
[ifdescription] => 0
[ifid] => 0
[bytes] => 16302
[ifdisposition] => 1
[disposition] => ATTACHMENT
[ifdparameters] => 1
[dparameters] => Array
(
[0] => stdClass Object
(
[attribute] => FILENAME
[value] => Hello from SwiftMailer.docx
)
)
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => NAME
[value] => Hello from SwiftMailer.docx
)
)
)
[2] => stdClass Object
(
[type] => 3
[encoding] => 3
[ifsubtype] => 1
[subtype] => PDF
[ifdescription] => 0
[ifid] => 0
[bytes] => 17514
[ifdisposition] => 1
[disposition] => ATTACHMENT
[ifdparameters] => 1
[dparameters] => Array
(
[0] => stdClass Object
(
[attribute] => FILENAME
[value] => Hello from SwiftMailer.pdf
)
)
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => NAME
[value] => Hello from SwiftMailer.pdf
)
)
)
[3] => stdClass Object
(
…
)
[4] => stdClass Object
(
…
)
[5] => stdClass Object
(
[type] => 2
[encoding] => 3
[ifsubtype] => 1
[subtype] => RFC822
[ifdescription] => 0
[ifid] => 0
[lines] => 1881
[bytes] => 146682
[ifdisposition] => 1
[disposition] => ATTACHMENT
[ifdparameters] => 1
[dparameters] => Array
(
[0] => stdClass Object
(
[attribute] => FILENAME
[value] => test-localhost.eml
)
)
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => NAME
[value] => test-localhost.eml
)
)
[parts] => Array
(
…
)
)
)
)
تعليقات
- تشرح وثائق PHP الخاصة بوظيفة [imap_fetch_structure] معنى الحقول المختلفة في الكائن الذي ترجعها الوظيفة:

القيم الرقمية لحقل [type] لها المعاني التالية:

القيم الرقمية لحقل [encoding] لها المعاني التالية:

بدأت الرسالة التي سجلها [imap-01.php] بالنص التالي:
Return-Path: <php7parlexemple@gmail.com>
Received: from [127.0.0.1] (lfbn-1-11924-110.w90-93.abo.wanadoo.fr. [90.93.230.110])
by smtp.gmail.com with ESMTPSA id e14sm7773816wma.41.2019.05.26.03.11.53
for <php7parlexemple@gmail.com>
(version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
Sun, 26 May 2019 03:11:54 -0700 (PDT)
Message-ID: <e613c47a421a66e2cf7f8e319616ec49@swift.generated>
Date: Sun, 26 May 2019 10:11:53 +0000
Subject: test-gmail-via-gmail
From: php7parlexemple@gmail.com
To: php7parlexemple@gmail.com
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: multipart/alternative; boundary="_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_"
--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
ligne 1
ligne 2
ligne 3
--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_
Content-Type: text/HTML; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<b>ligne 1<br/>ligne 2<br/>ligne 3</b>
--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_--
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
- تحدد السطران 15) و33) رسالة [multipart/mixed] (السطر m)؛
- السطران 18) و 16) يحددان الجزء الأول من الرسالة: رسالة النص العادي؛
- السطران 26) و 32) يحددان الجزء الثاني من الرسالة: رسالة HTML؛
نجد المعلومات المختلفة من الرسالة أعلاه في الكائن الذي يتم إرجاعه بواسطة [imap_fetchstructure]:
stdClass Object
(
[type] => 1
[encoding] => 0
[ifsubtype] => 1
[subtype] => MIXED
[ifdescription] => 0
[ifid] => 0
[bytes] => 253599
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => BOUNDARY
[value] => _=_swift_1558872295_5bc8ee2ca8b3723c0b39ca8bbfbebdeb_=_
)
)
[parts] => Array
(
[0] => stdClass Object
(
[type] => 1
[encoding] => 0
[ifsubtype] => 1
[subtype] => ALTERNATIVE
[ifdescription] => 0
[ifid] => 0
[bytes] => 429
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => BOUNDARY
[value] => _=_swift_1558872296_1e51aae79dfca4e7e0af112489fe8734_=_
)
)
[parts] => Array
(
[0] => stdClass Object
(
[type] => 0
[encoding] => 4
[ifsubtype] => 1
[subtype] => PLAIN
[ifdescription] => 0
[ifid] => 0
[lines] => 3
[bytes] => 27
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => CHARSET
[value] => utf-8
)
)
)
[1] => stdClass Object
(
[type] => 0
[encoding] => 4
[ifsubtype] => 1
[subtype] => HTML
[ifdescription] => 0
[ifid] => 0
[lines] => 1
[bytes] => 40
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => CHARSET
[value] => utf-8
)
)
)
)
)
- السطر 3: الرسالة من نوع MIME (ملحقات البريد الإلكتروني متعددة الأغراض) [متعددة الأجزاء]؛
- السطر 4: الرسالة مشفرة بـ 7 بتات؛
- السطر 5: [ifsubtype]=1 يشير إلى وجود حقل [subtype] في البنية؛
- السطر 6: حقل [subtype] يحدد نوع فرعي MIME، وهو في هذه الحالة النوع [mixed]. بشكل عام، نوع MIME للوثيقة هو [multipart/mixed]؛
- السطر 7: [ifdescription]=0 يشير إلى عدم وجود حقل [description] في البنية؛
- السطر 8: [ifid]=0 يشير إلى عدم وجود حقل [id] في البنية؛
- السطر 10: [ifdisposition]=0 يشير إلى عدم وجود حقل [disposition] في البنية؛
- السطر 11: [ifdparameters]=0 يشير إلى عدم وجود حقل [dparameters] في البنية؛
- السطر 12: [ifparameters]=1 يشير إلى وجود حقل [parameters] في البنية؛
- السطر 13: يصف حقل [parameters] معلمات الرسالة. هنا، يوجد حقل واحد فقط؛
- الأسطر 15-19: يصف هذا الكائن السطر التالي من الرسالة النصية:
تُستخدم هذه الأسطر لتحديد حدود الرسالة. في الرسالة التي تم استردادها بواسطة [imap-01.php]، يتوافق الجزء الموصوف للتو من الرسالة مع السطر m). السمة [boundary] ليست هي نفسها لأن لقطات الشاشة تتوافق مع نفس الرسالة ولكن تم إرسالها في أوقات مختلفة؛
- السطر 23: تبدأ هنا بنية الأجزاء المختلفة للرسالة؛
- الأسطر 25–45: هذا الجزء الأول من النوع [multipart/alternative]. وهو يتوافق مع السطر p) من نص الرسالة؛
- السطر 47: يحتوي هذا الجزء الأول نفسه على أجزاء فرعية؛
- الأسطر 47-70: هذا الجزء الفرعي الأول من النوع [text/plain] (السطران 51 و54)، ومشفّر كـ [QUOTED-PRINTABLE] (السطر 52)، وله معلمة [charset=utf-8] (السطران 66-67)؛
- الأسطر 49–72 تصف الأسطر s–x من الرسالة النصية؛
- الأسطر 74–99: تصف الجزء الفرعي الثاني من الجزء [multipart/alternative]؛
- الأسطر 74–99: هذا الجزء الفرعي الثاني من النوع [text/HTML] (الأسطر 76، 79)، ومشفّر بـ [ENCQUOTEDPRINTABLE] (السطر 77)، وله معلمة [charset=utf-8] (الأسطر 89–93)؛
- الأسطر 74-99 تصف الأسطر aa-ad من الرسالة النصية؛
أصبح قسم [multipart/alternative] الآن كاملاً. يبدأ قسم [application/vnd.openxmlformats-officedocument.wordprocessingml.document]، الموصوف بالنص التالي:
مرة أخرى، توجد هذه المعلومات في الكائن الذي ترجعها الدالة [imap_fetchstructure]:
[1] => stdClass Object
(
[type] => 3
[encoding] => 3
[ifsubtype] => 1
[subtype] => VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
[ifdescription] => 0
[ifid] => 0
[bytes] => 16302
[ifdisposition] => 1
[disposition] => ATTACHMENT
[ifdparameters] => 1
[dparameters] => Array
(
[0] => stdClass Object
(
[attribute] => FILENAME
[value] => Hello from SwiftMailer.docx
)
)
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => NAME
[value] => Hello from SwiftMailer.docx
)
)
)
- السطر 1: هذا هو الجزء الثاني من الرسالة الكلية. تذكر أن الجزء الأول كان من النوع [multipart/alternative]؛
- الأسطر 3-6: هذا الجزء الثاني من النوع [application/vnd.openxmlformats-officedocument.wordprocessingml.document] (السطران 3 و6) ومشفّر بـ Base64 (السطر 4)؛
- السطر 11: هذا الجزء الثاني هو مرفق (السطر 11) ويحتوي على معلمتين: [filename=Hello from SwiftMailer.docx] (الأسطر 15–21) و[name=Hello from SwiftMailer.docx] (الأسطر 26–32). لاحظ أن هذه المعلمة الأخيرة غير موجودة في الرسالة النصية. لذلك تمت إضافتها في الدالة [imap_fetchstructure]؛
يتم تكرار الأسطر 1–36 لكل مرفق من المرفقات الخمسة للرسالة.
وبالتالي، تتيح لنا دالة [imap_fetch_structure] الحصول على بنية الرسالة. تحدد هذه البنية الأجزاء، التي قد تحتوي بدورها على أجزاء فرعية. لاسترداد نص جزء أو جزء فرعي، نستخدم دالة [imap_fetchbody].
نقوم بتعديل الدالة [getMailBody]، التي تسمح لنا باسترداد نص الرسالة، على النحو التالي:
function getMailBody($imapResource, int $msgNumber, array $infos, object $infosMail): void {
// on récupère la structure du message
$structure = imap_fetchstructure($imapResource, $msgNumber);
if ($structure !== FALSE) {
// on récupère ces différentes parties
getParts($imapResource, $msgNumber, $infos, $infosMail, $structure);
}
}
function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
// calcul du n° de section
if (substr($sectionNumber, 0, 2) === "0.") {
$sectionNumber = substr($sectionNumber, 2);
}
print "-----contenu de la partie n° [$sectionNumber]\n";
// type de contenu
print "Content-Type: ";
switch ($part->type) {
case TYPETEXT:
print "TEXT/{$part->subtype}\n";
break;
case TYPEMULTIPART:
print "MULTIPART/{$part->subtype}\n";
break;
case TYPEAPPLICATION:
print "APPLICATION/{$part->subtype}\n";
break;
case TYPEMESSAGE:
print "MESSAGE/{$part->subtype}\n";
break;
default:
print "UNKNOWN/{$part->subtype}\n";
break;
}
// type de codage
$encodings=["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
print "Transfer-Encoding : ".$encodings[$part->encoding]."\n";
// on passe aux sous-parties éventuelles
if (isset($part->parts)) {
for ($i = 1; $i <= count($part->parts); $i++) {
// une nouvelle partie du message
$subpart = $part->parts[$i - 1];
// appel récursif - on demande le corps de la partie [$subpart]
getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
}
}
}
تعليقات
- السطر 3: نسترد بنية الرسالة؛
- السطر 6: نطلب عرض أجزائها المختلفة، الموجودة في المصفوفة [parts] للبنية؛
- السطر 10: تستقبل الدالة [getParts] المعلمات التالية:
- [$imapResource]: الاتصال بخادم IMAP؛
- [$msgNumber]: الرقم التسلسلي للرسالة التي نريد أجزاءها؛
- [$infos]: معلومات حول مكان تخزين الأجزاء التي سنجدها في نظام الملفات المحلي؛
- [$infosMail]: معلومات عامة عن البريد الإلكتروني (المرسل، المستلم (المستلمون)، الموضوع، إلخ)؛
- [$part]: كائن يمثل جزءًا من الرسالة؛
- [$sectionNumber]: رقم قسم (أو جزء) الرسالة؛
- الأسطر 17–34: تعرض نوع محتوى القسم [$section] من الرسالة. للقيام بذلك، نستخدم الحقول [$part→type] و [$part→subtype] من القسم [$part]؛
- السطور 36-37: يتم عرض نوع الترميز للجزء [$sectionNumber]؛
- الأسطر 40-47: ربما يحتوي الجزء الذي تم عرض معلومات عنه للتو على أجزاء فرعية خاصة به؛
- الأسطر 41-46: إذا كان الأمر كذلك، فإننا نطلب نوع محتوى الأجزاء الفرعية المختلفة للجزء الذي عرضناه للتو. هنا، نقوم باستدعاء متكرر لوظيفة [getParts]؛
مرة أخرى، نرسل رسالة بريد إلكتروني إلى مستخدم Gmail [php7parlexemple@gmail.com] باستخدام البرنامج النصي [smtp-02.php] ونقرأها باستخدام البرنامج النصي السابق [imap-02.php]. وينتج عن ذلك الإخراج التالي في وحدة التحكم:
------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [1] messages dans la boîte à lettres [{localhost:110/pop3}]
Récupération de la liste des messages non lus de la boîte à lettres [{localhost:110/pop3}]
-----contenu de la partie n° [0]
Content-Type: MULTIPART/MIXED
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1]
Content-Type: MULTIPART/ALTERNATIVE
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : quoted-printable
-----contenu de la partie n° [1.2]
Content-Type: TEXT/HTML
Transfer-Encoding : quoted-printable
-----contenu de la partie n° [2]
Content-Type: APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
Transfer-Encoding : base 64
-----contenu de la partie n° [3]
Content-Type: APPLICATION/PDF
Transfer-Encoding : base 64
-----contenu de la partie n° [4]
Content-Type: APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT
Transfer-Encoding : base 64
-----contenu de la partie n° [5]
Content-Type: UNKNOWN/PNG
Transfer-Encoding : base 64
-----contenu de la partie n° [6]
Content-Type: MESSAGE/RFC822
Transfer-Encoding : base 64
-----contenu de la partie n° [6.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : 7 bits
Fermeture de la connexion réussie.
يمكننا استرداد أنواع مختلفة من محتوى الرسائل بالإضافة إلى أنواع الترميز الخاصة بها. يتبع ترقيم الأجزاء القاعدة التالية:
- السطران 6-7: الجزء [multipart/mixed]، الذي يمثل الرسالة بأكملها، يُرقم بـ 0. ثم تُرقم الأجزاء المختلفة لهذا الكائن بـ 1، 2…
تتكون الرسالة من خمسة أجزاء إجمالاً:
- السطران 9-10: الجزء [multipart/alternative]، المرقّم برقم 1؛
- السطران 17-18: الجزء [APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT]، الذي يحمل الرقم 2. هذا مرفق ملف Word؛
- السطران 20-21: قسم [APPLICATION/PDF]، المرقّم برقم 3. هذا مرفق ملف PDF؛
- السطران 23-24: قسم [APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT]، المرقّم برقم 4. هذا مرفق ملف OpenOffice؛
- السطران 26-27: قسم [UNKNOWN/PNG] المسمى رقم 5. هذا مرفق ملف صورة؛
- السطران 30-31: القسم [MESSAGE/RFC822] المرقّم برقم 6. هذا مرفق بريد إلكتروني؛
عندما يحتوي جزء ما على أجزاء فرعية، يتم ترقيمها x.1، x.2... حيث x هو رقم الجزء الذي يضمها. وبالتالي:
- السطران 11-12: الجزء الأول من قسم [multipart/alternative] مرقّم بـ 1.1. وهو محتوى [text/plain]: رسالة البريد الإلكتروني؛
- السطران 14–15: الجزء الثاني من الجزء [multipart/alternative] مرقّم بـ 1.2. وهو من النوع [text/HTML]: رسالة البريد الإلكتروني بتنسيق HTML؛
- السطران 32-33: الجزء الأول من المرفق [MESSAGE/RFC822] يحمل الرقم 6.1. وهو من النوع [text/plain]. في الواقع، وفقًا لمعيار MIME، يختلف ترقيم أجزاء مرفق البريد الإلكتروني [MESSAGE/RFC822] عن القاعدة الموضحة أعلاه. وبالتالي، فإن الجزء الأول من مرفق [MESSAGE/RFC822] لا يحمل الرقم 6.1 بل رقمًا مختلفًا؛
الآن بعد أن عرفنا كيفية تحديد الأجزاء والأجزاء الفرعية المختلفة للبريد الإلكتروني، نحتاج إلى استرداد محتواها.
يتطور كود البرنامج النصي على النحو التالي:
function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
// calcul du n° de section
if (substr($sectionNumber, 0, 2) === "0.") {
$sectionNumber = substr($sectionNumber, 2);
}
print "-----contenu de la partie n° [$sectionNumber]\n";
// type de contenu
print "Content-Type: ";
switch ($part->type) {
case TYPETEXT:
print "TEXT/{$part->subtype}\n";
break;
case TYPEMULTIPART:
print "MULTIPART/{$part->subtype}\n";
break;
case TYPEAPPLICATION:
print "APPLICATION/{$part->subtype}\n";
break;
case TYPEMESSAGE:
print "MESSAGE/{$part->subtype}\n";
break;
default:
print "UNKNOWN/{$part->subtype}\n";
break;
}
// type de codage
$encodings = ["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
print "Transfer-Encoding : " . $encodings[$part->encoding] . "\n";
// est-ce un message ?
if ($part->type === TYPEMESSAGE) {
// on ne va pas gérer les sous-parties de ce message (mail attaché)
// on affiche le corps du mail attaché
print imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
} else {
// on passe aux sous-parties éventuelles
if (isset($part->parts)) {
for ($i = 1; $i <= count($part->parts); $i++) {
// une nouvelle partie du message
$subpart = $part->parts[$i - 1];
// appel récursif - on demande le corps de la partie [$subpart]
getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
}
} else {
// il n'y a pas de sous-parties - on affiche alors le corps du message
print imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
}
}
}
تعليقات
- السطر 46: تسترد الدالة [imap_fetchbody] نص الجزء رقم #[$sectionNumber] من الرسالة. يتبع ترقيم أجزاء الرسالة القاعدة الموضحة سابقًا؛
- السطر 1: نبدأ بالقسم «0»؛
- السطر 41: سيتم ترقيم الأقسام الفرعية لهذا القسم بـ "0.1" و "0.2"، في حين أنه ينبغي ترقيمها بـ "1" و "2"...
- الأسطر 3–5: نقوم بتصحيح هذا الخطأ؛
- الأسطر 37–43: إذا كان القسم الحالي يحتوي على أقسام فرعية، فإننا نمر على كل منها (الأسطر 38–43). رقم القسم الخاص بها هو [$sectionNumber.$i]؛
- الأسطر 44-47: عندما لا يكون هناك المزيد من الأقسام الفرعية، نعرض نص القسم الحالي باستخدام دالة [imap_fetchbody]. في مثالنا، هذه هي الأقسام [text/plain] و[text/HTML] والمرفقات؛
يؤدي تشغيل هذا البرنامج النصي إلى النتائج التالية:
------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [1] messages dans la boîte à lettres [{localhost:110/pop3}]
Récupération de la liste des messages non lus de la boîte à lettres [{localhost:110/pop3}]
-----contenu de la partie n° [0]
Content-Type: MULTIPART/MIXED
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1]
Content-Type: MULTIPART/ALTERNATIVE
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : quoted-printable
ligne 1
ligne 2
ligne 3
-----contenu de la partie n° [1.2]
Content-Type: TEXT/HTML
Transfer-Encoding : quoted-printable
<b>ligne 1<br/>ligne 2<br/>ligne 3</b>
-----contenu de la partie n° [2]
Content-Type: APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
Transfer-Encoding : base 64
UEsDBBQABgAIAAAAIQDfpNJsWgEAACAFAAATAAgCW0NvbnRlbnRfVHlwZXNdLnhtbCCiBAIooAAC
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
…
AAAAAAAAAF0mAABkb2NQcm9wcy9jb3JlLnhtbFBLAQItABQABgAIAAAAIQCdxkmwcgEAAMcCAAAQ
AAAAAAAAAAAAAAAAAAgpAABkb2NQcm9wcy9hcHAueG1sUEsFBgAAAAALAAsAwQIAALArAAAAAA==
-----contenu de la partie n° [3]
Content-Type: APPLICATION/PDF
Transfer-Encoding : base 64
JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl
Y29kZT4+CnN0cmVhbQp4nHWNvQoCMRCE+zzF1sLF2WSTSyAEPD0Lu4OAhdj5AxaC1/j6Rk4s5GSa
…
PDcxQUJGQ0JGQURGODYxM0NBNUJDODNFMDNDNjI1QkQwPgo8NzFBQkZDQkZBREY4NjEzQ0E1QkM4
M0UwM0M2MjVCRDA+IF0KL0RvY0NoZWNrc3VtIC9DMTRCN0Q5N0YwNUU1OTYxQzhDODg0NEI3NkNF
OEIwRQo+PgpzdGFydHhyZWYKMTIzMTQKJSVFT0YK
-----contenu de la partie n° [4]
Content-Type: APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT
Transfer-Encoding : base 64
UEsDBBQAAAgAAAs9uU5exjIMJwAAACcAAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlvbi92bmQub2Fz
aXMub3BlbmRvY3VtZW50LnRleHRQSwMEFAAACAAACz25TgAAAAAAAAAAAAAAABwAAABDb25maWd1
…
AQIUABQACAgIAAs9uU42l0SORAQAABIRAAALAAAAAAAAAAAAAAAAAI8bAABjb250ZW50LnhtbFBL
AQIUABQACAgIAAs9uU4Uf52+LgEAACUEAAAVAAAAAAAAAAAAAAAAAAwgAABNRVRBLUlORi9tYW5p
ZmVzdC54bWxQSwUGAAAAABEAEQBlBAAAfSEAAAAA
-----contenu de la partie n° [5]
Content-Type: UNKNOWN/PNG
Transfer-Encoding : base 64
iVBORw0KGgoAAAANSUhEUgAABiAAAAEMCAYAAABN1n5OAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAg
AElEQVR4nOy9e5TdV3Xn+Zm7aqprlBq1Rq1Wq7XU6opGrXaMMI6jAcfj9ihu4hAehkAghBASICF0
…
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAA2Mb8f9Q5r2ohJn6/AAAAAElFTkSuQmCC
-----contenu de la partie n° [6]
Content-Type: MESSAGE/RFC822
Transfer-Encoding : base 64
UmV0dXJuLVBhdGg6IGd1ZXN0QGxvY2FsaG9zdA0KUmVjZWl2ZWQ6IGZyb20gWzEyNy4wLjAuMV0g
KGxvY2FsaG9zdCBbMTI3LjAuMC4xXSkNCglieSBERVNLVE9QLTUyOEk1Q1Ugd2l0aCBFU01UUA0K
…
cjJvaEpuNi9BQUFBQUVsRlRrU3VRbUNDDQotLV89X3N3aWZ0XzE1NTg3NzA1MDJfYzRiODA4Yzk5
YzI3ZGVkMDQ1OTViZDExZjRiYWQxMWJfPV8tLQ0K
Fermeture de la connexion réussie.
تعليقات
- الأسطر 14–16: محتوى الرسالة النصية المشفرة بتنسيق [quoted-printable] (السطر 13)؛
- السطر 20: محتوى رسالة HTML المشفرة بتنسيق [quoted-printable] (السطر 19)؛
- الأسطر 24-28: محتوى ملف Word المشفر بـ [base64] (السطر 23)؛
- الأسطر 32-37: محتوى ملف PDF المشفر بـ [base64] (السطر 31)؛
- الأسطر 41-45: محتوى ملف OpenOffice المشفر بـ [base64] (السطر 40)؛
- الأسطر 50-55: محتويات ملف الصورة المشفرة بتنسيق [base64] (السطر 49)؛
- الأسطر 59-63: محتوى البريد الإلكتروني المرفق المشفر بـ [base64] (السطر 58)؛
الآن بعد أن:
- نحن نعرف كيفية استرداد النص من الأجزاء المختلفة للبريد الإلكتروني؛
- نعرف ترميز هذه النصوص؛
يمكننا حفظ هذه النصوص في ملفات.
يتطور الكود على النحو التالي:
function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
// calcul du n° de section
if (substr($sectionNumber, 0, 2) === "0.") {
$sectionNumber = substr($sectionNumber, 2);
}
print "-----contenu de la partie n° [$sectionNumber]\n";
// type de contenu
print "Content-Type: ";
switch ($part->type) {
case TYPETEXT:
print "TEXT/{$part->subtype}\n";
break;
case TYPEMULTIPART:
print "MULTIPART/{$part->subtype}\n";
break;
case TYPEAPPLICATION:
print "APPLICATION/{$part->subtype}\n";
break;
case TYPEMESSAGE:
print "MESSAGE/{$part->subtype}\n";
break;
default:
print "UNKNOWN/{$part->subtype}\n";
break;
}
// type de codage
$encodings = ["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
print "Transfer-Encoding : " . $encodings[$part->encoding] . "\n";
// est-ce un message ?
if ($part->type === TYPEMESSAGE) {
// on ne va pas gérer les sous-parties de ce message
savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
} else {
// on passe aux sous-parties éventuelles
if (isset($part->parts)) {
for ($i = 1; $i <= count($part->parts); $i++) {
// une nouvelle partie du message
$subpart = $part->parts[$i - 1];
// appel récursif - on demande le corps de la partie [$subpart]
getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
}
} else {
// il n'y a pas de sous-parties - on sauvegarde alors le corps du message
savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
}
}
}
- السطران 33 و 45: تم استبدال عرض نص جزء [$imapResource, $msgNumber, $sectionNumber] من البريد الإلكتروني بحفظه في ملف؛
وظيفة [savePart] هي كما يلي:
// sauvegarde d'une partie de message
function savePart($imapResource, int $msgNumber, string $sectionNumber, array $infos, object $infosMail): void {
// dossier de sauvegarde
$outputDir = $infos["output-dir"] . "/message-$msgNumber";
// si le dossier n'existe pas, on le crée
if (!file_exists($outputDir)) {
mkdir($outputDir);
}
// structure de la partie à sauvegarder
$struct = imap_bodystruct($imapResource, $msgNumber, $sectionNumber);
// type de document
$type = $struct->type;
// sous-type de document
$subtype = "";
if (isset($struct->subtype)) {
$subtype = strtolower($struct->subtype);
}
// on analyse le type de la partie
switch ($type) {
case TYPETEXT:
// cas du message texte : text/xxx
switch ($subtype) {
case plain:
saveText("$outputDir/message.txt", 0, imap_fetchBody($imapResource, $msgNumber, $sectionNumber), $infosMail, $struct);
break;
case HTML:
saveText("$outputDir/message.HTML", 1, imap_fetchBody($imapResource, $msgNumber, $sectionNumber), $infosMail, $struct);
break;
}
break;
default:
// autres cas - on ne s'intéresse qu'aux attachements
if (isset($struct->disposition)) {
$disposition = strtolower($struct->disposition);
if ($disposition === "attachment") {
// on a affaire à un attachement - on le sauvegarde
saveAttachment($imapResource, $msgNumber, $sectionNumber, $outputDir, $struct);
}
} else {
// on ne traitera pas cette partie
print "Partie [$sectionNumber] ignorée\n";
}
break;
}
}
- الأسطر 3-8: إنشاء مجلد النسخ الاحتياطي. يتم تسمية هذا المجلد برقم الرسالة التي يتم تحليل أقسامها؛
- السطر 10: يتم تحديد جزء الرسالة المراد حفظه بشكل فريد بواسطة المعلمات الثلاثة [$imapResource، $msgNumber، $sectionNumber]. نسترد بنية هذا الجزء باستخدام الدالة [imap_bodystruct]؛
- السطر 12: استرداد النوع الرئيسي لقسم الرسالة؛
- الأسطر 13–17: يتم استرداد النوع الفرعي الخاص به؛
- الأسطر 20-30: نقوم بمعالجة نوعي المحتوى: [text/plain] (الأسطر 23-25) و [text/HTML] (الأسطر 26-28). يتم تجاهل أنواع [text/xx] الأخرى؛
- السطر 24: سيتم حفظ النص من الجزء [text/plain] في ملف باسم [message.txt]؛
- السطر 27: سيتم حفظ النص من قسم [text/HTML] في ملف [message.HTML]؛
- الأسطر 31–43: نتعامل مع الحالات التي لا يكون فيها النوع الرئيسي هو [text]؛
- السطر 35: يتم النظر فقط في مرفقات الرسالة؛
- السطر 37: يتم حفظ هذه المرفقات في ملف باستخدام وظيفة [saveAttachment]؛
لتلخيص الكود السابق:
- يحفظ الأجزاء [text/plain] و[text/HTML] باستخدام الدالة [saveText]. وتمثل هذه الأجزاء محتوى البريد الإلكتروني؛
- يحفظ المرفقات المختلفة باستخدام وظيفة [saveAttachment]؛
تعمل وظيفة [saveText] على النحو التالي:
// sauvegarde du texte [$text] du message
function saveText(string $fileName, int $type, string $text, object $infosMail, object $struct) {
// préparation du texte à sauvegarder
// $text est encodé - on le décode
switch ($struct->encoding) {
case ENCBASE64:
$text = base64_decode($text);
break;
case ENCQUOTEDPRINTABLE:
$text = quoted_printable_decode($text);
break;
}
// entêtes du message
// from
$from = "From: ";
foreach ($infosMail->from as $expéditeur) {
$from .= $expéditeur->mailbox . "@" . $expéditeur->host . ";";
}
// to
$to = "To: ";
foreach ($infosMail->to as $destinataire) {
$to .= $destinataire->mailbox . "@" . $destinataire->host . ";";
}
// subject
$subject = "Subject: " . $infosMail->subject;
// création du texte à enregistrer
switch ($type) {
case 0:
// text/plain
$contents = "$from\n$to\n$subject\n\n$text";
break;
case 1:
// text/HTML
$contents = "$from<br/>\n$to<br/>\n$subject<br/>\n<br/>\n$text";
break;
}
// création du fichier
print "sauvegarde d'un message dans [$fileName]\n";
// création du fichier
if (! file_put_contents($fileName, $contents)) {
// échec de la création du fichier
print "Impossible de créer le fichier [$fileName]\n";
}
}
تعليقات
- السطر 1:
- [$fileName] هو اسم الملف الذي سيتم حفظ النص [$text] فيه؛
- [$type]: يساوي 0 لملف نصي، و1 لملف HTML؛
- [$text]: هو النص المراد حفظه. ولكن يجب أولاً فك تشفيره لأنه مشفر؛
- [$infosMail]: يحتوي على معلومات عامة عن البريد الإلكتروني. سنستخدم الحقول [from, to, subject]؛
- [$struct]: هي البنية التي تصف الجزء من البريد الإلكتروني الذي نقوم بحفظه. سيسمح لنا ذلك بتحديد نوع تشفير النص المراد حفظه؛
- الأسطر 4–12: نقوم بفك تشفير النص المراد حفظه؛
- الأسطر 13–25: نسترد معلومات [from, to, subject] من البريد الإلكتروني؛
- الأسطر 27–36: اعتمادًا على نوع (0 أو 1) النص المراد حفظه، نقوم بإنشاء نص عادي (السطر 30) أو نص HTML (السطر 34)؛
- السطر 40: يتم حفظ النص بالكامل في الملف [$fileName]؛
يتم حفظ المرفقات باستخدام الدالة [saveAttachment] التالية:
// sauvegarde d'un attachement
function saveAttachment($imapResource, int $msgNumber, string $sectionNumber, string $outputDir, object $struct) {
// on analyse la structure de l'attachement
// on cherche à récupérer le nom du fichier dans lequel sauvegarder l'attachement
// ce nom se trouve dans les [dparameters] de la structure
if (isset($struct->dparameters)) {
// on récupère les [dparameters]
$dparameters = $struct->dparameters;
$fileName = "";
// on parcourt le tableau des [dparameters]
foreach ($dparameters as $dparameter) {
// chaque [dparameter] est un objet avec deux attributs [attribute, value]
$attribute = strtolower($dparameter->attribute);
// l'attribut [filename] correspond au nom du fichier à créer
// dans ce cas le nom du fichier est dans [$dparameter->value]
if ($attribute === "filename") {
$fileName = $dparameter->value;
break;
}
}
// si on n'a pas trouvé de nom de fichier, on regarde dans l'attribut [parameters] de la structure
if ($fileName === "" && isset($struct->parameters)) {
// on récupère les [parameters]
$parameters = $struct->parameters;
foreach ($parameters as $parameter) {
// chaque paramètre est un dictionnaire à deux clés [attribute, value]
$attribute = strtolower($parameter->attribute);
// si l'attribut est [name], alors le [value] est le nom du fichier
if ($attribute === "name") {
$fileName = $parameter->value;
// le nom du fichier peut être encodé
// par exemple =?utf-8?Q?Cours-Tutoriels-Serge-Tah=C3=A9-1568x268=2Ep
// on récupère l'encodage avec une expression régulière
$champs = [];
$match = preg_match("/=\?(.+?)\?/", $fileName, $champs);
// si concordance, alors on décode le nom du fichier
if ($match) {
$fileName = iconv_mime_decode($fileName, 0, $champs[1]);
}
break;
}
}
}
}
// si on a trouvé un nom de fichier, alors on sauvegarde l'attachement
if ($fileName !== "") {
// sauvegarde de l'attachement
$fileName = "$outputDir/$fileName";
print "sauvegarde de l'attachement dans [$fileName]\n";
// création fichier
if ($file = fopen($fileName, "w")) {
// on récupère le texte encodé de l'attachement
$text = imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
// l'attachement est encodé - on le décode
switch ($struct->encoding) {
// base 64
case ENCBASE64:
$text = base64_decode($text);
break;
// quoted printable
case ENCQUOTEDPRINTABLE:
$text = quoted_printable_decode($text);
break;
default:
// on ignore les autres cas
break;
}
// écriture du texte dans le fichier
fputs($file, $text);
// fermeture fichier
fclose($file);
} else {
// échec de la création du fichier
print "L'attachement n'a pu être sauvegardé dans [$fileName]\n";
}
}
}
تعليقات
- السطر 2: تقبل الدالة [saveAttachment] المعلمات التالية:
- [$imapResource, int $msgNumber, string $sectionNumber] تحدد بشكل فريد قسم IMAP المراد حفظه؛
- [string $outputDir] هو دليل الحفظ؛
- [object $struct] تصف بنية جزء الرسالة المراد حفظه؛
- الأسطر 6–44: نبحث عن اسم الملف المرتبط بالمرفق. سنستخدم نفس اسم الملف هذا لحفظه. يوجد اسم ملف المرفق في المصفوفة [$struct→dparameters] أو المصفوفة [$struct→parameters]، أو كليهما؛
- الأسطر 30–40: إذا كان اسم الملف يحتوي على أحرف غير مشفرة بـ 7 بتات، فهذا يعني أنه تم تشفيره بـ [quoted-printable]. في هذه الحالة، في [$struct→dparameters]، يُسمى السمة [fileName*] بدلاً من [fileName]. وهذا يعني أنه لم يستوف الشرط في السطر 16. ثم يتم البحث عن اسم الملف في المصفوفة [$struct→parameters]؛
- السطر 32: مثال على اسم ملف مشفر. له الشكل التالي: =?original_encoding?current_encoding?encoded_name. وبالتالي، فإن الاسم [=?utf-8?Q?Cours-Tutoriels-Serge-Tah=C3=A9-1568x268=2Ep] يعني أن اسم الملف كان بتنسيق UTF-8 وهو حاليًا بتنسيق [quoted-printable] (Q)؛
- السطر 38: يتم فك تشفير اسم الملف باستخدام الدالة [iconv_mime_decode]، التي تأخذ ثلاثة معلمات هنا:
- السلسلة المراد فك تشفيرها؛
- يتم تعيينها على 0 افتراضيًا؛
- مجموعة الأحرف التي سيتم استخدامها لتمثيل السلسلة التي تم فك تشفيرها. توجد هذه المعلمة في السلسلة المراد فك تشفيرها. ويتم الحصول عليها باستخدام تعبير عادي في السطور 34–35؛
- الأسطر 45-75: يتم حفظ المرفق في ملف بالاسم الذي تم العثور عليه؛
لاختبار البرنامج النصي [imap-02.php]، أرسل أولاً بريدًا إلكترونيًا إلى [guest@localhost] باستخدام التكوين التالي:
وبالتالي، هناك خمسة مرفقات.
نقرأ البريد الإلكتروني المرسل باستخدام [imap-02.php] والتكوين التالي:
إخراج وحدة التحكم كما يلي:
------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [1] messages dans la boîte à lettres [{localhost:110/pop3}]
Récupération de la liste des messages non lus de la boîte à lettres [{localhost:110/pop3}]
-----contenu de la partie n° [0]
Content-Type: MULTIPART/MIXED
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1]
Content-Type: MULTIPART/ALTERNATIVE
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : quoted-printable
sauvegarde d'un message dans [output/localhost-pop3/message-1/message.txt]
-----contenu de la partie n° [1.2]
Content-Type: TEXT/HTML
Transfer-Encoding : quoted-printable
sauvegarde d'un message dans [output/localhost-pop3/message-1/message.HTML]
-----contenu de la partie n° [2]
Content-Type: APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/Hello from SwiftMailer.docx]
-----contenu de la partie n° [3]
Content-Type: APPLICATION/PDF
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/Hello from SwiftMailer.pdf]
-----contenu de la partie n° [4]
Content-Type: APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/Hello from SwiftMailer.odt]
-----contenu de la partie n° [5]
Content-Type: UNKNOWN/PNG
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/Cours-Tutoriels-Serge-Tahé-1568x268.png]
-----contenu de la partie n° [6]
Content-Type: MESSAGE/RFC822
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/test-localhost.eml]
Fermeture de la connexion réussie.
Done.
يمكن العثور على الملفات المحفوظة في المجلد [output/localhost-pop3/message-N]:

16.6.6. عميل POP3/IMAP باستخدام مكتبة [php-mime-mail-parser]
في البرنامج النصي السابق [imap-02.php]، تمكنا من حفظ:
- محتوى البريد الإلكتروني [text/plain] و [text/HTML]؛
- مرفقات البريد الإلكتروني؛
بالنسبة للمرفقات من النوع [message/rfc822]، قمنا أيضًا بحفظ محتوى المرفق. ومع ذلك، فإن هذا النوع من المرفقات هو في حد ذاته رسالة بريد إلكتروني، والتي بدورها تحتوي على محتوى [text/plain] و [text/HTML] بالإضافة إلى المرفقات. وقد نجد أنفسنا في الحالة التالية:
- [بريد إلكتروني 1] له بنية مشابهة لبنية مرفق [message/rfc822]؛
- [بريد إلكتروني 2] مرفق بالبريد الإلكتروني 1؛
- [رسالة بريد إلكتروني 3] مرفقة برسالة البريد الإلكتروني 2؛
- إلخ...
يقوم البرنامج النصي [imap-02.php] بحفظ محتويات [البريد الإلكتروني 1] (النص والمرفقات). كما يحفظ [البريد الإلكتروني 2] كوثيقة مرفقة ولكنه يتوقف عند هذا الحد. ولا يحاول تحليل [البريد الإلكتروني 2] لاستخراج النص والمرفقات. قد يعتقد المرء أن مجرد تطبيق ما تم فعله مع [البريد الإلكتروني 1] على [البريد الإلكتروني 2] سيكون كافياً. قد يكون الاستدعاء التكراري للطريقة التي عالجت [البريد 1] كافيًا للحصول على محتويات جميع رسائل البريد الإلكتروني المتداخلة. لسوء الحظ، يتم ترقيم أجزاء [البريد 2] باستخدام منطق مختلف عن ذلك المستخدم في [البريد 1]، مما يمنع استخدام نفس الخوارزمية في كلتا الحالتين ما لم يستخدم المرء منطقًا معقدًا إلى حد ما لحساب أرقام أجزاء رسالة البريد الإلكتروني، بغض النظر عن موقعها ضمن مجموعة رسائل البريد الإلكتروني المتداخلة.
كان البرنامج النصي [imap-02.php] معقدًا بالفعل. لتجنب زيادة تعقيد معالجة محتويات رسائل البريد الإلكتروني المتداخلة، سنستخدم مكتبة [php-mime-mail-parser] المتوفرة على GitHub (مايو 2019) على الرابط [https://github.com/php-mime-mail-parser/php-mime-mail-parser] والتي كتبها Vincent Dauce.
16.6.6.1. تثبيت مكتبة [php-mime-mail-parser]
تشرح صفحة النظرة العامة للمكتبة كيفية تثبيتها على نظام Windows:

هناك خطوتان لنظام Windows:
télécharger une DLL ;
modifier le fichier [php.ini] qui configure PHP ;
مكتبة [mailparse] DLL متاحة على الرابط [http://pecl.php.net/package/mailparse] (مايو 2019)؛

- في [2]، اختر أحدث إصدار مستقر من المكتبة؛

- في [3]، حدد إصدار PHP الذي تستخدمه (في هذا المستند، هو PHP 7.2)؛
- في [4]، حدد إصدار نظام التشغيل Windows الخاص بك (هنا هو Windows 64 بت). اختر الإصدار [Thread Safe]؛
لمعرفة إصدار PHP الذي تم تنزيله مع Laragon، افتح [Terminal] من نافذة Laragon واكتب الأمر التالي:
C:\myprograms\laragon-lite\www
λ php -v
PHP 7.2.11 (cli) (built: Oct 10 2018 02:04:07) ( ZTS MSVC15 (Visual C++ 2017) x64 )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
يتم سرد إصدار PHP 7.2.11 في السطر 3. ويُدرج في نفس السطر إصدار Windows المستخدم للتجميع (32 بت أو 64 بت).
بمجرد الحصول على ملف DLL، يجب نسخه إلى المجلد [<laragon>/bin/php/<php-version>/ext] [5]:

بمجرد الانتهاء من ذلك، يجب عليك تمكين هذا الملحق في ملف [php.ini] الذي يقوم بتكوين PHP (انظر القسم المرتبط):

من المحتمل ألا يكون السطر [7] موجودًا وأنك ستحتاج إلى إضافته بنفسك.
بمجرد تمكين الملحق، يمكنك التحقق من صلاحيته عن طريق كتابة الأمر التالي في محطة Laragon:
C:\myprograms\laragon-lite\www
λ php --ini
Configuration File (php.ini) Path: C:\windows
Loaded Configuration File: C:\myprograms\laragon-lite\bin\php\php-7.2.11-Win32-VC15-x64\php.ini
Scan for additional .ini files in: (none)
Additional .ini files parsed: (none)
يقوم الأمر [php –-ini] بتحميل ملف التكوين من السطر 4. ثم يقوم بتحميل ملفات DLL لجميع الامتدادات الممكّنة في [php.ini]. إذا كان أي منها غير صحيح، فسيتم الإبلاغ عن ذلك. وبالتالي، سيتم التحقق من صحة ملف DLL المضاف [php_mailparse.dll]. قد يتم الإعلان عن عدم صحته لأسباب مختلفة، وأكثرها شيوعًا هي التالية:
- لقد قمت بتنزيل ملف DLL لا يتطابق مع إصدار PHP المستخدم؛
- قمت بتنزيل ملف DLL 32 بت بينما لديك PHP 64 بت، أو العكس؛
بمجرد تمكين الملحق والتحقق منه، يمكنك المتابعة لتثبيت مكتبة [php-mime-mail-parser]:

أدخل الأمر [8] في محطة Laragon (انظر الرابط في الفقرة):

- في [1]، تحقق من أنك في الدليل [<laragon>/www]؛
- في [2]، الأمر لتثبيت مكتبة [php-mime-mail-parser]؛
- في [3]، لم يتم تثبيت أي شيء هنا لأن مكتبة [php-mime-mail-parser] كانت مثبتة بالفعل؛
تم تثبيت مكتبة [php-mime-mail-parser] في المجلد [<laragon>/www/vendor]:


- في [2-3]، ملفات المصدر الخاصة بمكتبة [php-mime-mail-parser]؛
الآن بعد أن تم إعداد بيئة العمل، يمكننا الانتقال إلى كتابة البرنامج النصي [imap-03.php].
16.6.6.2. البرنامج النصي [imap-03.php]
يستخدم البرنامج النصي [imap-03.php] نفس ملف التكوين [config-imap-01.json] الذي استخدمته البرامج النصية السابقة:
البرنامج النصي [imap-03.php] هو كما يلي:
<?php
// IMAP (Internet Message Access Protocol) client for reading e-mails
// written with the [php-mime-mail-parser] library
// available at URL [https://github.com/php-mime-mail-parser/php-mime-mail-parser] (May 2019)
//
// strict adherence to declared types of function parameters
declare (strict_types=1);
// error management
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
// mail reading parameters
const CONFIG_FILE_NAME = "config-imap-01.json";
// we retrieve the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration " . CONFIG_FILE_NAME . " n'existe pas";
exit;
}
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// letterbox reading
foreach ($mailboxes as $name => $infos) {
// follow-up
print "------------Lecture de la boîte à lettres [$name]\n";
// reading the mailbox
readmailbox($name, $infos);
}
// end
exit;
تعليقات
- الأسطر 18–23: يتم وضع محتويات ملف التكوين في قاموس [$mailboxes]؛
- الأسطر 26–31: تتم قراءة كل صندوق بريد بواسطة الدالة [readmailbox] (السطر 30). تقوم هذه الدالة فعليًا بقراءة الرسائل غير المقروءة من صندوق البريد. يتوافق صندوق البريد مع عنوان البريد الإلكتروني لمستخدم معين؛
وظيفة [readmailbox] هي كما يلي:
function readmailbox(string $name, array $infos): void {
// we connect
$imapResource = imap_open($name, $infos["user"], $infos["password"]);
if (!$imapResource) {
// failure
print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
exit;
}
// Connection established
print "Connexion établie avec le serveur [$name].\n";
// total messages in mailbox
$nbmsg = imap_num_msg($imapResource);
print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
// unread messages in current mailbox
if ($nbmsg > 0) {
print "Récupération de la liste des messages non lus de la boîte à lettres [$name]\n";
$msgNumbers = imap_search($imapResource, 'UNSEEN');
if ($msgNumbers === FALSE) {
print "Il n'y a pas de nouveaux messages dans la boîte à lettres [$name]\n";
} else {
// browse the list of unread messages
foreach ($msgNumbers as $msgNumber) {
print "---message n° [$msgNumber]\n";
// we retrieve the body of message n° $msgNumber
getMailBody($imapResource, $msgNumber, $infos);
// if the protocol is POP3, we delete the message after retrieving it
$pop3 = $infos["pop3"];
if ($pop3 !== NULL) {
// mark the message as "to be deleted
imap_delete($imapResource, $msgNumber);
}
}
// end unread messages
if ($pop3 !== NULL) {
// messages marked as "to be deleted" are deleted
imap_expunge($imapResource);
}
}
}
// closing the connection
$imapClose = imap_close($imapResource);
if (!$imapClose) {
// failure
print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
} else {
// success
print "Fermeture de la connexion réussie.\n";
}
}
تعليقات
كود وظيفة [readmailbox] هو نفسه الموجود في البرامج النصية السابقة.
وظيفة [getMailBody] (السطر 25)، التي تقوم بتحليل نص الرسالة (المحتوى + المرفقات)، هي كما يلي:
// analyse du corps du message
function getMailBody($imapResource, int $msgNumber, array $infos): void {
// on récupère le texte entier du message
$text = imap_fetchbody($imapResource, $msgNumber, "");
if ($text === FALSE) {
print "Le corps du message [$msgNumber] n'a pu être récupéré";
return;
}
// on crée un parseur qui va analyser le texte du message
$parser = (new PhpMimeMailParser\Parser())->setText($text);
// on récupère les différentes parties du message
$outputDir = $infos["output-dir"] . "/message-$msgNumber";
getParts($parser, $msgNumber, $outputDir);
}
تعليقات
- السطر 2: تقبل الدالة [getMailBody] ثلاثة معلمات:
- [$imapResource]: مورد IMAP الذي تتصل به؛
- [$msgNumber]: رقم الرسالة (في صندوق البريد) المراد معالجتها؛
- [$infos]: معلومات متنوعة عن صندوق البريد قيد المعالجة؛
- السطر 4: يتم استرداد الرسالة #[$msgNumber] بالكامل؛
- الأسطر 5–8: حالة تعذر استرداد محتوى الرسالة؛
- السطر 10: نبدأ في استخدام مكتبة [php-mime-mail-parser]. سيكون الكائن [$parser] مسؤولاً عن تحليل نص الرسالة؛
- السطر 12: سيكون [$outputDir] هو المجلد الذي سيتم فيه حفظ محتوى النص والمرفقات للرسالة #[$msgNumber]؛
- السطر 13: نطلب من الدالة [getParts] العثور على الأجزاء المختلفة (محتوى النص والمرفقات) للرسالة #[$msgNumber] وحفظها في المجلد [$outputDir]؛
وظيفة [getParts] هي كما يلي:
// récupération des différentes parties d'un message
function getParts(PhpMimeMailParser\Parser $parser, int $msgNumber, string $outputDir): void {
// on crée le dossier de sauvegarde du message si besoin est
if (!file_exists($outputDir)) {
if (!mkdir($outputDir)) {
print "Le dossier [$outputDir] n'a pu être créé\n";
return;
}
}
// on récupère les entêtes du message
$arrayHeaders = $parser->getHeaders();
// on sauvegarde les messages texte
$parts = $parser->getInlineParts("text");
for ($i = 1; $i <= count($parts); $i++) {
print "-- Sauvegarde d'un message de type [text/plain]\n";
saveMessage($parts[$i - 1], 0, $arrayHeaders, "$outputDir/message_$i.txt");
}
// on sauvegarde les messages html
$parts = $parser->getInlineParts("html");
for ($i = 1; $i <= count($parts); $i++) {
print "-- Sauvegarde d'un message de type [text/html]\n";
saveMessage($parts[$i - 1], 1, $arrayHeaders, "$outputDir/message_$i.html");
}
// on récupère les attachements du message
$attachments = $parser->getAttachments();
// n° de l'attachement
$iAttachment = 0;
// on parcourt la liste des attachements
foreach ($attachments as $attachment) {
// type d'attachement
$fileType = $attachment->getContentType();
print "-- Sauvegarde d'un attachement de type [$fileType] dans le fichier [$outputDir/{$attachment->getFilename()}]\n";
// on sauvegarde l'attachement
try {
$attachment->save($outputDir, PhpMimeMailParser\Parser::ATTACHMENT_DUPLICATE_SUFFIX);
} catch (Exception $e) {
print "L'attachement n'a pu être sauvegardé : " . $e->getMessage() . "\n";
}
// cas particulier du type message/rfc822
if ($fileType === "message/rfc822") {
// l'attachement est lui-même un message - on va le parser lui aussi
// on change de répertoire de sauvegarde
$iAttachment++;
$outputDir = $outputDir . "/rfc822-$iAttachment";
// on change le contenu à parser
$parser->setText($attachment->getContent());
// on analyse le message de façon récursive
getParts($parser, $msgNumber, $outputDir);
}
}
}
تعليقات
- السطر 2: تأخذ الدالة [getParts] ثلاثة معلمات:
- محلل [$parser] تم تمرير نص الرسالة الكامل المراد تحليله إليه؛
- [$msgNumber] هو رقم الرسالة التي يتم تحليلها حاليًا؛
- [$outputDir] هو الدليل الذي يجب حفظ محتويات الرسالة ومرفقاتها فيه؛
- الأسطر 4-9: إنشاء مجلد [$outputDir]؛
- السطر 11: استرداد رؤوس الرسالة التي يتم تحليلها (من، إلى، الموضوع، إلخ)؛
- السطر 13: استرداد أجزاء البريد الإلكتروني من النوع [text/plain]. استرداد مصفوفة؛
- الأسطر 14-17: حفظ جميع عناصر المصفوفة التي تم استردادها، مع إعطاء كل منها اسم ملف مختلف؛
- السطر 19: استرداد أجزاء البريد الإلكتروني من النوع [text/html]. يتم استرداد جدول؛
- الأسطر 20-23: نحفظ جميع عناصر المصفوفة المسترجعة، مع إعطاء كل منها اسم ملف مختلف؛
- السطر 25: استرداد قائمة المرفقات للرسالة التي تم تحليلها؛
- السطر 29: نكرر هذه القائمة؛
- السطر 24: استرجاع نوع المرفق (سمة Content-Type)؛
- الأسطر 34–38: حفظ المرفق في المجلد [$outputDir]. المعلمة الثانية [PhpMimeMailParser\Parser::ATTACHMENT_DUPLICATE_SUFFIX] هي استراتيجية تسمية للمرفقات. إذا كان [$attachment→getFilename()] هو X وكان الملف X موجودًا بالفعل، فإن مكتبة [php-mime-mail-parser] تجرب الأسماء [X_1] و[X_2] وما إلى ذلك، حتى تجد اسم ملف غير موجود؛
- السطر 40: نتحقق مما إذا كان المرفق عبارة عن بريد إلكتروني؛
- الأسطر 41–48: إذا كان الأمر كذلك، يتم تحليل هذه الرسالة الإلكترونية بدورها لاستخراج محتوياتها ومرفقاتها؛
- السطر 44: إذا تم تعيين [$outputDir] إلى X وكانت الرسالة التي تم تحليلها تحتوي على مرفقين بريد إلكتروني، فسيتم حفظ الأول في المجلد [$outputDir/rfc822-1] والثاني في المجلد [$outputDir/rfc822-2]؛
- السطر 46: يصبح محتوى البريد الإلكتروني المرفق النص الجديد المراد تحليله؛
- السطر 48: يتم استدعاء الدالة [getParts] بشكل متكرر لتحليل النص الجديد؛
تقوم الدالة [saveMessage] بحفظ محتوى النص للرسالة المراد تحليلها:
// sauvegarde d'un message texte
function saveMessage(string $text, int $type, array $arrayHeaders, string $filename): void {
// contenu à sauvegarder
$contents = "";
// ajout des entêtes
switch ($type) {
case 0:
// text/plain
foreach ($arrayHeaders as $key => $value) {
$contents .= "$key: $value\n";
}
$contents .= "\n";
break;
case 1:
// text/HTML
foreach ($arrayHeaders as $key => $value) {
$contents .= "$key: $value<br/>\n";
}
$contents .= "<br/>\n";
}
// ajout du texte du message
$contents .= $text;
// sauvegarde du tout
if (!file_put_contents($filename, $contents)) {
// échec
print "Le message n'a pu être sauvegardé dans le fichier [$filename]\n";
} else {
// réussite
print "Le message a été sauvegardé dans le fichier [$filename]\n";
}
}
تعليقات
- تقبل الدالة [saveMessage] المعلمات التالية:
- [$text]: النص المراد حفظه؛
- [$type]: نوع النص (0: text/plain، 1: text/HTML)؛
- [$arrayHeaders]: رؤوس الرسالة التي تم تحليلها؛
- [$filename]: اسم الملف الذي يجب حفظ [$text] فيه؛
- السطر 4: سيمثل [$contents] النص الكامل المراد حفظه؛
- الأسطر 6–20: أولاً، سيتم حفظ جميع رؤوس الرسالة (من، إلى، الموضوع، إلخ)؛
- الأسطر 16–19: بالنسبة للنص HTML، ينتهي كل سطر بعلامة <br/> بحيث يظهر كل عنوان في سطر منفصل في المتصفح؛
- السطر 22: يُضاف نص الرسالة المراد حفظه إلى العناوين؛
- الأسطر 24–30: يتم حفظ المجموعة بأكملها في الملف [$filename]؛
يؤدي استخدام مكتبة [php-mime-mail-parser] إلى تبسيط كتابة البرنامج النصي لقراءة البريد الإلكتروني بشكل كبير.
يُستخدم البرنامج النصي [smtp-02.php] لإرسال بريد إلكتروني إلى المستخدم [guest@localhost] بالتكوين التالي:
- الأسطر 11–15: هناك خمسة مرفقات؛
- السطر 15: [test-localhost-2.eml] هو بريد إلكتروني مبني على النحو التالي:
- يحتوي [test-localhost-2.eml] على 4 مرفقات (نفس المرفقات الموجودة في الأسطر 11–14) ورسالة بريد إلكتروني مرفقة؛
- تحتوي رسالة البريد الإلكتروني المرفقة بـ [test-localhost-2.eml] على 4 مرفقات (نفس المرفقات الموجودة في الأسطر 11–14)؛
يُستخدم البرنامج النصي [imap-03.php] لقراءة صندوق بريد المستخدم [guest@localhost] باستخدام الإعدادات التالية:
بعد التنفيذ، أصبحت بنية مجلد [output/localhost-pop3] كما يلي:

- في [1]، المرفقات الخمسة من البريد الإلكتروني الذي استلمه [guest@localhost]؛
- في [2]، المرفقات الخمسة من البريد الإلكتروني [test-localhost-2.eml] في [1]؛
- في [3]، المرفقات الأربعة من البريد الإلكتروني [test-localhost.eml] الموجود في [2]؛
إخراج وحدة التحكم كما يلي:
------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [1] messages dans la boîte à lettres [{localhost:110/pop3}]
Récupération de la liste des messages non lus de la boîte à lettres [{localhost:110/pop3}]
---message n° [1]
-- Sauvegarde d'un message de type [text/plain]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/message_1.txt]
-- Sauvegarde d'un message de type [text/html]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/message_1.html]
-- Sauvegarde d'un attachement de type [application/vnd.openxmlformats-officedocument.wordprocessingml.document] dans le fichier [output/localhost-pop3/message-1/Hello from SwiftMailer.docx]
-- Sauvegarde d'un attachement de type [application/pdf] dans le fichier [output/localhost-pop3/message-1/Hello from SwiftMailer.pdf]
-- Sauvegarde d'un attachement de type [application/vnd.oasis.opendocument.text] dans le fichier [output/localhost-pop3/message-1/Hello from SwiftMailer.odt]
-- Sauvegarde d'un attachement de type [image/png] dans le fichier [output/localhost-pop3/message-1/Cours-Tutoriels-Serge-Tahé-1568x268.png]
-- Sauvegarde d'un attachement de type [message/rfc822] dans le fichier [output/localhost-pop3/message-1/test-localhost-2.eml]
-- Sauvegarde d'un message de type [text/plain]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/message_1.txt]
-- Sauvegarde d'un message de type [text/html]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/message_1.html]
-- Sauvegarde d'un attachement de type [application/vnd.openxmlformats-officedocument.wordprocessingml.document] dans le fichier [output/localhost-pop3/message-1/rfc822-1/Hello from SwiftMailer.docx]
-- Sauvegarde d'un attachement de type [application/pdf] dans le fichier [output/localhost-pop3/message-1/rfc822-1/Hello from SwiftMailer.pdf]
-- Sauvegarde d'un attachement de type [application/vnd.oasis.opendocument.text] dans le fichier [output/localhost-pop3/message-1/rfc822-1/Hello from SwiftMailer.odt]
-- Sauvegarde d'un attachement de type [image/png] dans le fichier [output/localhost-pop3/message-1/rfc822-1/Cours-Tutoriels-Serge-Tahé-1568x268.png]
-- Sauvegarde d'un attachement de type [message/rfc822] dans le fichier [output/localhost-pop3/message-1/rfc822-1/test-localhost.eml]
-- Sauvegarde d'un message de type [text/plain]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/message_1.txt]
-- Sauvegarde d'un message de type [text/html]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/message_1.html]
-- Sauvegarde d'un attachement de type [application/vnd.openxmlformats-officedocument.wordprocessingml.document] dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Hello from SwiftMailer.docx]
-- Sauvegarde d'un attachement de type [application/pdf] dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Hello from SwiftMailer.pdf]
-- Sauvegarde d'un attachement de type [application/vnd.oasis.opendocument.text] dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Hello from SwiftMailer.odt]
-- Sauvegarde d'un attachement de type [image/png] dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Cours-Tutoriels-Serge-Tahé-1568x268.png]
Fermeture de la connexion réussie.
إذا قمت بعرض [message_1.HTML] من [3] في متصفح، فستحصل على ما يلي:
