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, він повинен бути здатний її інтерпретувати. Це аналогічно ситуації, коли дві особи, А і Б, спілкуються по телефону: їхній діалог передається через телефон. Мова кодується у вигляді сигналів телефоном А, передається по телефонних лініях, досягає телефону Б, де її декодують. Тоді людина Б чує слова. Саме тут виникає поняття протоколу діалогу: якщо А розмовляє французькою, а Б не розуміє цієї мови, А і Б не зможуть вести змістовний діалог;
Тому обидва додатки, що спілкуються, повинні домовитися про тип діалогу, який вони використовуватимуть. Наприклад, діалог із сервісом ftp відрізняється від діалогу із сервісом pop: ці два сервіси не приймають однакових команд. Вони мають різний протокол діалогу;
16.1.2. Характеристики протоколу TCP
Тут ми розглянемо лише мережеві комунікації, що використовують транспортний протокол TCP, основні характеристики якого наведені нижче:
- процес, який бажає здійснити передачу, спочатку встановлює з’єднання з процесом-одержувачем інформації, яку він збирається передати. Це з’єднання встановлюється між портом машини-відправника та портом машини-одержувача. Між цими двома портами створюється віртуальний шлях, який буде зарезервований виключно для двох процесів, що встановили з’єднання;
- усі пакети, що надсилаються процесом-джерелом, проходять цим віртуальним шляхом і надходять у тому порядку, в якому вони були відправлені;
- передана інформація має безперервний характер. Процес-відправник надсилає інформацію у своєму темпі. Вона не обов’язково надсилається одразу: протокол TCP чекає, поки її накопичиться достатньо для відправлення. Інформація зберігається у структурі, що називається сегментом TCP. Цей сегмент, щойно заповнений, передається на рівень IP, де він інкапсулюється в пакет IP;
- кожен сегмент, надісланий за протоколом TCP, має свій номер. Протокол TCP одержувача перевіряє, чи сегменти надходять у правильній послідовності. За кожен правильно отриманий сегмент він надсилає підтвердження про отримання відправнику;
- коли останній отримує його, він повідомляє про це процес-відправник. Таким чином, останній може дізнатися, що сегмент надійшов за призначенням;
- якщо через певний час протокол TCP, який відправив сегмент, не отримає підтвердження про отримання, він повторно надсилає цей сегмент, тим самим гарантуючи якість послуги передачі інформації;
- віртуальний канал, встановлений між двома процесами, що взаємодіють, має номер full-duplex: це означає, що інформація може передаватися в обох напрямках. Таким чином, процес-приймач може надсилати підтвердження про отримання навіть тоді, коли процес-відправник продовжує надсилати інформацію. Це дозволяє, наприклад, протоколу-відправнику TCP надсилати кілька сегментів, не чекаючи на підтвердження про отримання. Якщо через певний час він виявляє, що не отримав підтвердження про отримання певного сегмента № n, він відновить передачу сегментів з цього моменту;
16.1.3. Взаємодія «клієнт-сервер»
Часто комунікація в Інтернеті є асиметричною: машина А ініціює з’єднання, щоб звернутися до машини Б із запитом на послугу: вона вказує, що хоче встановити з’єднання зі службою SB1 на машині Б. Остання приймає або відхиляє цей запит. Якщо вона приймає, машина А може надсилати свої запити до служби SB1. Ці запити повинні відповідати протоколу взаємодії, який розуміє служба SB1. Таким чином між машиною А, яку називають клієнтською машиною, та машиною Б, яку називають серверною машиною, встановлюється діалог «запит-відповідь». Один із двох партнерів закриє з’єднання.
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: HyperText Transfer Protocol — протокол взаємодії з веб-сервером (сервер HTTP);
- SMTP: Simple Mail Transfer Protocol — протокол взаємодії з сервером відправлення електронної пошти (сервер SMTP);
- POP: Post Office Protocol — протокол взаємодії з сервером зберігання електронної пошти (сервер POP). Він призначений для отримання електронних листів, а не для їх надсилання;
- IMAP: Internet Message Access Protocol — протокол взаємодії з сервером зберігання електронної пошти (сервер IMAP). Цей протокол поступово замінив старіший протокол POP;
- FTP: File Transfer Protocol — протокол взаємодії з сервером зберігання файлів (сервер FTP);
Усі ці протоколи мають особливість бути текстовими: клієнт і сервер обмінюються текстовими рядками. Якщо у нас є клієнт, здатний:
- встановити з’єднання з сервером TCP;
- виводити на консоль текстові рядки, які надсилає йому сервер;
- надіслати на сервер рядки тексту, які користувач вводить з клавіатури;
тож ми можемо взаємодіяти з сервером TCP, що використовує протокол текстових рядків, за умови, що ми знаємо правила цього протоколу.
16.2.2. Утиліти TCP

У кодах, пов’язаних із цим документом, містяться дві утиліти зв’язку TCP:
- [RawTcpClient] дозволяє підключитися до порту P сервера S;
- [RawTcpServer] дозволяє створити сервер, який очікує на клієнтів на порту P;
Сервер TCP [RawTcpServer]викликається за допомогою синтаксису [RawTcpServeur port] для створення служби TCP на порту [port] локальної машини (комп’ютера, на якому ви працюєте):
- сервер може обслуговувати декількох клієнтів одночасно;
- сервер виконує команди, введені користувачем за допомогою клавіатури. Це такі команди:
- list: виводить список клієнтів, які наразі підключені до сервера. Вони відображаються у форматі [id=x-nom=y]. Поле [id] слугує для ідентифікації клієнтів;
- send x [texte]: надсилає текст клієнту № x (id=x). Квадратні дужки [] не надсилаються. Вони необхідні в команді. Вони слугують для візуального виділення тексту, що надсилається клієнту;
- close x: закриває з’єднання з клієнтом № x;
- quit: закриває всі з’єднання та зупиняє роботу служби;
- рядки, надіслані клієнтом на сервер, відображаються на консолі;
- увесь обмін даними записується у текстовий файл із назвою [machine-portService.txt], де
- [machine] — ім’я комп’ютера, на якому виконується код;
- [port] — порт служби, що відповідає на запити клієнта;
Клієнт TCP [RawTcpClient] викликається за допомогою синтаксису [RawTcpClient serveur port] для підключення до порту [port] сервера [serveur]:
- рядки, введені користувачем з клавіатури, надсилаються на сервер;
- рядки, надіслані сервером, відображаються на консолі;
- увесь обмін даними записується у текстовий файл із назвою [serveur-port.txt];
Розглянемо приклад. Відкриваємо два командні вікна Windows і в кожному з них переходимо до папки з утилітами. В одному з вікон запускаємо сервер [RawTcpServer] на порту 100:

- у [1] ми перебуваємо в папці з утилітами;
- у [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] — журнали сервера: ім’я файлу відповідає імені клієнта [machine-port];
- [26] — журнали клієнта: ім’я файлу відповідає імені сервера [machine-port];
Журнали сервера мають такий вигляд:
Журнали клієнта мають такий вигляд:
16.3. Отримати ім’я або адресу IP комп’ютера в Інтернеті

Комп'ютери в Інтернеті ідентифікуються за адресою IP (IPv4 або IPv6) і найчастіше за іменем. Але в кінцевому підсумку використовується лише адреса IP. Тому іноді потрібно знати адресу IP комп’ютера, ідентифікованого за його ім’ям.
Скрипт [ip-01.php] має такий вигляд:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
//
// обробка помилок
error_reporting(E_ALL & E_STRICT);
ini_set("display_errors", "on");
//
// константи
$HOTES = array("istia.univ-angers.fr", "www.univ-angers.fr", "www.ibm.com", "localhost", "", "xx");
// адреси IP та імена машин $HOTES
for ($i = 0; $i < count($HOTES); $i++) {
getIPandName($HOTES[$i]);
}
// кінець
print "Terminé\n";
exit;
//------------------------------------------------
function getIPandName(string $nomMachine): void {
//$nomMachine: ім’я машини, адресу якої потрібно отримати IP
//
// nomMachine-->адреса IP
$ip = gethostbyname($nomMachine);
print "---------------\n";
if ($ip !== $nomMachine) {
print "ip[$nomMachine]=$ip\n";
// адреса 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($nom) дозволяє отримати адресу IP «ip3.ip2.ip1.ip0» комп’ютера з іменем $nom. Якщо комп'ютер $nom не існує, функція повертає $nom як результат;
- рядок 30: функція gethostbyaddr($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 (HyperText Transfer Protocol)
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: serveur:port]. Регістр команди [Host] не має значення. Нагадаємо, що клієнт звертається до локального сервера, який працює на порту 100;
- команда [User-Agent] вказує ідентифікатор клієнта;
- команда [Accept] вказує, які типи документів приймає клієнт;
- команда [Accept-Language] вказує, якою мовою бажано отримати запитувані документи, якщо вони існують кількома мовами;
- команда [Connection] вказує бажаний режим з’єднання: [keep-alive] вказує, що з’єднання має підтримуватися до завершення обміну даними;
- у [7]: клієнт завершує свої команди порожнім рядком;
Ми завершуємо з’єднання, зупиняючи сервер:

16.4.2. Приклад 2
Тепер, коли ми знаємо команди, що надсилаються браузером для запиту URL, ми будемо запитувати цей 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]. Ви отримаєте такий вихідний код:
Тепер завантажимо 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: цільовий комп’ютер у форматі «комп’ютер:порт»;
- рядок 6: ідентифікатор клієнта HTTP: можна вказати будь-що;
- рядок 7: тип документа, який підтримує клієнт, у даному випадку текст HTML;
- рядок 8: бажана мова для запитуваного документа;
- рядок 9: символ кінця рядка для команд, що надсилаються клієнтом: він може відрізнятися залежно від того, чи сервер працює на комп’ютері з ОС Unix (\n) чи Windows (\r\n);
Скрипт [http-01.php] має такий вигляд:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
//
// обробка помилок
// error_reporting(E_ALL & E_STRICT);
// ini_set("display_errors", "on");
//
// константи
const CONFIG_FILE_NAME = "config-http-01.json";
//
// отримуємо конфігурацію
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// отримання тексту HTML з URL конфігураційного файлу
foreach ($config as $site => $protocole) {
// читання індекс-сторінки сайту $ite
$résultat = getURL($site, $protocole);
// відображення результату
print "$résultat\n";
}//для
// кінець
exit;
//-----------------------------------------------------------------------
function getURL(string $site, array $protocole, $suivi = TRUE): string {
// зчитує $siteURL та зберігає його у файлі $site.HTML
// діалог «клієнт-сервер» відбувається за протоколом $protocole
//
// відкриття з'єднання на порту $site
$erreurNumber = 0;
$erreur = "";
$connexion = fsockopen($site, $protocole["port"], $erreurNumber, $erreur);
// повернення у разі помилки
if ($connexion === FALSE) {
return "Echec de la connexion au site (" . $site . " ," . $protocole["port"] . " : $erreur";
}
// $connexion представляє двонаправлений потік обміну даними
// між клієнтом (цією програмою) та веб-сервером, до якого звертаються
// цей канал використовується для обміну командами та інформацією
// протокол взаємодії — HTTP
//
// створення файлу $site.HTML
$HTML = fopen("output/$site.HTML", "w");
if ($HTML === FALSE) {
// закриття з'єднання клієнт/сервер
fclose($connexion);
// повернення помилки
return "Erreur lors de la création du fichier $site.HTML";
}
// клієнт розпочне діалог HTTP із сервером
if ($suivi) {
print "Client : début de la communication avec le serveur [$site] ----------------------------\n";
}
// залежно від сервера рядки клієнта повинні закінчуватися символами \n або \r\n
$endOfLine = $protocole["endOfLine"];
// для спрощення ми не перевіряємо випадки помилок у зв’язку між клієнтом і сервером
// клієнт надсилає команду GET, щоб запросити URL $protocole["GET"]
// синтаксис GET URL HTTP/1.1
$commande = "GET " . $protocole["GET"] . " HTTP/1.1$endOfLine";
// відстеження?
if ($suivi) {
print "--> $commande";
}
// команда надсилається на сервер
fputs($connexion, $commande);
// відправлення інших заголовків HTTP
foreach ($protocole as $verb => $value) {
if ($verb !== "GET" && $verb != "port"" && $verb !="endOfLine") {
// формується команда
$commande = "$verb: $value$endOfLine";
// продовження?
if ($suivi) {
print "--> $commande";
}
// команда надсилається на сервер
fputs($connexion, $commande);
}
}
// заголовки (headers) протоколу HTTP повинні закінчуватися порожнім рядком
fputs($connexion, $endOfLine);
//
// сервер тепер відповість на каналі $connexion. Він надішле всі
// свої дані, а потім закриє канал. Отже, клієнт зчитує все, що надходить з $connexion
// до закриття каналу
//
// спочатку зчитуються заголовки HTTP, надіслані сервером
// вони також закінчуються порожнім рядком
if ($suivi) {
print "Réponse du serveur [$site] ----------------------------\n";
}
$fini = FALSE;
while (!$fini && $ligne = fgets($connexion, 1000)) {
// чи є порожній рядок?
$champs = [];
preg_match("/^(.*?)\s+$/", $ligne, $champs);
if ($champs[1] !== "") {
if ($suivi) {
// виводимо заголовок HTTP
print "<-- " . $champs[1] . "\n";
}
} else {
// це був порожній рядок — заголовки HTTP завершені
$fini = TRUE;
}
}
// зчитується документ HTML, який йтиме після порожнього рядка
while ($ligne = fgets($connexion, 1000)) {
// рядок зберігається у файлі HTML на сайті
fputs($HTML, $ligne);
}
// сервер закрив з'єднання — клієнт у свою чергу закриває його
fclose($connexion);
// закриття файлу $HTML
fclose($HTML);
// повернення
return "Fin de la communication avec le site [$site]. Vérifiez le fichier [$site.HTML]";
}
Коментарі до коду:
- рядок 14: файл конфігурації використовується для створення словника:
- ключами словника є веб-сервери, до яких слід надсилати запити;
- значення визначають протокол HTTP, якого слід дотримуватися;
- рядки 16–21: виконується цикл по списку веб-серверів із конфігурації;
- рядок 26: функція getURL($site,$protocole,$suivi) запитує документ із веб-сайту $site і зберігає його у текстовому файлі $site.HTML.Par: за замовчуванням обмін даними між клієнтом і сервером реєструється в консолі ($suivi=TRUE);
- рядок 33: функція fsockopen($site,$port,$errNumber,$erreur) дозволяє встановити з’єднання зі службою TCP / IP, що працює на порту $port комп’ютера $site. Якщо з’єднання не вдалося встановити, [$errNumber] — це номер помилки, а [$erreur] — відповідне повідомлення про помилку. Після встановлення з’єднання між клієнтом і сервером численні служби TCP / IP обмінюються рядками тексту. У даному випадку це протокол HTTP (HyperText Transfer Protocol). Потік даних, що надходить від сервера до клієнта, можна розглядати як текстовий файл, який зчитується за допомогою [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: якщо ми перебуваємо в режимі [suivi], отриманий заголовок 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 не має символу / наприкінці. Це не ті самі URL. Виконання клієнта 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/курси-уроки-програмування/
<-- 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 fichier URL].
Щоб уникнути захаращення папки [www] у Laragon, перемістимося в інше місце файлової системи:

- у [1] перейдемо до папки [c:\temp]. Якщо ця папка не існує, ви можете створити її або вибрати іншу;
- у [2] створюємо папку з назвою [curl];
- у [3] вибираємо цю папку;
- у [4] переглядаємо його вміст. Він порожній;
Переконайтеся, що сервер Apache в Laragon запущений, і за допомогою [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 до хосту localhost залишено без змін
- рядки 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 до хосту tahe.developpez.com залишено без змін
- рядки 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/курси-уроки-з-програмування
% 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/курси-уроки-програмування/
< 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 для розміщення sergetahe.com без змін
* Issue another request to this URL: 'http://sergetahe.com/курси-уроки-програмування/'
* Found bundle for host sergetahe.com: 0x1c88548 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) з хостом 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 на хост sergetahe.com залишено без змін
- рядок 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:
Кожен елемент словника [clé, valeur] має таку структуру:
- clé: ім’я веб-сервера;
- valeur — словник із такими ключами:
- timeout — максимальний час очікування відповіді від сервера. Після його закінчення клієнт роз'єднається;
- url: URL запитуваного документа;
Код скрипта [http-02.php] такий:
<?php
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
//
// обробка помилок
//error_reporting(E_ALL & E_STRICT);
//ini_set("display_errors", "on");
//
// константи
const CONFIG_FILE_NAME = "config-http-02.json";
//
// отримуємо конфігурацію
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// отримання тексту HTML з URL у файлі конфігурації
foreach ($config as $site => $infos) {
// читання URL з сайту $ite
$résultat = getUrl($site, $infos["url"], $infos["timeout"]);
// відображення результату
print "$résultat\n";
}//для
// кінець
exit;
//-----------------------------------------------------------------------
function getUrl(string $site, string $url, int $timeout, $suivi = TRUE): string {
// зчитує URL $url і зберігає його у файлі output/$site.HTML
//
// продовження
print "Client : début de la communication avec le serveur [$site] ----------------------------\n";
// Ініціалізація сеансу cURL
$curl = curl_init($url);
if ($curl === FALSE) {
// сталася помилка
return "Erreur lors de l'initialisation de la session cURL pour le site [$site]";
}
// параметри curl
$options = [
// режим детального виведення
CURLOPT_VERBOSE => true,
// нове з'єднання — кеш відсутній
CURLOPT_FRESH_CONNECT => true,
// час очікування запиту (у секундах)
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => $timeout,
// не перевіряти дійсність сертифікатів SSL
CURLOPT_SSL_VERIFYPEER => false,
// дотримуватися перенаправлень
CURLOPT_FOLLOWLOCATION => true,
// отримати запитуваний документ у вигляді символьного рядка
CURLOPT_RETURNTRANSFER => true
];
// налаштування curl
curl_setopt_array($curl, $options);
// Виконання запиту
$page_content = curl_exec($curl);
// закриття сеансу cURL
curl_close($curl);
// обробка результату
if ($page_content !== FALSE) {
// збереження результату в $site.HTML
$result = file_put_contents("output/$site.HTML", $page_content);
if ($result === FALSE) {
// повернення з помилкою
return "Erreur lors de la création du fichier [output/$site.HTML]";
}
// повернення з успішним результатом
return "Fin de la communication avec le serveur [$site]. Vérifiez le fichier [output/$site.HTML]";
} else {
// сталася помилка зв’язку
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/курси-уроки-з-програмування
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 на хості sergetahe.com залишилося без змін
* Issue another request to this URL: 'http://sergetahe.com/курси-уроки-з-програмування'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) з хостом 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/курси-та-посібники-з-програмування/
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< Cache-control: private
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 на хост sergetahe.com залишився без змін
* Issue another request to this URL: 'http://sergetahe.com/курси-уроки-програмування/'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) з хостом 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/курси-уроки-програмування/wp-json/>; rel="https://api.w.org/"
< Link: <http://sergetahe.com/курси-уроки-програмування/>; 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 для розміщення sergetahe.com без змін
* 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 на хост tahe.developpez.com залишено без змін
* 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 для розміщення www.polytech-angers.fr без змін
* 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) з хостом 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 до хосту www.polytech-angers.fr залишився без змін
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 для хосту localhost залишено без змін
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 (Simple Mail Transfer Protocol)
16.5.1. Вступ

У цьому розділі:
- [Serveur B] буде локальним сервером SMTP, який ми встановимо;
- [Client 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] — папка «Вхідні» (Inbox) користувача [PHP7] (16);
- у [17] цей користувач має порожню папку «Вхідні»;
- у [18-19] увійдіть до облікового запису Google користувача [php7parlexemple@gmail.com]. Ми налаштуємо безпеку облікового запису;

- у [21] надайте дозвіл іншим додаткам, окрім додатків Google, використовувати обліковий запис [php7parlexemple]. Якщо цього не зробити, наш локальний поштовий сервер [hMailServer] не зможе зв’язуватися із сервером Gmail SMTP;

16.5.3. Встановлення сервера SMTP
Для наших тестів ми встановимо поштовий сервер [hMailServer], який є одночасно сервером SMTP, що дозволяє надсилати електронні листи, а також сервером POP3 (Post Office Protocol), що дозволяє читати листи, збережені на сервері, та сервером IMAP (Internet Message Access Protocol), який також дозволяє читати листи, збережені на сервері, але має ширші можливості. Зокрема, він дозволяє керувати зберіганням листів на сервері.
Поштовий сервер [hMailServer] доступний на URL [https://www.hmailserver.com/] (травень 2019 року).

Під час встановлення вам буде запропоновано ввести певну інформацію:

- у [1-2] виберіть як поштовий сервер, так і інструменти для його адміністрування;
- під час встановлення вас попросять ввести пароль адміністратора: запишіть його, оскільки він вам знадобиться;
[hMailServer] встановлюється як служба Windows, що запускається автоматично під час завантаження системи. Краще вибрати ручний запуск:
- у [3] введіть [services] у поле введення на панелі стану;

- у [4-8] переведіть службу в режим [manuel] (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] служба Gmail SMTP є захищеною: для доступу до неї потрібен обліковий запис 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 nom-de-la-machine-client]. Сервер відповідає йому серією повідомлень у форматі [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]. Це не те, чого ми хотіли, але якщо не використовувати цей релейний сервер, сервіс Gmail SMTP відхиляє листи, надіслані з [hMailServer], оскільки сервіс Gmail SMTP вимагає автентифікації, яку [hMailServer] не надсилає. Безсумнівно, існує спосіб обійти цю проблему, але я його не знайшов;
- у [8] видно, що лист було отримано з машини [DESKTOP-528I5CU], на якій розміщений поштовий сервер [hMailServer];
- у [9] вказано відправника повідомлення. Бачимо, що це не [guest@localhost];
- у [10] — оригінальний відправник повідомлення. Цього разу це дійсно [guest@localhost];
- у [11] — тема;
- у [12] — одержувач;
- у [13] — повідомлення;
Зрештою, наш клієнт [RawTcpClient] зміг надіслати повідомлення, хоча й виникла проблема з відправником. Ми маємо основу для створення клієнта SMTP, написаного у форматі PHP.
16.5.5. Базовий клієнт SMTP, написаний на мові PHP
Ми відтворимо в PHP те, що ми раніше вивчили з протоколу SMTP.

Скрипт [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] — це масив, кожен елемент якого є словником типу [nom=>infos]. Значення [infos] саме по собі є словником із такими ключами та значеннями:
- [smtp-server]: ім’я сервера SMTP, яке слід використовувати;
- [smtp-port]: номер порту служби SMTP;
- [from]: відправник повідомлення;
- [to]: одержувач повідомлення;
- [subject]: тема повідомлення;
- [message]: повідомлення, яке потрібно надіслати;
- Перший елемент використовує сервер SMTP [localhost] для надсилання електронного листа користувачеві [localhost];
- 2-й елемент використовує сервери SMTP та [localhost] для надсилання електронного листа користувачеві з [Gmail];
- третій елемент використовує сервер SMTP [Gmail] для надсилання електронного листа користувачеві [Gmail];
Код [smtp-01.php] клієнта SMTP виглядає наступним чином:
<?php
// клієнт SMTP (протокол передачі SendMail), що дозволяє надсилати повідомлення
// протокол зв’язку «клієнт-сервер» SMTP
// -> клієнт підключається до порту 25 сервера SMTP
// <- сервер надсилає йому привітальне повідомлення
// -> клієнт надсилає команду EHLO із зазначенням імені свого комп’ютера
// <- сервер відповідає OK або ні
// -> клієнт надсилає команду MAIL FROM: <відправник>
// <- сервер відповідає OK або ні
// -> клієнт надсилає команду RCPT TO: <адресат>
// <- сервер відповідає OK або ні
// -> клієнт надсилає команду DATA
// <- сервер відповідає OK або ні
// -> клієнт надсилає всі рядки свого повідомлення і завершує його рядком, що містить
// єдиний символ.
// <- сервер відповідає OK або ні
// -> клієнт надсилає команду QUIT
// <- сервер відповідає OK або ні
// відповіді сервера мають вигляд xxx текст, де xxx — трицифрове число. Будь-яке
// число xxx >=500 вказує на помилку.
// Відповідь може містити кілька рядків, які починаються з xxx, за винятком останнього
// у форматі xxx (пробіл)
// обмінювані рядки тексту повинні закінчуватися символами RC(#13) та LF(#10)
//
// клієнт SMTP (протокол передачі SendMail), що дозволяє надсилати повідомлення
//
// обробка помилок
//ini_set («error_reporting», E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
//
// параметри відправлення пошти
const CONFIG_FILE_NAME = "config-smtp-01.json";
// отримання конфігурації
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// відправлення листів
foreach ($mails as $name => $infos) {
// відстеження
print "Envoi du mail [$name]\n";
// відправлення листа
$résultat = sendmail($name, $infos, TRUE);
// відображення результату
print "$résultat\n";
}//для
// завершення
exit;
//sendmail
//-----------------------------------------------------------------------
function sendmail(string $name, array $infos, bool $verbose = TRUE): string {
// надсилає повідомлення [$name,$infos]. Якщо $verbose=TRUE , відстежується обмін даними між клієнтом і сервером
// отримується ім’я клієнта
$client = gethostbyaddr(gethostbyname(""));
// відкриття з’єднання з сервером SMTP
$connexion = fsockopen($infos["smtp-server"], (int) $infos["smtp-port"]);
// повернення у разі помилки
if ($connexion === FALSE) {
return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
}
// $connexion представляє двосторонній потік обміну даними
// між клієнтом (цією програмою) та SMTP-сервером, до якого встановлено з’єднання
// цей канал використовується для обміну командами та інформацією
// після встановлення з’єднання сервер надсилає привітальне повідомлення, яке ми читаємо
$erreur = sendCommand($connexion, "", $verbose, TRUE);
if ($erreur !== "") {
// завершення з'єднання
fclose($connexion);
// повернення
return $erreur;
}
// команда EHLO
$erreur = sendCommand($connexion, "EHLO $client", $verbose, TRUE);
if ($erreur !== "") {
// завершення з'єднання
fclose($connexion);
// повернення
return $erreur;
}
// команда MAIL FROM:
$erreur = sendCommand($connexion, sprintf("MAIL FROM: <%s>", $infos["from"]), $verbose, TRUE);
if ($erreur !== "") {
// завершення з'єднання
fclose($connexion);
// повернення
return $erreur;
}
// команда RCPT TO:
$erreur = sendCommand($connexion, sprintf("RCPT TO: <%s>", $infos["to"]), $verbose, TRUE);
if ($erreur !== "") {
// завершення з'єднання
fclose($connexion);
// повернення
return $erreur;
}
// команда DATA
$erreur = sendCommand($connexion, "DATA", $verbose, TRUE);
if ($erreur !== "") {
// закриття з'єднання
fclose($connexion);
// повернення
return $erreur;
}
// підготовка повідомлення для відправлення
// воно має містити такі рядки
// From: відправник
// To: одержувач
// Subject:
// порожній рядок
// Повідомлення
// .
$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 !== "") {
// завершення з'єднання
fclose($connexion);
// повернення
return $erreur;
}
// команда quit
$erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
if ($erreur !== "") {
// закриття з'єднання
fclose($connexion);
// повернення
return $erreur;
}
// кінець
fclose($connexion);
return "Message envoyé";
}
// --------------------------------------------------------------------------
function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF): string {
// надсилає $commande у канал $connexion
// режим детального виведення, якщо $verbose=1
// якщо $withRCLF=1, додає послідовність RCLF до обміну
// дані
if ($withRCLF) {
$RCLF = "\r\n";
} else {
$RCLF = "";
}
// надсилання команди, якщо $commande не порожній
if ($commande!=="") {
fputs($connexion, "$commande$RCLF");
// можливе відлуння
if ($verbose) {
affiche($commande, 1);
}
}//якщо
// читання відповіді
$réponse = fgets($connexion, 1000);
// можливе відлуння
if ($verbose) {
affiche($réponse, 2);
}
// отримання коду помилки
$codeErreur = (int) substr($réponse, 0, 3);
// останній рядок відповіді?
while (substr($réponse, 3, 1) === "-") {
// читання відповіді
$réponse = fgets($connexion, 1000);
// можливе відлуння
if ($verbose) {
affiche($réponse, 2);
}
}//while
// відповідь завершено
// помилка, повернута сервером?
if ($codeErreur >= 500) {
return substr($réponse, 4);
}
// повернення без помилки
return "";
}
// --------------------------------------------------------------------------
function affiche($échange, $sens) {
// виводить на екран $échange
// якщо $sens=1, виводить -->$echange
// якщо $sens=2, виводить <-- $échange без останніх 2 символів RCLF
switch ($sens) {
case 1:
print "--> [$échange]\n";
break;
case 2:
$L = strlen($échange);
print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
break;
}//перемикач
}
Коментарі
- рядок 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, текст);
- рядки 125–132: команда QUIT;
- рядок 140: функція [sendCommand] відповідає за надсилання команд клієнта на сервер SMTP. Вона приймає чотири параметри:
- [$connexion]: з’єднання, що пов’язує клієнта з сервером;
- [$commande]: команда, яку потрібно надіслати;
- [$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] описано в URL [https://swiftmailer.symfony.com/docs/introduction.HTML] (травень 2019 р.).
Спочатку запустіть Laragon:

- у [1] відкрийте термінал;

- в [3], переконайтеся, що ви перебуваєте в папці [<laragon>/www], де <laragon> — це папка встановлення Laragon;
- у [3] введіть вказану команду (травень 2019 р.). Перевірте точну команду у URL та [https://swiftmailer.symfony.com/docs/introduction.HTML];
- у [4] вказано, що встановлення чи оновлення не відбулося. Це пов’язано з тим, що бібліотека вже була встановлена на цьому комп’ютері;
- у [5] — папка інсталяції [swiftmailer] [6];
- у [7] — файл, який знадобиться нам у нашому скрипті;
Після цього переконайтеся, що папка [<laragon>/www/vendor] [5] дійсно знаходиться у гілці [Include Path] 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. Використовуйте свої;
- [attachments]: вказує імена файлів, які потрібно додати до листа;
Код скрипта [smtp-02.php] такий:
<?php
// клієнт SMTP (протокол передачі SendMail), що дозволяє надсилати повідомлення
//
// обробка помилок
//ini_set («error_reporting», E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
//
// параметри відправлення пошти
const CONFIG_FILE_NAME = "config-smtp-02.json";
// отримання конфігурації
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// відправлення листів
foreach ($mails as $name => $infos) {
// відстеження
print "Envoi du mail [$name]\n";
// відправлення листа
$résultat = sendmail($name, $infos);
// відображення результату
print "$résultat\n";
}//for
// кінець
exit;
//-----------------------------------------------------------------------
function sendmail($name, $infos) {
// надсилає $infos[message] на SMTP-сервер $infos[smtp-server] через порт $infos[smt-port]
// якщо $infos[tls] має значення «true», буде використано підтримку TLS
// лист надсилається від імені $infos[from]
// для одержувача $infos['to']
// До повідомлення додано документ $info[attachment]
// повідомлення має тему $infos[subject]
//
// повідомлення у форматі HTML
$messageHTML = str_replace("\n", "<br/>", $infos["message"]);
try {
// створення повідомлення
$message = (new \Swift_Message())
// тема повідомлення
->setSubject($infos["subject"])
// відправник
->setFrom($infos["from"])
// адресати з використанням словника (setTo/setCc/setBcc)
->setTo($infos["to"])
// текст повідомлення
->setBody($infos["message"])
// HTML-варіант
->addPart("<b>$messageHTML</b>", 'text/html')
;
// вкладення
foreach ($infos["attachments"] as $attachment) {
// шлях до вкладення
$fileName = __DIR__ . $attachment;
// перевіряємо, чи існує файл
if (file_exists($fileName)) {
// додаємо документ до повідомлення
$message->attach(\Swift_Attachment::fromPath($fileName));
} else {
// помилка
print "L'attachement [$fileName] n'existe pas\n";
}
}
// протокол TLS?
if ($infos["tls"] === "TRUE") {
// TLS
$transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"], 'tls'))
->setUsername($infos["user"])
->setPassword($infos["password"]);
} else {
// немає TLS
$transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"]));
}
// менеджер відправлення
$mailer = new \Swift_Mailer($transport);
// відправлення повідомлення
$result = $mailer->send($message);
// кінець
return "Message [$name] envoyé";
} catch (\Throwable $ex) {
// помилка
return "Erreur lors de l'envoi du message [$name] : " . $ex->getMessage();
}
}
Коментарі
- рядок 10: ми завантажуємо файл [autoload.php], який знаходиться в папці [<lagagon>/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]. Необхідно вказати щонайменше дві інформації: nom та port сервера 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 (Post Office Protocol) та IMAP (Internet Message Access Protocol)
16.6.1. Вступ
Для читання листів, що зберігаються на поштовому сервері, існують два протоколи:
- протокол POP3 (Post Office Protocol) — історично перший протокол, але зараз він майже не використовується;
- протокол IMAP (Internet Message Access Protocol) — новіший за POP3 і найпоширеніший на сьогодні;
Щоб ознайомитися з протоколом POP3, ми використаємо таку архітектуру:

- [Serveur B] буде локальним сервером POP3 / IMAP, реалізованим поштовим сервером [hMailServer];
- [Client A] буде клієнтом POP3 / IMAP у різних формах:
- клієнт [RawTcpClient] для виявлення протоколу POP3;
- скрипт PHP, що відтворює протокол POP3 клієнта [RawTcpClient];
- скрипт PHP, що використовує бібліотеку IMAP з PHP, яка дозволяє реалізувати як клієнти IMAP, так і POP3;
16.6.2. Ознайомлення з протоколом POP3
Спочатку ми використовуємо скрипт [smtp-01.php] для надсилання електронного листа користувачеві [guest@localhost]. Якщо ви провели тестування, пов’язане зі скриптом, цей користувач, як правило, отримав листи, але ми не змогли це перевірити. Щоб надіслати йому новий лист, скористайтеся, наприклад, таким файлом конфігурації [config-smtp-01.json]:
Тепер давайте подивимося за допомогою клієнта [RawTcpClient], як можна переглянути поштову скриньку користувача [guest@localhost]:
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
// клієнт POP3 (Post Office Protocol), що дозволяє читати повідомлення з поштової скриньки
// протокол POP3 клієнт-сервер
// -> клієнт підключається до порту 110 сервера SMTP
// <- сервер надсилає йому привітальне повідомлення
// -> клієнт надсилає команду USER «користувач»
// <- сервер відповідає OK або ні
// -> клієнт надсилає команду PASS mot_de_passe
// <- сервер відповідає OK або ні
// -> клієнт надсилає команду LIST
// <- сервер відповідає OK або ні
// -> клієнт надсилає команду RETR з номером для кожного листа
// <- сервер відповідає OK або ні. Якщо OK — надсилає вміст запитуваного листа
// -> сервер надсилає всі рядки листа і завершує його рядком, що містить
// єдиний символ.
// -> клієнт надсилає команду DELE № для видалення листа
// <- сервер відповідає OK або ні
// // -> клієнт надсилає команду QUIT для завершення діалогу з сервером
// <- сервер відповідає OK або ні
// відповіді сервера мають вигляд +OK текст або -ERR текст
// Відповідь може містити кілька рядків. У такому разі останній рядок складається з однієї крапки
// обмінювані рядки тексту повинні закінчуватися символами RC(#13) та LF(#10)
//
// клієнт POP3 (протокол передачі SendMail), що дозволяє читати електронні листи
//
// обробка помилок
//ini_set («error_reporting», E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
//
// параметри відправлення пошти
const CONFIG_FILE_NAME = "config-pop3-01.json";
// отримання конфігурації
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// читання поштових скриньок
foreach ($mailboxes as $name => $infos) {
// відстеження
print "Lecture de la boîte à lettres [$name]\n";
// читання поштової скриньки
$résultat = readmail($name, $infos, TRUE);
// відображення результату
print "$résultat\n";
}//for
// кінець
exit;
//читання пошти
//-----------------------------------------------------------------------
function readmail(string $name, array $infos, bool $verbose = TRUE): string {
// читає вміст поштової скриньки [$name]
// імпортує всі повідомлення
// кожне повідомлення видаляється після його прочитання
// Якщо $verbose=1, відстежує обмін даними між клієнтом і сервером
//
// відкриття з'єднання з сервером SMTP
$connexion = fsockopen($infos["server"], (int) $infos["port"]);
// повернення у разі помилки
if ($connexion === FALSE) {
return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
}
// $connexion представляє двосторонній потік обміну даними
// між клієнтом (цією програмою) та сервером POP3, до якого встановлено з’єднання
// цей канал використовується для обміну командами та інформацією
// після встановлення з’єднання сервер надсилає привітальне повідомлення, яке ми читаємо
$erreur = sendCommand($connexion, "", $verbose, TRUE);
if ($erreur !== "") {
// завершення з'єднання
fclose($connexion);
// повернення
return $erreur;
}
// команда USER
$erreur = sendCommand($connexion, "USER {$infos["user"]}", $verbose, TRUE);
if ($erreur !== "") {
// завершення з'єднання
fclose($connexion);
// повернення
return $erreur;
}
// команда PASS
$erreur = sendCommand($connexion, "PASS {$infos["password"]}", $verbose, TRUE);
if ($erreur !== "") {
// закриття з'єднання
fclose($connexion);
// повернення
return $erreur;
}
// команда LIST
$premièreLigne = "";
$erreur = sendCommand($connexion, "LIST", $verbose, TRUE, $premièreLigne);
if ($erreur !== "") {
// закриття з'єднання
fclose($connexion);
// повернення
return $erreur;
}
// аналіз першого рядка для визначення кількості повідомлень
$champs = [];
preg_match("/^\+OK (\d+)/", $premièreLigne, $champs);
$nbMessages = (int) $champs[1];
// цикл по повідомленнях
$iMessage = 0;
while ($iMessage < $nbMessages && $iMessage < $infos["maxmails"]) {
// команда RETR
$erreur = sendCommand($connexion, "RETR " . ($iMessage + 1), $verbose, TRUE);
if ($erreur !== "") {
// закриття з'єднання
fclose($connexion);
// повернення
return $erreur;
}
// команда DELE
$erreur = sendCommand($connexion, "DELE " . ($iMessage + 1), $verbose, TRUE);
if ($erreur !== "") {
// закриття з'єднання
fclose($connexion);
// повернення
return $erreur;
}
// наступне повідомлення
$iMessage++;
}
// команда QUIT
$erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
if ($erreur !== "") {
// завершення з'єднання
fclose($connexion);
// повернення
return $erreur;
}
// кінець
fclose($connexion);
return "Terminé";
}
// --------------------------------------------------------------------------
function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF, string &$premièreLigne = ""): string {
// надсилає $commande у канал $connexion
// режим детального виведення, якщо $verbose=1
// якщо $withRCLF=1, додає послідовність RCLF до обміну
// вставляє перший рядок відповіді в [$premièreLigne
// ]
// дані
if ($withRCLF) {
$RCLF = "\r\n";
} else {
$RCLF = "";
}
// надсилає команду, якщо $commande не порожній
if ($commande !== "") {
fputs($connexion, "$commande$RCLF");
// можливе відлуння
if ($verbose) {
affiche($commande, 1);
}
}//if
// читання відповіді
$réponse = fgets($connexion, 1000);
// зберігаємо перший рядок
$premièreLigne = $réponse;
// можливе відлуння
if ($verbose) {
affiche($réponse, 2);
}
// отримання коду помилки
$codeErreur = substr($réponse, 0, 1);
if ($codeErreur === "-") {
// сталася помилка
return substr($réponse, 5);
}
// особливі випадки команд RETR та LIST, які мають багаторядкові відповіді
$commande = substr(strtolower($commande), 0, 4);
if ($commande === "list" || $commande === "retr") {
// останній рядок відповіді?
$champs = [];
$match = preg_match("/^\.\s+$/", $réponse, $champs);
while (!$match) {
// читання відповіді
$réponse = fgets($connexion, 1000);
// можливе відлуння
if ($verbose) {
affiche($réponse, 2);
}
// аналіз відповіді
$champs = [];
$match = preg_match("/^\.\s+$/", $réponse, $champs);
}//while
}
// повернення без помилки
return "";
}
// --------------------------------------------------------------------------
function affiche($échange, $sens) {
// виводить на екран $échange
// якщо $sens=1, виводить -->$echange
// якщо $sens=2, виводить <-- $échange без останніх 2 символів RCLF
switch ($sens) {
case 1:
print "--> [$échange]\n";
break;
case 2:
$L = strlen($échange);
print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
break;
}//перемикач
}
Коментарі
Як ми вже зазначали, [pop3-01.php] є адаптованою версією скрипта [smtp-01.php], який ми вже коментували. Ми зупинимося лише на основних відмінностях:
- рядок 55: функція [readmail] відповідає за зчитування листів із поштової скриньки. Дані для підключення до цієї поштової скриньки містяться у словнику [$infos];
- рядки 61–66: встановлення з’єднання із сервером POP3;
- рядки 71–77: зчитування привітального повідомлення, надісланого сервером;
- рядки 78–85: надсилання команди [USER] для ідентифікації користувача, листи якого потрібно отримати;
- рядки 86–93: надсилання команди [PASS] для введення пароля цього користувача;
- рядки 94–102: надсилається команда [LIST], щоб дізнатися, скільки листів є у поштовій скриньці цього користувача.
- рядок 96: додаємо параметр [$premièreLigne] до параметрів функції [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, до яких слід звертатися. Кожен елемент є структурою [clé:valeur], де:
- [clé] — це сервер, до якого слід звернутися. Тут їх два:
- [{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;
- [valeur]: це словник, що визначає наступні елементи:
- [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), що дозволяє читати електронні листи
//
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// обробка помилок
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
//
// параметри читання пошти
const CONFIG_FILE_NAME = "config-imap-01.json";
// отримання конфігурації
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// читання поштових скриньок
foreach ($mailboxes as $name => $infos) {
// відстеження
print "------------Lecture de la boîte à lettres [$name]\n";
// читання поштової скриньки
readmailbox($name, $infos);
}
// завершення
exit;
//-----------------------------------------------------------------------
function readmailbox(string $name, array $infos): void {
// Спроба підключення
$imapResource = imap_open($name, $infos["user"], $infos["password"]);
// Перевірка повернення функції imap_open()
if (!$imapResource) {
// Помилка
print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
} else {
// З'єднання встановлено
print "Connexion établie avec le serveur [$name].\n";
// загальна кількість повідомлень у поштовій скриньці
$nbmsg = imap_num_msg($imapResource);
print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
// непрочитані повідомлення в поточній поштовій скриньці
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) {
// Отримуємо інформацію про повідомлення № $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);
}
// отримується текст повідомлення № $msgNumber
getMailBody($imapResource, $msgNumber, $infos);
// якщо протокол має вигляд POP3, повідомлення видаляється
$pop3 = $infos["pop3"];
if ($pop3 !== NULL) {
// повідомлення видаляється у два етапи
imap_delete($imapResource, $msgNumber);
imap_expunge($imapResource);
}
}
}
}
}
// закриття з'єднання
$imapClose = imap_close($imapResource);
if (!$imapClose) {
// Помилка
print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
} else {
// успіх
print "Fermeture de la connexion réussie.\n";
}
}
function getMailBody($imapResource, int $msgNumber, array $infos): void {
// отримано тіло повідомлення № $msgNumber
$corpsMail = imap_body($imapResource, $msgNumber);
print "Enregistrement du message dans le fichier {$infos["output-dir"]}/{$infos["prefix"]}$msgNumber\n";
// створено папку, якщо це необхідно
if (!file_exists($infos["output-dir"])) {
mkdir($infos["output-dir"]);
}
// зберігаємо повідомлення
if (!file_put_contents($infos["output-dir"] . "/" . $infos["prefix"] . $msgNumber, $corpsMail)) {
print "Echec de l'enregistrement\n";
}
}
Коментарі
- рядки 19–24: виконується цикл по всіх серверах, знайдених у файлі конфігурації;
- рядок 32: функція [raedmailbox] зчитує поштову скриньку, вказану в [$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], щоб користувач [php7parlexemple] у Gmail та користувач [guest] у [localhost] отримали нові повідомлення. Після цього запустимо скрипт [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 {
// отримуємо структуру повідомлення
$structure=imap_fetchstructure($imapResource, $msgNumber);
// виводимо її
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_fetchstructure] наведено значення різних полів об’єкта, що повертається функцією:

Числові значення поля [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 (Multipurpose Internet Mail Extensions) [multipart];
- рядок 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), закодована у форматі [ENCQUOTEDPRINTABLE] (рядок 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) і закодована у форматі Base 64 (рядок 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 {
// отримуємо структуру повідомлення
$structure = imap_fetchstructure($imapResource, $msgNumber);
if ($structure !== FALSE) {
// отримуємо ці різні частини
getParts($imapResource, $msgNumber, $infos, $infosMail, $structure);
}
}
function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
// розрахунок номера розділу
if (substr($sectionNumber, 0, 2) === "0.") {
$sectionNumber = substr($sectionNumber, 2);
}
print "-----contenu de la partie n° [$sectionNumber]\n";
// тип вмісту
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;
}
// тип кодування
$encodings=["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
print "Transfer-Encoding : ".$encodings[$part->encoding]."\n";
// переходимо до можливих підчастин
if (isset($part->parts)) {
for ($i = 1; $i <= count($part->parts); $i++) {
// нова частина повідомлення
$subpart = $part->parts[$i - 1];
// рекурсивний виклик — запитується тіло частини [$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 {
// обчислення номера розділу
if (substr($sectionNumber, 0, 2) === "0.") {
$sectionNumber = substr($sectionNumber, 2);
}
print "-----contenu de la partie n° [$sectionNumber]\n";
// тип вмісту
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;
}
// тип кодування
$encodings = ["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
print "Transfer-Encoding : " . $encodings[$part->encoding] . "\n";
// чи це повідомлення?
if ($part->type === TYPEMESSAGE) {
// підрозділи цього повідомлення (вкладене повідомлення) не оброблятимуться
// відображається текст вкладеного листа
print imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
} else {
// переходимо до можливих підчастин
if (isset($part->parts)) {
for ($i = 1; $i <= count($part->parts); $i++) {
// нова частина повідомлення
$subpart = $part->parts[$i - 1];
// рекурсивний виклик — запитується тіло частини [$subpart]
getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
}
} else {
// підчастин немає — тоді відображається тіло повідомлення
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 {
// розрахунок номера розділу
if (substr($sectionNumber, 0, 2) === "0.") {
$sectionNumber = substr($sectionNumber, 2);
}
print "-----contenu de la partie n° [$sectionNumber]\n";
// тип вмісту
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;
}
// тип кодування
$encodings = ["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
print "Transfer-Encoding : " . $encodings[$part->encoding] . "\n";
// чи є це повідомлення?
if ($part->type === TYPEMESSAGE) {
// підрозділи цього повідомлення не оброблятимуться
savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
} else {
// переходимо до можливих підрозділів
if (isset($part->parts)) {
for ($i = 1; $i <= count($part->parts); $i++) {
// нова частина повідомлення
$subpart = $part->parts[$i - 1];
// рекурсивний виклик — запитуємо тіло частини [$subpart]
getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
}
} else {
// підрозділів немає — тоді зберігається тіло повідомлення
savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
}
}
}
- рядки 33 і 45: відображення тексту частини [$imapResource, $msgNumber, $sectionNumber] електронного листа тепер замінено на його збереження у файлі;
Функція [savePart] виглядає так:
// збереження частини повідомлення
function savePart($imapResource, int $msgNumber, string $sectionNumber, array $infos, object $infosMail): void {
// папка для збереження
$outputDir = $infos["output-dir"] . "/message-$msgNumber";
// якщо папка не існує, її створюють
if (!file_exists($outputDir)) {
mkdir($outputDir);
}
// структура частини, яку потрібно зберегти
$struct = imap_bodystruct($imapResource, $msgNumber, $sectionNumber);
// тип документа
$type = $struct->type;
// підтип документа
$subtype = "";
if (isset($struct->subtype)) {
$subtype = strtolower($struct->subtype);
}
// аналізується тип частини
switch ($type) {
case TYPETEXT:
// у разі текстового повідомлення: 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:
// інші випадки — розглядаються лише вкладення
if (isset($struct->disposition)) {
$disposition = strtolower($struct->disposition);
if ($disposition === "attachment") {
// маємо справу з вкладенням — його зберігаємо
saveAttachment($imapResource, $msgNumber, $sectionNumber, $outputDir, $struct);
}
} else {
// цю частину не обробляти
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] має такий вигляд:
// збереження тексту [$text] повідомлення
function saveText(string $fileName, int $type, string $text, object $infosMail, object $struct) {
// підготовка тексту до збереження
// $text закодовано — його декодують
switch ($struct->encoding) {
case ENCBASE64:
$text = base64_decode($text);
break;
case ENCQUOTEDPRINTABLE:
$text = quoted_printable_decode($text);
break;
}
// заголовки повідомлення
// від
$from = "From: ";
foreach ($infosMail->from as $expéditeur) {
$from .= $expéditeur->mailbox . "@" . $expéditeur->host . ";";
}
// кому
$to = "To: ";
foreach ($infosMail->to as $destinataire) {
$to .= $destinataire->mailbox . "@" . $destinataire->host . ";";
}
// тема
$subject = "Subject: " . $infosMail->subject;
// створення тексту для збереження
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;
}
// створення файлу
print "sauvegarde d'un message dans [$fileName]\n";
// створення файлу
if (! file_put_contents($fileName, $contents)) {
// помилка під час створення файлу
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]:
// збереження вкладення
function saveAttachment($imapResource, int $msgNumber, string $sectionNumber, string $outputDir, object $struct) {
// аналіз структури вкладення
// здійснюється пошук імені файлу, у який слід зберегти вкладення
// це ім’я міститься у [dparameters] структури
if (isset($struct->dparameters)) {
// витягуються елементи [dparameters]
$dparameters = $struct->dparameters;
$fileName = "";
// проходимо по масиву [dparameters]
foreach ($dparameters as $dparameter) {
// кожен [dparameter] є об’єктом із двома атрибутами [attribute, value]
$attribute = strtolower($dparameter->attribute);
// атрибут [filename] відповідає імені файлу, що створюється
// у цьому випадку ім’я файлу міститься в [$dparameter->value]
if ($attribute === "filename") {
$fileName = $dparameter->value;
break;
}
}
// якщо ім’я файлу не знайдено, перевіряється атрибут [parameters] у структурі
if ($fileName === "" && isset($struct->parameters)) {
// отримуємо [parameters]
$parameters = $struct->parameters;
foreach ($parameters as $parameter) {
// кожен параметр є словником із двома ключами [attribute, value]
$attribute = strtolower($parameter->attribute);
// якщо атрибут має значення [name], то [value] є іменем файлу
if ($attribute === "name") {
$fileName = $parameter->value;
// ім'я файлу може бути закодовано
// наприклад =?utf-8?Q?Курси-Посібники-Серж-Тах=C3=A9-1568x268=2Ep
// кодування визначається за допомогою регулярного виразу
$champs = [];
$match = preg_match("/=\?(.+?)\?/", $fileName, $champs);
// якщо збіг є, то декодуємо ім’я файлу
if ($match) {
$fileName = iconv_mime_decode($fileName, 0, $champs[1]);
}
break;
}
}
}
}
// якщо знайдено ім’я файлу, то зберігаємо вкладення
if ($fileName !== "") {
// збереження вкладення
$fileName = "$outputDir/$fileName";
print "sauvegarde de l'attachement dans [$fileName]\n";
// створення файлу
if ($file = fopen($fileName, "w")) {
// отримуємо закодований текст вкладення
$text = imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
// вкладення закодовано — його декодують
switch ($struct->encoding) {
// Base64
case ENCBASE64:
$text = base64_decode($text);
break;
// quoted printable
case ENCQUOTEDPRINTABLE:
$text = quoted_printable_decode($text);
break;
default:
// інші випадки ігноруються
break;
}
// запис тексту у файл
fputs($file, $text);
// закриття файлу
fclose($file);
} else {
// помилка під час створення файлу
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: приклад закодованої назви файлу. Вона має такий вигляд: =?codage_original?codage_actuel?nom_encodé. Отже, ім’я [=?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], а також вкладення. Тож може скластися така ситуація:
- файл [mail 1], структура якого аналогічна структурі вкладення типу [message/rfc822];
- файл [mail 2], доданий до листа 1;
- файл [mail 3], прикріплений до листа 2;
- тощо…
Скрипт [imap-02.php] зберігає вміст [mail 1] (тексти та вкладення). Він зберігає [mail 2] як вкладений документ, але на цьому зупиняється. Він не намагається проаналізувати [mail 2], щоб витягти з нього тексти та вкладення. Можна було б подумати, що достатньо застосувати до [mail 2] те, що було зроблено для [mail 1]. Рекурсивного виклику методу, який обробляв [mail 1], могло б вистачити, щоб отримати вміст усіх листів, вкладених один в одного. На жаль, частини [mail 2] пронумеровані за логікою, відмінною від тієї, що використовувалася для [mail 1], що унеможливлює застосування одного й того самого алгоритму в обох випадках, якщо тільки не використовувати досить складну логіку для обчислення номерів частин листа, незалежно від його положення в сукупності вкладених листів.
Скрипт [imap-02.php] і без того був складним. Щоб уникнути його подальшого ускладнення для обробки вмісту вкладених листів, ми будемо використовувати бібліотеку [php-mime-mail-parser], доступну на Github (травень 2019 року) за посиланням URL [https://github.com/php-mime-mail-parser/php-mime-mail-parser], написану Вінсентом Досом.
16.6.6.1. Встановлення бібліотеки [php-mime-mail-parser]
На сторінці опису бібліотеки вказано, як встановити її в Windows:

Встановлення OS у Windows складається з двох етапів:
télécharger une DLL ;
modifier le fichier [php.ini] qui configure PHP ;
LA DLL з бібліотеки [mailparse] доступний у URL [http://pecl.php.net/package/mailparse] (травень 2019 року);

- у [2] слід вибрати найновішу та найстабільнішу версію бібліотеки;

- у [3] виберіть версію PHP, яку ви використовуєте (у цьому документі це PHP 7.2);
- у [4] виберіть версію вашої OS для Windows (тут це 64-бітна версія Windows). Ми вибираємо версію [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 або 74 біти).
Отримавши файл DLL, його потрібно скопіювати до папки [<laragon>/bin/php/<version-php>/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, що використовується;
- ви завантажили 32-бітний файл DLL, хоча у вас встановлено 64-бітний PHP, або навпаки;
Після активації та перевірки розширення можна перейти до встановлення бібліотеки [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), що дозволяє читати електронні листи
// написано з використанням бібліотеки [php-mime-mail-parser]
// доступний за адресоюURL [https://github.com/php-mime-mail-parser/php-mime-mail-parser] (травень 2019 р.)
//
// суворе дотримання оголошених типів параметрів функцій
declare (strict_types=1);
// обробка помилок
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// залежності
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
// параметри читання пошти
const CONFIG_FILE_NAME = "config-imap-01.json";
// завантаження конфігурації
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);
// читання поштових скриньок
foreach ($mailboxes as $name => $infos) {
// відстеження
print "------------Lecture de la boîte à lettres [$name]\n";
// читання поштової скриньки
readmailbox($name, $infos);
}
// кінець
exit;
Коментарі
- рядки 18–23: вміст файлу конфігурації поміщається у словник [$mailboxes];
- рядки 26–31: кожна поштова скринька зчитується функцією [readmailbox] (рядок 30). Ця функція фактично зчитує непрочитані повідомлення з поштової скриньки. Поштова скринька відповідає електронній адресі певного користувача;
Функція [readmailbox] має такий вигляд:
function readmailbox(string $name, array $infos): void {
// вхід
$imapResource = imap_open($name, $infos["user"], $infos["password"]);
if (!$imapResource) {
// помилка
print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
exit;
}
// З'єднання встановлено
print "Connexion établie avec le serveur [$name].\n";
// загальна кількість повідомлень у поштовій скриньці
$nbmsg = imap_num_msg($imapResource);
print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
// непрочитані повідомлення в поточній поштовій скриньці
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) {
print "---message n° [$msgNumber]\n";
// завантажується текст повідомлення № $msgNumber
getMailBody($imapResource, $msgNumber, $infos);
// якщо протокол — POP3, повідомлення видаляється після його отримання
$pop3 = $infos["pop3"];
if ($pop3 !== NULL) {
// позначити повідомлення як «для видалення»
imap_delete($imapResource, $msgNumber);
}
}
// завершення перегляду непрочитаних повідомлень
if ($pop3 !== NULL) {
// видаляються повідомлення, позначені як «для видалення»
imap_expunge($imapResource);
}
}
}
// завершення з'єднання
$imapClose = imap_close($imapResource);
if (!$imapClose) {
// помилка
print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
} else {
// успіх
print "Fermeture de la connexion réussie.\n";
}
}
Коментарі
Код функції [readmailbox] такий самий, як і в попередніх скриптах.
Функція [getMailBody] (рядок 25), яка аналізує тіло повідомлення (вміст + вкладення), має такий вигляд:
// аналіз тексту повідомлення
function getMailBody($imapResource, int $msgNumber, array $infos): void {
// отримано повний текст повідомлення
$text = imap_fetchbody($imapResource, $msgNumber, "");
if ($text === FALSE) {
print "Le corps du message [$msgNumber] n'a pu être récupéré";
return;
}
// створено парсер для аналізу тексту повідомлення
$parser = (new PhpMimeMailParser\Parser())->setText($text);
// отримання різних частин повідомлення
$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] має такий вигляд:
// витяг різних частин повідомлення
function getParts(PhpMimeMailParser\Parser $parser, int $msgNumber, string $outputDir): void {
// створюється папка для збереження повідомлення, якщо це необхідно
if (!file_exists($outputDir)) {
if (!mkdir($outputDir)) {
print "Le dossier [$outputDir] n'a pu être créé\n";
return;
}
}
// витягуємо заголовки повідомлення
$arrayHeaders = $parser->getHeaders();
// зберігаються текстові повідомлення
$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");
}
// зберігаються 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");
}
// витягуються вкладення повідомлення
$attachments = $parser->getAttachments();
// номер вкладення
$iAttachment = 0;
// перегляд списку вкладень
foreach ($attachments as $attachment) {
// тип вкладення
$fileType = $attachment->getContentType();
print "-- Sauvegarde d'un attachement de type [$fileType] dans le fichier [$outputDir/{$attachment->getFilename()}]\n";
// зберігання вкладення
try {
$attachment->save($outputDir, PhpMimeMailParser\Parser::ATTACHMENT_DUPLICATE_SUFFIX);
} catch (Exception $e) {
print "L'attachement n'a pu être sauvegardé : " . $e->getMessage() . "\n";
}
// особливий випадок типу message/rfc822
if ($fileType === "message/rfc822") {
// вкладення саме по собі є повідомленням — його також буде проаналізовано
// змінюємо каталог збереження
$iAttachment++;
$outputDir = $outputDir . "/rfc822-$iAttachment";
// змінюємо вміст, що підлягає аналізу
$parser->setText($attachment->getContent());
// повідомлення аналізується рекурсивно
getParts($parser, $msgNumber, $outputDir);
}
}
}
Коментарі
- рядок 2: функція [getParts] приймає три параметри:
- парсер [$parser], якому передано повний текст повідомлення, що аналізується;
- [$msgNumber] — номер повідомлення, що аналізується;
- [$outputDir] — це папка, в якій мають зберігатися вміст та вкладення повідомлення;
- рядки 4–9: створення папки [$outputDir];
- рядок 11: витягуються заголовки повідомлення, що аналізується (from, to, subject…);
- рядок 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] зберігає текстовий вміст повідомлення, що аналізується:
// збереження текстового повідомлення
function saveMessage(string $text, int $type, array $arrayHeaders, string $filename): void {
// вміст, що зберігається
$contents = "";
// додавання заголовків
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";
}
// додавання тексту повідомлення
$contents .= $text;
// збереження всього
if (!file_put_contents($filename, $contents)) {
// помилка
print "Le message n'a pu être sauvegardé dans le fichier [$filename]\n";
} else {
// успіх
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: спочатку будуть збережені всі заголовки повідомлення (from, to, subject…);
- рядки 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] — 5 вкладень з листа, отриманого [guest@localhost];
- у [2] — 5 вкладень з листа [test-localhost-2.eml], отриманого від [1];
- у [3] — 4 вкладення з листа [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] у браузері, отримаємо таке:
