Skip to content

16. Funkcje sieciowe

Przejdziemy teraz do funkcji sieciowych modułu PHP, które umożliwiają nam programowanie protokołów TCP / IP (Transfer Control Protocol / Internet Protocol).

Image

16.1. Podstawy programowania internetowego

16.1.1. Informacje ogólne

Rozważmy komunikację między dwoma zdalnymi komputerami A i B:

Image

Gdy aplikacja AppA na komputerze A chce nawiązać komunikację z aplikacją AppB na komputerze B w Internecie, musi znać kilka informacji:

  • adres IP (protokół internetowy) lub nazwę komputera B;
  • numer portu, na którym działa aplikacja AppB. Komputer B może bowiem obsługiwać wiele aplikacji działających w Internecie. Gdy otrzymuje informacje z sieci, musi wiedzieć, do której aplikacji są one przeznaczone. Aplikacje na komputerze B mają dostęp do sieci poprzez „okienka”, zwane również portami komunikacyjnymi. Informacja ta jest zawarta w pakiecie odebranym przez komputer B, aby mógł on zostać dostarczony do właściwej aplikacji;
  • protokoły komunikacyjne rozumiane przez komputer B. W naszym badaniu będziemy korzystać wyłącznie z protokołów TCP-IP;
  • protokół dialogowy akceptowany przez aplikację AppB. Komputery A i B będą bowiem „komunikować się” ze sobą. Treść tej komunikacji zostanie zakodowana w protokołach TCP-IP. Niemniej jednak, gdy na końcu łańcucha aplikacja AppB odbierze informacje wysłane przez aplikację AppA, musi być w stanie je zinterpretować. Sytuacja ta przypomina przypadek, w którym dwie osoby, A i B, komunikują się przez telefon: ich rozmowa jest przekazywana przez telefon. Mowa zostanie zakodowana w postaci sygnałów przez telefon A, przetransportowana liniami telefonicznymi, dotrze do telefonu B, gdzie zostanie zdekodowana. Osoba B słyszy wówczas słowa. W tym miejscu pojawia się pojęcie protokołu dialogowego: jeśli A mówi po francusku, a B nie rozumie tego języka, A i B nie będą w stanie prowadzić sensownej rozmowy;

Dlatego obie komunikujące się aplikacje muszą uzgodnić, jaki rodzaj dialogu przyjmą. Na przykład dialog z usługą ftp nie przebiega tak samo jak z usługą pop: te dwie usługi nie akceptują tych samych poleceń. Mają one inny protokół dialogowy;

16.1.2. Cechy charakterystyczne protokołu TCP

W niniejszym opracowaniu zajmiemy się wyłącznie komunikacją sieciową wykorzystującą protokół transportowy TCP, którego główne cechy przedstawiono poniżej:

  • proces, który chce wysłać dane, najpierw nawiązuje połączenie z procesem, do którego mają trafić te informacje. Połączenie to odbywa się między portem komputera wysyłającego a portem komputera odbierającego. Pomiędzy tymi dwoma portami powstaje w ten sposób wirtualna ścieżka, która będzie zarezerwowana wyłącznie dla tych dwóch procesów, które nawiązały połączenie;
  • wszystkie pakiety wysyłane przez proces źródłowy przechodzą tą wirtualną ścieżką i docierają w kolejności, w jakiej zostały wysłane;
  • przesyłane informacje mają charakter ciągły. Proces wysyłający przesyła informacje we własnym tempie. Nie muszą one być wysyłane natychmiast: protokół TCP czeka, aż zgromadzi ich wystarczającą ilość, aby je wysłać. Są one przechowywane w strukturze zwanej segmentem TCP. Segment ten, po zapełnieniu, zostanie przekazany do warstwy IP, gdzie zostanie zamknięty w pakiecie IP;
  • każdy segment wysyłany przez protokół TCP jest ponumerowany. Protokół TCP odbiorcy sprawdza, czy segmenty są odbierane w odpowiedniej kolejności. Za każdy poprawnie odebrany segment wysyła potwierdzenie odbioru do nadawcy;
  • gdy nadawca je otrzyma, informuje o tym proces wysyłający. Dzięki temu proces ten może stwierdzić, że segment dotarł do miejsca przeznaczenia;
  • jeśli po upływie pewnego czasu protokół TCP, który wysłał segment, nie otrzyma potwierdzenia odbioru, ponownie wysyła dany segment, gwarantując w ten sposób jakość usługi przekazywania informacji;
  • wirtualny obwód ustanowiony między dwoma komunikującymi się procesami to full-duplex: oznacza to, że informacje mogą przepływać w obu kierunkach. W ten sposób proces docelowy może wysyłać potwierdzenia odbioru, nawet gdy proces źródłowy nadal wysyła informacje. Pozwala to na przykład protokołowi źródłowemu TCP na wysyłanie wielu segmentów bez oczekiwania na potwierdzenie odbioru. Jeśli po upływie pewnego czasu stwierdzi, że nie otrzymał potwierdzenia odbioru określonego segmentu o numerze n, wznowi wysyłanie segmentów od tego punktu;

16.1.3. Relacja klient-serwer

Komunikacja w Internecie ma często charakter asymetryczny: maszyna A nawiązuje połączenie, aby zwrócić się do maszyny B z prośbą o usługę: określa, że chce nawiązać połączenie z usługą SB1 na maszynie B. Ta ostatnia akceptuje lub odrzuca prośbę. Jeśli zgodzi się, urządzenie A może wysyłać swoje żądania do usługi SB1. Żądania te muszą być zgodne z protokołem komunikacyjnym obsługiwanym przez usługę SB1. W ten sposób nawiązywana jest wymiana żądania i odpowiedzi między maszyną A, zwaną maszyną kliencką, a maszyną B, zwaną maszyną serwerową. Jedna z tych dwóch stron zamknie połączenie.

16.1.4. Architektura klienta

Architektura programu sieciowego korzystającego z usług aplikacji serwerowej będzie wyglądać następująco:

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. Architektura serwera

Architektura programu świadczącego usługi będzie wyglądać następująco:

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

Program serwera inaczej traktuje początkowe żądanie połączenia od klienta niż jego kolejne żądania mające na celu uzyskanie usługi. Program sam nie świadczy usługi. Gdyby to robił, w trakcie świadczenia usługi nie mógłby nasłuchiwać żądań połączenia, a klienci nie byliby wówczas obsługiwani. Postępuje więc inaczej: gdy tylko żądanie połączenia zostanie odebrane na porcie nasłuchowym, a następnie zaakceptowane, serwer tworzy zadanie odpowiedzialne za realizację usługi żądanej przez klienta. Usługa ta jest realizowana na innym porcie serwera, zwanym portem usługowym. W ten sposób można obsługiwać wielu klientów jednocześnie.

Zadanie obsługi będzie miało następującą strukturę:

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. Poznaj protokoły komunikacyjne w Internecie

16.2.1. Wprowadzenie

Gdy klient nawiąże połączenie z serwerem, między nimi rozpoczyna się dialog. Charakter tego dialogu określa tzw. protokół komunikacyjny serwera. Do najpopularniejszych protokołów internetowych należą:

  • HTTP: HyperText Transfer Protocol – protokół komunikacji z serwerem WWW (serwer HTTP);
  • SMTP: Simple Mail Transfer Protocol – protokół komunikacji z serwerem wysyłającym wiadomości e-mail (serwer SMTP);
  • POP: Post Office Protocol – protokół komunikacji z serwerem przechowującym wiadomości e-mail (serwer POP). Służy on do pobierania otrzymanych wiadomości e-mail, a nie do ich wysyłania;
  • IMAP: Internet Message Access Protocol – protokół komunikacji z serwerem przechowującym wiadomości e-mail (serwer IMAP). Protokół ten stopniowo zastąpił starszy protokół POP;
  • FTP: File Transfer Protocol – protokół komunikacji z serwerem przechowującym pliki (serwer FTP);

Wszystkie te protokoły charakteryzują się tym, że są protokołami opartymi na wierszach tekstowych: klient i serwer wymieniają między sobą wiersze tekstowe. Jeśli dysponujemy klientem zdolnym do:

  • nawiązać połączenie z serwerem TCP;
  • wyświetlać na konsoli wiersze tekstowe wysyłane przez serwer;
  • wysyłać na serwer wiersze tekstu wpisane przez użytkownika za pomocą klawiatury;

w ten sposób można komunikować się z serwerem TCP wykorzystującym protokół oparty na wierszach tekstowych, o ile znane są zasady działania tego protokołu.

16.2.2. Narzędzia TCP

Image

W kodach związanych z tym dokumentem znajdują się dwa narzędzia komunikacyjne TCP:

  • [RawTcpClient] umożliwia połączenie się z portem P serwera S;
  • [RawTcpServer] umożliwia utworzenie serwera, który oczekuje na klientów na porcie P;

Serwer TCP [RawTcpServer]wywoływa się za pomocą składni [RawTcpServeur port] w celu utworzenia usługi TCP na porcie [port] komputera lokalnego (komputera, na którym pracujesz):

  • serwer może obsługiwać wielu klientów jednocześnie;
  • serwer wykonuje polecenia wpisywane przez użytkownika za pomocą klawiatury. Są to następujące polecenia:
    • list: wyświetla listę klientów aktualnie podłączonych do serwera. Są oni wyświetlani w formacie [id=x-nom=y]. Pole [id] służy do identyfikacji klientów;
    • send x [texte]: wysyła tekst do klienta nr x (id=x). Nawiasy kwadratowe [] nie są wysyłane. Są one niezbędne w poleceniu. Służą do wizualnego oddzielenia tekstu wysyłanego do klienta;
    • close x: zamyka połączenie z klientem nr x;
    • quit: zamyka wszystkie połączenia i zatrzymuje działanie usługi;
  • wiersze wysyłane przez klienta do serwera są wyświetlane na konsoli;
  • cała wymiana danych jest rejestrowana w pliku tekstowym o nazwie [machine-portService.txt], gdzie
    • [machine] to nazwa komputera, na którym uruchomiony jest kod;
    • [port] to port usługi, który odpowiada na żądania klienta;

Klient TCP [RawTcpClient] wywołuje się za pomocą składni [RawTcpClient serveur port] w celu połączenia się z portem [port] serwera [serveur]:

  • wiersze wpisane przez użytkownika na klawiaturze są wysyłane do serwera;
  • wiersze wysyłane przez serwer są wyświetlane na konsoli;
  • cała wymiana danych jest rejestrowana w pliku tekstowym o nazwie [serveur-port.txt];

Spójrzmy na przykład. Otwieramy dwa okna wiersza poleceń systemu Windows i w każdym z nich przechodzimy do folderu z narzędziami. W jednym z okien uruchamiamy serwer [RawTcpServer] na porcie 100:

Image

  • w [1] znajdujemy się w folderze narzędzi;
  • w [2] uruchamiamy serwer TCP na porcie 100;
  • w [3] serwer przechodzi w stan oczekiwania na klienta TCP;
  • w [4] serwer oczekuje na polecenie wpisane przez użytkownika z klawiatury;

W drugim oknie poleceń uruchamia się klienta TCP:

Image

  • w [5] znajdujemy się w folderze narzędzi;
  • w [6] uruchamiamy klienta TCP: nakazujemy mu połączyć się z portem 100 na komputerze lokalnym (tym, na którym pracujesz);
  • w [7] klientowi udało się połączyć z serwerem. Podajemy dane klienta: znajduje się on na komputerze [DESKTOP-528I5CU] (w tym przykładzie jest to komputer lokalny) i używa portu [50405] do komunikacji z serwerem:
  • w [8] klient oczekuje na polecenie wpisane przez użytkownika za pomocą klawiatury;

Wróćmy do okna serwera. Jego zawartość uległa zmianie:

Image

  • na [9], wykryto klienta. Serwer przydzielił mu numer 1. Serwer poprawnie zidentyfikował zdalnego klienta (komputer i port);
  • w [10] serwer ponownie oczekuje na nowego klienta;

Wróćmy do okna klienta i wyślijmy polecenie do serwera:

Image

  • w [11] widoczne jest polecenie wysłane do serwera;

Wróćmy do okna serwera. Jego zawartość uległa zmianie:

Image

  • w [12], w nawiasach, komunikat odebrany przez serwer;

Wyślijmy odpowiedź do klienta:

Image

  • na [13] – odpowiedź wysłana do klienta 1. Wysyłany jest wyłącznie tekst znajdujący się w nawiasach, a nie same nawiasy;

Wróćmy do okna klienta:

Image

  • w [14], odpowiedź odebrana przez klienta. Otrzymany tekst to ten w nawiasach kwadratowych;

Wróćmy do okna serwera, aby zobaczyć inne polecenia:

Image

  • w [15], żądamy listy klientów;
  • w [16] – odpowiedź;
  • w [17] zamykamy połączenie z klientem nr 1;
  • w [18] – potwierdzenie z serwera;
  • w [19] wyłączamy serwer;
  • w [20] – potwierdzenie serwera;

Wróćmy do okna klienta:

Image

  • w [21] klient wykrył zakończenie usługi;

Utworzono dwa pliki dziennika, jeden dla serwera, drugi dla klienta:

Image

  • w [25] – logi serwera: nazwa pliku to nazwa klienta [machine-port];
  • [26] – logi klienta: nazwa pliku to nazwa serwera [machine-port];

Dzienniki serwera są następujące:

<-- [hello from client]
--> [hello from server]

Logi klienta są następujące:

--> [hello from client]
<-- [hello from server]

16.3. Pobranie nazwy lub adresu IP komputera z Internetu

Image

Komputery w Internecie są identyfikowane za pomocą adresu IP (IPv4 lub IPv6), a najczęściej za pomocą nazwy. Jednak ostatecznie wykorzystywany jest wyłącznie adres IP. Dlatego czasami trzeba znać adres IP urządzenia identyfikowanego za pomocą nazwy.

Skrypt [ip-01.php] wygląda następująco:


<?php

// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
//
// obsługa błędów
error_reporting(E_ALL & E_STRICT);
ini_set("display_errors", "on");
//
// stałe
$HOTES = array("istia.univ-angers.fr", "www.univ-angers.fr", "www.ibm.com", "localhost", "", "xx");
// adresy IP i nazwy maszyn z $HOTES
for ($i = 0; $i < count($HOTES); $i++) {
  getIPandName($HOTES[$i]);
}
// koniec
print "Terminé\n";
exit;

//------------------------------------------------
function getIPandName(string $nomMachine): void {
  //$nomMachine: nazwa komputera, którego adres ma zostać uzyskany IP
  //
  // nomMachine-->adres IP
  $ip = gethostbyname($nomMachine);
  print "---------------\n";
  if ($ip !== $nomMachine) {
    print "ip[$nomMachine]=$ip\n";
    // adres 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";
  }
}

Komentarze

  • wiersze 7–8: nakazuje się, aby PHP zgłaszał wszystkie błędy (E_ALL i E_STRICT) oraz aby były one wyświetlane. Tryb ten jest zalecany wyłącznie w trybie programistycznym w celu ulepszenia kodu przy pomocy ostrzeżeń generowanych przez PHP. W trybie produkcyjnym w wierszu 8 należy ustawić wartość „off”. Od wersji PHP 5.4 poziom E_STRICT jest zawarty w E_ALL;
  • wiersz 11: lista maszyn, dla których potrzebna jest nazwa i adres IP;

Funkcje sieciowe z poziomu PHP są wykorzystywane w funkcji getIpandName z wiersza 21.

  • wiersz 25: funkcja gethostbyname($nom) pozwala uzyskać adres IP „ip3.ip2.ip1.ip0” komputera o nazwie $nom. Jeśli komputer o nazwie $nom nie istnieje, funkcja zwraca jako wynik $nom;
  • wiersz 30: funkcja gethostbyaddr($ip) pozwala uzyskać nazwę komputera o adresie $ip w postaci „ip3.ip2.ip1.ip0”. Jeśli komputer o adresie $ip nie istnieje, funkcja zwraca wynik $ip;

Wyniki:


---------------
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. Protokół HTTP (HyperText Transfer Protocol)

16.4.1. Przykład 1

Image

Gdy przeglądarka wyświetla plik URL, pełni ona rolę klienta serwera internetowego, czyli serwera HTTP. To przeglądarka podejmuje inicjatywę i jako pierwsza wysyła do serwera pewną liczbę poleceń. W tym pierwszym przykładzie:

  • serwerem będzie narzędzie [RawTcpServer];
  • klientem będzie przeglądarka;

Najpierw uruchamiamy serwer na porcie 100:

Image

Następnie za pomocą przeglądarki wysyłamy żądanie do serwera URL [localhost:100], co oznacza, że serwer HTTP, do którego kierujemy zapytanie, działa na porcie 100 na komputerze lokalnym:

Image

Wróćmy do okna serwera:

Image

  • w [3] – klient, który się połączył;
  • w [4-7] – ciąg wierszy tekstu, które wysłał:
    • w [4]: ten wiersz ma format [GET URL HTTP/1.1]. Zawiera on żądanie dotyczące URL / oraz prośbę skierowaną do serwera o użycie protokołu HTTP 1.1;
    • w [5]: ten wiersz ma format [Host: serveur:port]. Wielkość liter w poleceniu [Host] nie ma znaczenia. Przypominamy, że klient wysyła zapytanie do lokalnego serwera działającego na porcie 100;
    • polecenie [User-Agent] podaje tożsamość klienta;
    • polecenie [Accept] wskazuje, jakie typy dokumentów są akceptowane przez klienta;
    • polecenie [Accept-Language] określa, w jakim języku mają być dostarczone żądane dokumenty, jeśli są one dostępne w wielu językach;
    • polecenie [Connection] określa preferowany tryb połączenia: [keep-alive] oznacza, że połączenie musi być utrzymane do momentu zakończenia wymiany danych;
    • w przypadku [7]: klient kończy swoje polecenia pustym wierszem;

Zamyka się połączenie poprzez zamknięcie serwera:

Image

16.4.2. Przykład 2

Teraz, gdy znamy polecenia wysyłane przez przeglądarkę w celu uzyskania URL, poprosimy o ten URL za pomocą naszego klienta TCP [RawTcpClient]. Serwer Apache w Laragonie będzie naszym serwerem WWW.

Uruchommy Laragon, a następnie serwer WWW Apache:

Image

Image

Teraz za pomocą przeglądarki wywołajmy adresy URL i [http://localhost:80]. W tym przypadku podajemy jedynie adres serwera [localhost:80], a nie adres dokumentu URL. W tej sytuacji żądany jest adres URL, czyli katalog główny serwera WWW:

Image

  • na [1], czyli żądany URL. Początkowo wpisano [http://localhost:80], a przeglądarka (w tym przypadku Firefox) przekształcił go po prostu na [localhost], ponieważ protokół [http] jest domyślny, gdy nie podano żadnego protokołu, a port [80] jest domyślny, gdy nie określono portu;
  • w [2], strona główna / zapytanego serwera WWW;

Teraz przyjrzyjmy się tekstowi otrzymanemu przez przeglądarkę:

Image

  • Klikamy prawym przyciskiem myszy na otrzymaną stronę i wybieramy opcję [2]. Otrzymujemy następujący kod źródłowy:
<!DOCTYPE HTML>
<HTML>
    <head>
        <title>Laragon</title>

        <link href="<a href="view-source:https://fonts.googleapis.com/css?family=Karla:400">https://fonts.googleapis.com/css?family=Karla:400</a>" 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="<a href="view-source:http://localhost/?q=info">/?q=info</a>">info</a></span><br />
                      Document Root: C:/myprograms/laragon-lite/www<br />

                </div>
                <div class="opt">
                  <div><a title="Getting Started" href="<a href="view-source:https://laragon.org/docs">https://laragon.org/docs</a>">Getting Started</a></div>
                </div>
            </div>

        </div>
    </body>
</HTML>

Teraz wywołajmy URL i [http://localhost:80] za pomocą naszego klienta TCP:

Image

  • w [1] łączymy się z portem 80 serwera localhost. To właśnie tam działa serwer WWW Laragon;

Teraz wpisujemy polecenia, które znaleźliśmy w poprzednim akapicie:

Image

  • w [1] polecenie [GET]. Żądamy katalogu głównego / serwera WWW;
  • po [2], polecenie [Host];
  • są to jedyne dwa niezbędne polecenia. W przypadku pozostałych poleceń serwer WWW przyjmie wartości domyślne;
  • w pliku [3] znajduje się pusty wiersz, który musi zamykać polecenia klienta;
  • poniżej linii 3 znajduje się odpowiedź serwera WWW;
  • od [4] do pustej linii [5] znajdują się nagłówki HTTP odpowiedzi serwera;
  • po wierszu [5] znajduje się żądany dokument HTML, którego numer to [6];

Wpisujemy [quit], aby zakończyć działanie klienta, a następnie ładujemy plik logów [localhost-80.txt]:

--> [GET / HTTP/1.1]
--> [Host: localhost:80]
--> []
<-- [HTTP/1.1 200 OK]
<-- [Date: Thu, 16 May 2019 14:24:39 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]
<-- []
<-- [<!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>]
  • wiersze 11–79: otrzymany dokument HTML. W poprzednim przykładzie Firefox otrzymał ten sam dokument;

Mamy teraz podstawy do zaprogramowania klienta TCP, który żądałby pliku URL.

16.4.3. Przykład 3

Image

Skrypt [http-01.php] jest klientem HTTP skonfigurowanym przez plik jSON [config-http-01.json]. Jego zawartość jest następująca:

{
    "localhost": {
        "port": 80,
        "GET": "/",
        "Host": "localhost:80",
        "User-Agent": "client PHP",
        "Accept": "text/HTML",
        "Accept-Language": "fr",
        "endOfLine":"\r\n"
    }
}
  • wiersz 2: nazwa komputera, na którym znajduje się serwer WWW, z którym należy się połączyć;
  • wiersz 3: port, na którym działa ten serwer WWW;
  • wiersz 4: identyfikator URL pożądanego dokumentu;
  • wiersz 5: komputer docelowy w formacie komputer:port;
  • wiersz 6: identyfikator klienta HTTP: można wpisać dowolną wartość;
  • wiersz 7: typ dokumentu akceptowany przez klienta, w tym przypadku tekst HTML;
  • wiersz 8: żądany język dokumentu;
  • wiersz 9: znak końca wiersza dla poleceń wysyłanych przez klienta: może się on różnić w zależności od tego, czy serwer działa na systemie Unix (\n), czy Windows (\r\n);

Skrypt [http-01.php] wygląda następująco:


<?php

// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
//
// obsługa błędów
// error_reporting(E_ALL & E_STRICT);
// ini_set("display_errors", "on");
//
// stałe
const CONFIG_FILE_NAME = "config-http-01.json";
//
// pobieramy konfigurację
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// pobieranie tekstu HTML z URL z pliku konfiguracyjnego
foreach ($config as $site => $protocole) {
  // odczyt strony głównej serwisu $ite
  $résultat = getURL($site, $protocole);
  // wyświetlenie wyniku
  print "$résultat\n";
}//dla
// koniec
exit;

//-----------------------------------------------------------------------
function getURL(string $site, array $protocole, $suivi = TRUE): string {
  // odczytuje $site["GET"] i zapisuje go w pliku $site.HTML
  // komunikacja klient–serwer odbywa się zgodnie z protokołem $protocole
  //
  // nawiązanie połączenia na porcie $site
  $erreurNumber = 0;
  $erreur = "";
  $connexion = fsockopen($site, $protocole["port"], $erreurNumber, $erreur);
  // powrót w przypadku błędu
  if ($connexion === FALSE) {
    return "Echec de la connexion au site (" . $site . " ," . $protocole["port"] . " : $erreur";
  }
  // $connexion reprezentuje dwukierunkowy strumień komunikacji
  // między klientem (tym programem) a serwerem WWW, z którym nawiązano połączenie
  // kanał ten służy do wymiany poleceń i informacji
  // protokołem komunikacji jest HTTP
  //
  // utworzenie pliku $site.HTML
  $HTML = fopen("output/$site.HTML", "w");
  if ($HTML === FALSE) {
    // zamknięcie połączenia klient–serwer
    fclose($connexion);
    // zwrot błędu
    return "Erreur lors de la création du fichier $site.HTML";
  }
  // klient rozpocznie dialog HTTP z serwerem
  if ($suivi) {
    print "Client : début de la communication avec le serveur [$site] ----------------------------\n";
  }
  // w zależności od serwerów linie klienta muszą kończyć się znakiem \n lub \r\n
  $endOfLine = $protocole["endOfLine"];
  // dla uproszczenia nie sprawdzamy przypadków błędów w komunikacji klient–serwer
  // klient wysyła polecenie GET, aby zażądać $protocole["GET"]
  // składnia GET URL HTTP/1.1
  $commande = "GET " . $protocole["GET"] . " HTTP/1.1$endOfLine";
  // śledzenie?
  if ($suivi) {
    print "--> $commande";
  }
  // wysyłamy polecenie do serwera
  fputs($connexion, $commande);
  // wysyłanie pozostałych nagłówków HTTP
  foreach ($protocole as $verb => $value) {
    if ($verb !== "GET" && $verb != "port"" && $verb !="endOfLine") {
      // tworzenie polecenia
      $commande = "$verb: $value$endOfLine";
      // kontynuacja?
      if ($suivi) {
        print "--> $commande";
      }
      // wysyłamy polecenie do serwera
      fputs($connexion, $commande);
    }
  }
  // nagłówki (headers) protokołu HTTP muszą kończyć się pustym wierszem
  fputs($connexion, $endOfLine);
  //
  // serwer odpowie teraz na kanale $connexion. Wyśle wszystkie
  // swoje dane, a następnie zamknie kanał. Klient odczytuje zatem wszystko, co nadchodzi z $connexion
  // aż do zamknięcia kanału
  //
  // najpierw odczytuje się nagłówki HTTP wysłane przez serwer
  // one również kończą się pustym wierszem
  if ($suivi) {
    print "Réponse du serveur [$site] ----------------------------\n";
  }
  $fini = FALSE;
  while (!$fini && $ligne = fgets($connexion, 1000)) {
    // czy mamy pusty wiersz?
    $champs = [];
    preg_match("/^(.*?)\s+$/", $ligne, $champs);
    if ($champs[1] !== "") {
      if ($suivi) {
        // wyświetlamy nagłówek HTTP
        print "<-- " . $champs[1] . "\n";
      }
    } else {
      // to był pusty wiersz – nagłówki HTTP zostały zakończone
      $fini = TRUE;
    }
  }
  // odczytujemy dokument HTML, który będzie następował po pustym wierszu
  while ($ligne = fgets($connexion, 1000)) {
    // zapisano wiersz w pliku HTML na stronie
    fputs($HTML, $ligne);
  }
  // serwer zamknął połączenie – klient również je zamyka
  fclose($connexion);
  // zamknięcie pliku $HTML
  fclose($HTML);
  // powrót
  return "Fin de la communication avec le site [$site]. Vérifiez le fichier [$site.HTML]";
}

Komentarze do kodu:

  • wiersz 14: plik konfiguracyjny służy do utworzenia słownika:
    • klucze słownika to serwery WWW, do których należy wysłać zapytania;
    • wartości określają protokół HTTP, którego należy przestrzegać;
  • wiersze 16–21: odbywa się pętla po liście serwerów internetowych z konfiguracji;
  • wiersz 26: funkcja getURL($site,$protocole,$suivi) żąda dokumentu ze strony internetowej $site i zapisuje go w pliku tekstowym $site.HTML.Par – domyślnie komunikacja między klientem a serwerem jest rejestrowana w konsoli ($suivi=TRUE);
  • wiersz 33: funkcja fsockopen($site,$port,$errNumber,$erreur) umożliwia nawiązanie połączenia z usługą TCP / IP działającą na porcie $port na komputerze $site. Jeśli połączenie nie powiedzie się, [$errNumber] to numer błędu, a [$erreur] to powiązany komunikat o błędzie. Po nawiązaniu połączenia klient–serwer wiele usług TCP / IP wymienia ze sobą wiersze tekstu. Dotyczy to w tym przypadku protokołu HTTP (HyperText Transfer Protocol). Strumień danych z serwera docierający do klienta można wówczas traktować jako plik tekstowy odczytywany za pomocą [fgets]. To samo dotyczy strumienia danych wysyłanego z klienta do serwera, który można zapisywać za pomocą [fputs];
  • wiersze 44–50: utworzenie pliku [$site.HTML], w którym zostanie zapisany otrzymany dokument HTML;
  • wiersz 60: pierwsze polecenie klienta musi być poleceniem [GET URL HTTP/1.1];
  • wiersz 66: funkcja fputs umożliwia klientowi wysyłanie danych do serwera. W tym przypadku wysłany wiersz tekstu ma następujące znaczenie: „Chcę (GET) stronę [URL] z witryny internetowej, z którą jestem połączony. Korzystam z protokołu HTTP w wersji 1.1”;
  • wiersze 68–79: wysyłane są pozostałe wiersze protokołu HTTP [Host, User-Agent, Accept, Accept-Language]. Ich kolejność nie ma znaczenia;
  • wiersz 81: wysyłamy pusty wiersz do serwera, aby zasygnalizować, że klient zakończył wysyłanie swoich nagłówków HTTP i oczekuje teraz na żądany dokument;
  • wiersze 92–106: serwer najpierw wyśle serię nagłówków HTTP, które zawierają różne informacje o żądanym dokumencie. Nagłówki te kończą się pustym wierszem;
  • wiersz 93: odczytujemy wiersz wysłany przez serwer za pomocą funkcji PHP [fgets];
  • wiersz 96: pobieramy treść wiersza bez spacji (znaków spacji, znaków końca wiersza) z końca wiersza;
  • wiersz 97: sprawdzamy, czy pobrano pusty wiersz oznaczający koniec nagłówków HTTP wysłanych przez serwer;
  • wiersze 98–101: jeśli jesteśmy w trybie [suivi], otrzymany nagłówek HTTP jest wyświetlany na konsoli;
  • wiersze 108–111: wiersze tekstu odpowiedzi serwera można odczytywać po jednym wierszu za pomocą pętli while i zapisywać w pliku tekstowym [output/$site.HTML]. Gdy serwer WWW wyśle całą żądaną stronę, zamyka połączenie z klientem. Po stronie klienta zostanie to wykryte jako koniec pliku;

Wyniki:

Konsola wyświetla następujące logi:


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]

W naszym przykładzie otrzymany plik [output/localhost.HTML] wygląda następująco:


<!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>

Otrzymaliśmy ten sam dokument, co w przeglądarce Firefox.

16.4.4. Przykład 4

W tym przykładzie pokażemy, że napisany przez nas klient HTTP jest niewystarczający. Zmodyfikujmy plik konfiguracyjny [config-http-01.json] w następujący sposób:

{
    "tahe.developpez.com": {
        "port": 443,
        "GET": "/",
        "Host": "sergetahe.com:443",
        "User-Agent": "script PHP 7",
        "Accept": "text/HTML",
        "Accept-Language": "fr",
        "endOfLine":"\n"
    }
}

W tym przypadku zażądamy pliku URL [http://tahe.developpez.com:443/]. Port 443 na serwerze [tahe.developpez.com] jest portem wykorzystywanym dla bezpiecznego protokołu HTTP, zwanego HTTPS. W tym protokole komunikacja między klientem a serwerem rozpoczyna się od wymiany informacji, które zabezpieczają połączenie. Klient musi zatem korzystać z protokołu [HTTPS], a nie z protokołu [HTTP], czego nasz klient nie robi.

Po zastosowaniu tego pliku konfiguracyjnego wyniki wyświetlane w konsoli są następujące:


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]
  • wiersz 8: serwer [tahe.developpez.com] odpowiedział, że żądanie klienta było nieprawidłowe;

Zawartość pliku [output/tahe.developpez.com.HTML] jest wówczas następująca:


<!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>

Serwer wyraźnie wskazuje, że nie zastosowaliśmy właściwego protokołu.

Skorzystajmy teraz z następującego pliku konfiguracyjnego:

{
    "sergetahe.com": {
        "port": 80,
        "GET": "/cours-tutoriels-de-programmation/",
        "Host": "sergetahe.com:80",
        "User-Agent": "script PHP 7",
        "Accept": "text/HTML",
        "Accept-Language": "fr",
        "endOfLine": "\n"
    }
}

Wyniki wyświetlane w konsoli są następujące:


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]
  • wiersz 11 wskazuje, że serwer wysyła dokument fragmentami;

Przekłada się to na obecność liczb w strumieniu wysyłanym do klienta: każda liczba informuje klienta o liczbie znaków w kolejnej części wysyłanej przez serwer. Oto jak to wygląda w pliku [output/sergetahe.com.HTML]:

Image

  • w plikach [1] i [2] – rozmiar fragmentów 1 i 2 dokumentu w systemie szesnastkowym;

Prawidłowy klient HTTP nie powinien pozostawiać tych liczb w ostatecznym dokumencie HTML.

Oto kolejny przykład:

{
    "sergetahe.com": {
        "port": 80,
        "GET": "/cours-tutoriels-de-programmation",
        "Host": "sergetahe.com:80",
        "User-Agent": "script PHP 7",
        "Accept": "text/HTML",
        "Accept-Language": "fr",
        "endOfLine": "\n"
    }
}

Wygląda podobnie do poprzedniego przykładu, ale żądany w wierszu 4 URL nie zawiera znaku / na końcu. Nie są to te same URL. Uruchomienie klienta HTTP daje wówczas następujące wyniki w konsoli:


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/kursy-i-samouczki-programowania/
<-- 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]
  • w wierszu 8 wskazano, że żądany dokument zmienił się z URL. Nowy identyfikator URL podano w wierszu 13. Należy zwrócić uwagę, że tym razem nowy identyfikator URL kończy się znakiem /;

Plik [output/serge.tahe.com.HTML] ma zatem następującą postać:


<!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>

Klient o numerze HTTP powinien być w stanie śledzić przekierowania. W tym przypadku powinien automatycznie ponownie zażądać nowych numerów URL i [http://sergetahe.com/cours-tutoriels-de-programmation/].

16.4.5. Przykład 5

Poprzednie przykłady pokazały nam, że nasz klient HTTP był niewystarczający. Teraz przedstawimy narzędzie o nazwie [curl], które pozwala pobierać dokumenty internetowe, radząc sobie z wymienionymi trudnościami: protokołem HTTPS, dokumentami wysyłanymi fragmentami, przekierowaniami… Narzędzie [curl] zostało zainstalowane wraz z Laragonem:

Image

Otwórzmy terminal Laragon [1]:

Image

W terminalu wpisujemy następujące polecenie:

Image

  • w [1] – typ konsoli;
  • w [2] – bieżący katalog. Ten katalog jest szczególny: to właśnie stąd serwer Apache w Laragon pobiera dokumenty, o które go prosimy. Należy więc unikać zaśmiecania tego katalogu;
  • w [3] – wpisane polecenie;

Możliwe, że polecenie [curl --help] spowoduje błąd. Najbardziej prawdopodobną przyczyną jest to, że nie masz odpowiedniego typu terminala. W takim przypadku otwórz inny terminal za pomocą polecenia [4-6];

Polecenie [curl --help] wyświetla wszystkie opcje konfiguracyjne [curl]. Jest ich kilkadziesiąt. Będziemy korzystać z bardzo niewielkiej ich liczby. Aby zażądać pliku URL, wystarczy wpisać polecenie [curl URL]. Polecenie to wyświetli na konsoli żądany dokument. Jeśli dodatkowo chcemy uzyskać komunikację HTTP między klientem a serwerem, wpiszemy [curl --verbose URL]. Wreszcie, aby zapisać żądany dokument HTML w pliku, wpiszemy [curl --verbose --output fichier URL].

Aby uniknąć zaśmiecania folderu [www] w Laragonie, przejdźmy do innej lokalizacji w systemie plików:

Image

  • w przypadku pliku [1] należy przejść do folderu [c:\temp]. Jeśli ten folder nie istnieje, można go utworzyć lub wybrać inny;
  • w [2] tworzymy folder o nazwie [curl];
  • w katalogu [3] zaznaczamy ten katalog;
  • w [4] wyświetlamy zawartość folderu. Jest on pusty;

Upewnij się, że serwer Apache w Laragonie jest uruchomiony, a następnie w katalogu [curl] wywołaj katalogi URL i [http://localhost/] za pomocą polecenia [curl –verbose –output localhost.HTML http://localhost/]. Otrzymujemy następujące wyniki:


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 do hosta localhost pozostawiono bez zmian                                                   
  • wiersze 8–12: wiersze wysłane przez [curl] do serwera [localhost]. Rozpoznajemy protokół HTTP;
  • wiersze 13–19: wiersze wysłane w odpowiedzi przez serwer;
  • wiersz 13: wskazuje, że żądany dokument został pomyślnie otrzymany;

Plik [localhost.HTML] zawiera żądany dokument. Można to sprawdzić, otwierając plik w edytorze tekstu.

Teraz poprośmy o plik URL [https://tahe.developpez.com:443/]. Aby uzyskać ten plik URL, klient HTTP musi obsługiwać format HTTPS. Tak jest w przypadku klienta [curl].

Wyniki wyświetlone w konsoli są następujące:


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 do hosta tahe.developpez.com pozostawiono bez zmian
  • wiersze 10–40: wymiana danych między klientem a serwerem w celu zabezpieczenia połączenia: połączenie zostanie zaszyfrowane;
  • wiersze 42–45: nagłówki HTTP wysłane przez klienta [curl] do serwera;
  • wiersz 48: żądany dokument został znaleziony;
  • wiersz 53: dokument jest wysyłany fragmentami;

[curl] poprawnie obsługuje zarówno bezpieczny protokół HTTPS, jak i fakt, że dokument jest wysyłany fragmentami. Wysłany dokument znajdzie się tutaj, w pliku [tahe.developpez.com.HTML].

Sprawdźmy teraz URL i [http://sergetahe.com/cours-tutoriels-de-programmation]. Widzieliśmy już, że w przypadku tego URL występowało przekierowanie do URL [http://sergetahe.com/cours-tutoriels-de-programmation/] (z końcowym znakiem /).

Wyniki wyświetlane w konsoli są następujące:


c:\Temp\curl
λ curl --verbose --output sergetahe.com.HTML --location http://sergetahe.com/kursy-i-samouczki-programowania
  % 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/kursy-i-samouczki-programowania/
< 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, aby zachować sergetahe.com w niezmienionej postaci
* Issue another request to this URL: 'http://sergetahe.com/kursy-i-samouczki-programowania/'
* Found bundle for host sergetahe.com: 0x1c88548 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) z hostem 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 do hosta sergetahe.com pozostawiono bez zmian
  • wiersz 2: używamy opcji [--location], aby wskazać, że chcemy podążać za przekierowaniami wysyłanymi przez serwer;
  • wiersz 13: serwer informuje, że żądany dokument zmienił adres na URL;
  • wiersz 18: podaje nowy adres URL żądanego dokumentu;
  • wiersz 27: [curl] wysyła nowe żądanie, tym razem do nowego adresu URL;
  • wiersz 33: wykorzystywany jest nowy identyfikator URL;
  • wiersz 38: serwer odpowiada, że znalazł żądany dokument;
  • wiersz 41: wysyła go fragmentami;

Żądany dokument zostanie znaleziony w pliku [sergetahe.com.HTML].

16.4.6. Przykład 6

Plik PHP posiada rozszerzenie o nazwie [libcurl], które umożliwia wykorzystanie możliwości narzędzia [curl] w programie PHP. Najpierw należy upewnić się, że rozszerzenie to jest włączone w pliku [php.ini] opisanym w akapicie „link”:

Image

Upewnij się, że wiersz 889 powyżej nie jest skomentowany.

Napiszemy skrypt [http-02.php], który będzie korzystał z następującego pliku konfiguracyjnego jSON:

{
    "sergetahe.com": {
        "timeout": 5,
        "url": "http://sergetahe.com"
    },
    "tahe.developpez.com": {
        "timeout": 5,
        "url": "https://tahe.developpez.com"
    },  
    "www.polytech-angers.fr": {
        "timeout": 5,
        "url": "http://www.polytech-angers.fr"
    },  
    "localhost": {
        "timeout": 5,
        "url": "http://localhost"
    }
}

Każdy element słownika [clé, valeur] ma następującą strukturę:

  • clé: nazwa serwera WWW;
  • valeur to słownik zawierający następujące klucze:
    • timeout: maksymalny czas oczekiwania na odpowiedź serwera. Po upływie tego czasu klient rozłączy się;
    • url: URL żądanego dokumentu;

Kod skryptu [http-02.php] jest następujący:


<?php

// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
//
// obsługa błędów
//error_reporting(E_ALL & E_STRICT);
//ini_set("display_errors", "on");
//
// stałe
const CONFIG_FILE_NAME = "config-http-02.json";
//
// pobieranie konfiguracji
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);

// pobieranie tekstu HTML z URL z pliku konfiguracyjnego
foreach ($config as $site => $infos) {
  // odczyt URL ze strony $ite
  $résultat = getUrl($site, $infos["url"], $infos["timeout"]);
  // wyświetlanie wyników
  print "$résultat\n";
}//dla
// koniec
exit;

//-----------------------------------------------------------------------
function getUrl(string $site, string $url, int $timeout, $suivi = TRUE): string {
  // odczytuje plik URL $url i zapisuje go w pliku output/$site.HTML
  //
  // kontynuacja
  print "Client : début de la communication avec le serveur [$site] ----------------------------\n";

  // Inicjalizacja sesji cURL
  $curl = curl_init($url);
  if ($curl === FALSE) {
    // wystąpił błąd
    return "Erreur lors de l'initialisation de la session cURL pour le site [$site]";
  }
  // opcje curl
  $options = [
    // tryb szczegółowy
    CURLOPT_VERBOSE => true,
    // nowe połączenie – brak pamięci podręcznej
    CURLOPT_FRESH_CONNECT => true,
    // limit czasu żądania (w sekundach)
    CURLOPT_TIMEOUT => $timeout,
    CURLOPT_CONNECTTIMEOUT => $timeout,
    // nie sprawdzaj ważności certyfikatów SSL
    CURLOPT_SSL_VERIFYPEER => false,
    // śledź przekierowania
    CURLOPT_FOLLOWLOCATION => true,
    // pobieranie żądanego dokumentu w postaci ciągu znaków
    CURLOPT_RETURNTRANSFER => true
  ];

  // konfiguracja curl
  curl_setopt_array($curl, $options);
  // Wykonanie żądania
  $page_content = curl_exec($curl);
  // Zamknięcie sesji cURL
  curl_close($curl);

  // przetwarzanie wyniku
  if ($page_content !== FALSE) {
    // zapisanie wyniku w $site.HTML
    $result = file_put_contents("output/$site.HTML", $page_content);
    if ($result === FALSE) {
      // zwrot błędu
      return "Erreur lors de la création du fichier [output/$site.HTML]";
    }
    // pomyślny powrót
    return "Fin de la communication avec le serveur [$site]. Vérifiez le fichier [output/$site.HTML]";
  } else {
    // wystąpił błąd komunikacji
    return "Erreur de communication avec le serveur [$site]";
  }
}

Komentarze

  • wiersz 14: wykorzystuje się plik konfiguracyjny do utworzenia słownika [$config];
  • wiersze 17–22: odbywa się pętla nad listą stron znalezionych w konfiguracji;
  • wiersz 19: dla każdej witryny wywoływana jest funkcja [getUrl], która pobiera plikURL $infos[«url»] z limitem czasu $infos[«timeout»];
  • wiersz 34: rozpoczyna się sesja [curl]. [curl_init] nie nawiązuje jeszcze połączenia z serwerem WWW. Zwraca zasób [$curl], który będzie parametrem dla wszystkich kolejnych funkcji [curl];
  • wiersze 35–38: jeśli inicjalizacja sesji [curl] zakończy się niepowodzeniem, funkcja [curl_init] zwraca wartość logiczną FALSE;
  • wiersze 40–54: słownik [$options] skonfiguruje połączenie [curl] z serwerem;
  • wiersz 57: opcje połączenia są przekazywane do zasobu [$curl];
  • wiersz 59: żądane połączenie z URL z określonymi opcjami. Ze względu na opcję [CURLOPT_RETURNTRANSFER => true] funkcja [curl_exec] zwraca jako wynik dokument przesłany przez serwer w postaci ciągu znaków. Funkcja [curl_exec] zwraca wartość logiczną FALSE w przypadku niepowodzenia połączenia;
  • wiersz 64: analizowany jest wynik funkcji [curl_exec];
  • wiersz 66: otrzymana strona jest zapisywana w pliku lokalnym;
  • wiersze 69, 72, 75: zwracamy wynik funkcji [getUrl];

Po uruchomieniu skryptu [http-02.php] otrzymujemy następujące wyniki w konsoli:


* 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/kursy-i-samouczki-programowania
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 do hosta sergetahe.com pozostawiono bez zmian
* Issue another request to this URL: 'http://sergetahe.com/kursy-i-samouczki-programowania'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) z hostem 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/kursy-i-samouczki-programowania/
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< Cache-control: private
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 do hosta sergetahe.com pozostawiono bez zmian
* Issue another request to this URL: 'http://sergetahe.com/kursy-i-samouczki-programowania/'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) z hostem 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/kursy-i-samouczki-programowania/wp-json/>; rel="https://api.w.org/"
< Link: <http://sergetahe.com/kursy-i-samouczki-programowania/>; 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, aby zachować nienaruszony link 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 do serwera tahe.developpez.com pozostawiono bez zmian
* 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, aby zachować nienaruszony adres 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) z hostem 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 do hosta www.polytech-angers.fr pozostawiono bez zmian
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 do hosta localhost pozostawiono bez zmian

Fin de la communication avec le serveur [localhost]. Vérifiez le fichier [output/localhost.HTML]

Komentarze

  • otrzymujemy te same dane wejściowe i wyjściowe, co w przypadku narzędzia [curl];
  • na zielono – logi skryptu;
  • na niebiesko – polecenia wysłane do serwera;
  • na żółto – polecenia otrzymane w odpowiedzi przez klienta;

16.4.7. Wnioski

W niniejszym akapicie zapoznaliśmy się z protokołem HTTP oraz napisaliśmy skrypt [http-02.php] umożliwiający pobranie pliku URL z sieci.

16.5. Protokół SMTP (Simple Mail Transfer Protocol)

16.5.1. Wprowadzenie

Image

W tym rozdziale:

  • [Serveur B] będzie lokalnym serwerem SMTP, który zainstalujemy;
  • [Client A] będzie klientem SMTP w różnych postaciach:
    • klient [RawTcpClient] służący do odkrywania protokołu SMTP;
    • skrypt PHP odtwarzający protokół SMTP klienta [RawTcpClient];
    • skrypt PHP wykorzystujący bibliotekę [SwiftMailServer], umożliwiającą wysyłanie wszelkiego rodzaju wiadomości e-mail;

16.5.2. Utworzenie adresu [gmail]

Aby przeprowadzić nasze testy SMTP, będziemy potrzebować adresu e-mail, na który możemy wysłać wiadomość. W tym celu utworzymy adres w serwisie Gmail:

Image

  • w [5] tworzymy użytkownika [php7parlexemple] (wybierz inną nazwę);
  • w przypadku [6] hasło będzie brzmiało [PHP7parlexemple] (wybierz inną nazwę);
  • w [7] zatwierdzamy te dane;

Image

  • wypełnij pola [9-10], a następnie zatwierdź (11);
  • zaakceptuj warunki korzystania z Google (12-13), a następnie zatwierdź (14);

Image

  • w [15] skrzynka odbiorcza (Inbox) użytkownika [PHP7] (16);
  • w [17] skrzynka odbiorcza tego użytkownika jest pusta;
  • w [18-19] zaloguj się na konto Google użytkownika [php7parlexemple@gmail.com]. Skonfigurujemy zabezpieczenia konta;

Image

  • w [21] zezwól aplikacjom innym niż Google na korzystanie z konta [php7parlexemple]. Jeśli tego nie zrobimy, nasz lokalny serwer pocztowy [hMailServer] nie będzie mógł komunikować się z serwerem Gmaila o identyfikatorze SMTP;

Image

16.5.3. Instalacja serwera SMTP

W ramach naszych testów zainstalujemy serwer pocztowy [hMailServer], który jest jednocześnie serwerem SMTP umożliwiającym wysyłanie wiadomości e-mail, serwer POP3 (Post Office Protocol), umożliwiający odczytywanie wiadomości e-mail przechowywanych na serwerze, oraz serwer IMAP (Internet Message Access Protocol), który również pozwala na odczytywanie wiadomości e-mail przechowywanych na serwerze, ale oferuje szersze możliwości. Umożliwia on w szczególności zarządzanie przechowywaniem wiadomości e-mail na serwerze.

Serwer pocztowy [hMailServer] jest dostępny pod adresem URL [https://www.hmailserver.com/] (maj 2019 r.).

Image

Podczas instalacji zostaną wyświetlone prośby o podanie pewnych informacji:

Image

  • w [1-2] należy wybrać zarówno serwer pocztowy, jak i narzędzia do jego administrowania;
  • podczas instalacji zostanie wyświetlony monit o podanie hasła administratora: należy je zapisać, ponieważ będzie ono potrzebne;

[hMailServer] instaluje się jako usługa systemu Windows uruchamiana automatycznie przy starcie komputera. Zaleca się wybranie opcji ręcznego uruchamiania:

  • w [3] należy wpisać [services] w polu wprowadzania danych na pasku stanu;

Image

  • w przypadku [4-8] należy ustawić usługę w trybie [manuel] (6), a następnie ją uruchomić (7);

Po uruchomieniu serwer [hMailServer] należy skonfigurować. Serwer został zainstalowany wraz z programem administracyjnym [hMailServer Administrator]:

Image

  • w [2], w polu wprowadzania danych na pasku stanu wpisać [hmailserver];
  • w [3] uruchomić administratora;
  • w [4]: zaloguj administratora na serwerze [hMailServer];
  • w [5] wpisz hasło podane podczas instalacji [hMailServer];

Image

Utworzymy konto użytkownika:

  • kliknij prawym przyciskiem myszy na [Accounts] (7), a następnie (8), aby dodać nowego użytkownika;
  • w zakładce [General] (9) definiujemy użytkownika [guest] (10) z hasłem [guest] (11). Będzie on miał adres e-mail [guest@localhost] (10);
  • w [12] użytkownik [guest] jest aktywowany;

Image

Image

  • w [15] konfiguruje się protokół SMTP serwera pocztowego;
  • w [16] konfiguruje się dystrybucję wiadomości e-mail;
  • w pliku [17] konfiguruje się dystrybucję wiadomości e-mail przeznaczonych dla serwera lokalnego (localhost);
  • w pliku [18] określa się nazwę komputera lokalnego (localhost). Skrypt z akapitu „link” pozwala uzyskać tę nazwę;
  • w pliku [19] konfiguruje się serwer przekaźnikowy SMTP: jest to serwer, który zajmie się dystrybucją wiadomości e-mail nieprzeznaczonych dla komputera lokalnego (localhost);
  • w pliku [20] – serwer SMTP serwisu Gmail. Wybieramy Gmaila, ponieważ utworzyliśmy tam konto w akapicie „link”;
  • w [21] – port SMTP serwisu Gmail;
  • W przypadku [22] usługa Gmaila o nazwie SMTP jest usługą zabezpieczoną: aby uzyskać do niej dostęp, konieczne jest posiadanie konta Gmail;
  • w [23], użytkownik [php7parlexemple] utworzony w akapicie „link”;
  • w [24] hasło tego użytkownika: [PHP7parlexemple] utworzonego w akapicie „link”;
  • w [25] podano typ protokołu bezpieczeństwa używanego przez Gmaila;

Image

  • w [27] – port usługi SMTP;
  • w polu [28] należy zaznaczyć, że usługa ta nie wymaga uwierzytelniania;
  • w polu [30] należy wpisać wiadomość powitalną, którą serwer SMTP wyśle do swoich klientów;

16.5.4. Protokół SMTP

Image

Zapoznamy się z protokołem SMTP w następującym środowisku:

  • klientem A będzie klient generyczny TCP o nazwie [RawTcpClient];
  • serwer B będzie serwerem pocztowym o nazwie [hMailServer];
  • klient A poprosi serwer B o dostarczenie wiadomości e-mail do użytkownika [php7parlexemple@gmail.com];
  • sprawdzimy, czy użytkownik ten rzeczywiście otrzymał wysłaną wiadomość;

Uruchamiamy klienta w następujący sposób:

Image

  • jako [1] łączymy się z portem 25 na komputerze lokalnym, gdzie działa usługa SMTP uruchomiona przez [hMailServer]. Argument [--quit bye] oznacza, że użytkownik zakończy działanie programu, wpisując polecenie [bye]. Bez tego argumentu poleceniem kończącym działanie programu jest [quit]. Jednak [quit] jest również poleceniem protokołu SMTP. Musimy zatem uniknąć tej niejednoznaczności;
  • w przypadku [2] klient jest prawidłowo połączony;
  • w [3] klient oczekuje na polecenia wpisane z klawiatury;
  • w [4] serwer wysyła mu wiadomość powitalną;

Image

  • w [5] klient wysyła polecenie [EHLO nom-de-la-machine-client]. Serwer odpowiada mu serią komunikatów o postaci [250-xx] (6). Kod [250] wskazuje na pomyślne wykonanie polecenia wysłanego przez klienta;
  • w [7] klient podaje nadawcę wiadomości, w tym przypadku [guest@localhost]. Użytkownik ten musi istnieć na serwerze pocztowym [hMailServer]. Tak jest w tym przypadku, ponieważ wcześniej utworzyliśmy tego użytkownika;
  • w polu [8] znajduje się odpowiedź serwera;
  • w [9] podano odbiorcę wiadomości, w tym przypadku użytkownika Gmaila [php7parlexemple@gmail.com];
  • w [10] – odpowiedź serwera;
  • w [11] polecenie [DATA] informuje serwer, że klient zamierza wysłać treść wiadomości;
  • w [12] – odpowiedź serwera;
  • w formacie [13-16] klient musi przesłać listę wierszy tekstu zakończoną wierszem zawierającym wyłącznie jedną kropkę. Komunikat może zawierać wiersze [Subject :, From :, To :] (13) służące odpowiednio do określenia tematu komunikatu, nadawcy i odbiorcy;
  • w formacie [14] po powyższych nagłówkach musi następować pusty wiersz;
  • w [15] – tekst wiadomości;
  • w wierszu [16] znajduje się tylko jedna kropka, która oznacza koniec wiadomości;
  • w [17], gdy serwer odbierze wiersz zawierający tylko jedną kropkę, umieszcza wiadomość w kolejce;
  • w [18] klient informuje serwer, że zakończył wysyłanie;
  • w [19] – odpowiedź serwera;
  • w [20] widać, że serwer zamknął połączenie z klientem;

Teraz sprawdźmy, czy użytkownik [php7parlexemple@gmail.com] rzeczywiście otrzymał wiadomość:

Image

  • w [2] widać, że użytkownik [php7parlexemple@gmail.com] rzeczywiście otrzymał wiadomość;

Image

Image

Image

  • w [7] widoczny jest nadawca wiadomości e-mail. Widać, że nie jest to [guest@localhost]. Wynika to z faktu, że wiadomość została dostarczona przez serwer przekaźnikowy zdefiniowany w konfiguracji [hmailServer]. A serwerem przekaźnikowym jest [smtp.gmail.com] powiązany z danymi identyfikacyjnymi użytkownika Gmaila [php7parlexemple@gmail.com]. Każda wiadomość e-mail pochodząca z serwera [hMailServer] będzie wyglądała, jakby pochodziła od użytkownika [php7parlexemple@gmail.com]. Nie jest to zamierzony efekt, ale jeśli nie użyjemy tego serwera przekaźnikowego, usługa Gmaila o identyfikatorze SMTP odrzuca wiadomości wysyłane przez [hMailServer], ponieważ usługa Gmaila o identyfikatorze SMTP wymaga uwierzytelnienia, którego [hMailServer] nie wysyła. Z pewnością istnieje sposób na obejście tego problemu, ale nie udało mi się go znaleźć;
  • w [8] widać, że wiadomość została odebrana z komputera [DESKTOP-528I5CU], na którym znajduje się serwer pocztowy [hMailServer];
  • w [9] widoczny jest nadawca wiadomości. Widać, że nie jest to [guest@localhost];
  • w polu [10] – pierwotny nadawca wiadomości. Tym razem jest to rzeczywiście [guest@localhost];
  • w [11] – temat;
  • w [12] – odbiorca;
  • w [13] – treść wiadomości;

Ostatecznie nasz klient [RawTcpClient] zdołał wysłać wiadomość, mimo że wystąpił problem z nadawcą. Mamy podstawy do stworzenia klienta SMTP napisanego w PHP.

16.5.5. Podstawowy klient SMTP napisany w języku PHP

W PHP odtworzymy to, czego nauczyliśmy się wcześniej na podstawie protokołu SMTP.

Image

Skrypt [smtp-01.php] jest konfigurowany przez następujący plik 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] to tablica, w której każdy element jest słownikiem typu [nom=>infos]. Wartość [infos] jest z kolei słownikiem zawierającym następujące klucze i wartości:

  • [smtp-server]: nazwa serwera SMTP, który ma być używany;
  • [smtp-port]: numer portu usługi SMTP;
  • [from]: nadawca wiadomości;
  • [to]: odbiorca wiadomości;
  • [subject]: temat wiadomości;
  • [message]: wiadomość do wysłania;
  • Pierwszy element korzysta z serwera SMTP [localhost] w celu wysłania wiadomości e-mail do użytkownika [localhost];
  • drugi element wykorzystuje serwer SMTP [localhost] do wysłania wiadomości e-mail do użytkownika z serwera [Gmail];
  • trzeci element korzysta z serwera SMTP [Gmail] w celu wysłania wiadomości e-mail do użytkownika serwera [Gmail];

Kod [smtp-01.php] klienta SMTP wygląda następująco:


<?php

// klient SMTP (protokół transferu SendMail) umożliwiający wysłanie wiadomości
// protokół komunikacyjny klient-serwer SMTP
// -> klient łączy się z serwerem SMTP na porcie 25
// <- serwer wysyła mu wiadomość powitalną
// -> klient wysyła polecenie EHLO z nazwą swojego komputera
// <- serwer odpowiada OK lub nie
// -> klient wysyła polecenie MAIL FROM: <nadawca>
// <- serwer odpowiada OK lub nie
// -> klient wysyła polecenie RCPT TO: <odbiorca>
// <- serwer odpowiada OK lub nie
// -> klient wysyła polecenie DATA
// <- serwer odpowiada OK lub nie
// -> klient wysyła wszystkie wiersze swojej wiadomości i kończy wierszem zawierającym znak
// jedyny znak.
// <- serwer odpowiada OK lub nie
// -> klient wysyła polecenie QUIT
// <- serwer odpowiada OK lub nie
// odpowiedzi serwera mają postać xxx tekst, gdzie xxx to trzycyfrowa liczba. Każda
// liczba xxx >=500 oznacza błąd.
// Odpowiedź może składać się z kilku wierszy, z których wszystkie oprócz ostatniego zaczynają się od xxx
// o postaci xxx (spacja)
// wymieniane wiersze tekstu muszą kończyć się znakami RC(#13) i LF(#10)
//
//  klient SMTP (protokół transferu SendMail) umożliwiający wysłanie wiadomości
//
// obsługa błędów
//ini_set („error_reporting”, E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
//
// parametry wysyłania wiadomości e-mail
const CONFIG_FILE_NAME = "config-smtp-01.json";

// pobieranie konfiguracji
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);

// wysyłanie wiadomości
foreach ($mails as $name => $infos) {
  // śledzenie przesyłki
  print "Envoi du mail [$name]\n";
  // wysłanie przesyłki
  $résultat = sendmail($name, $infos, TRUE);
  // wyświetlenie wyniku
  print "$résultat\n";
}//dla
// koniec
exit;

//sendmail
//-----------------------------------------------------------------------

function sendmail(string $name, array $infos, bool $verbose = TRUE): string {
  // wysyła wiadomość [$name,$infos]. Jeśli $verbose=TRUE    , śledź wymianę danych między klientem a serwerem
  // pobieramy nazwę klienta
  $client = gethostbyaddr(gethostbyname(""));
  // nawiązywanie połączenia z serwerem SMTP
  $connexion = fsockopen($infos["smtp-server"], (int) $infos["smtp-port"]);
  // powrót w przypadku błędu
  if ($connexion === FALSE) {
    return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
  }
  // $connexion przedstawia dwukierunkowy przepływ komunikacji
  // między klientem (tym programem) a serwerem SMTP, z którym nawiązano połączenie
  // kanał ten służy do wymiany poleceń i informacji
  // po nawiązaniu połączenia serwer wysyła wiadomość powitalną, którą odczytujemy
  $erreur = sendCommand($connexion, "", $verbose, TRUE);
  if ($erreur !== "") {
    // zakończenie połączenia
    fclose($connexion);
    // powrót
    return $erreur;
  }
  // polecenie EHLO
  $erreur = sendCommand($connexion, "EHLO $client", $verbose, TRUE);
  if ($erreur !== "") {
    // zamknięcie połączenia
    fclose($connexion);
    // powrót
    return $erreur;
  }
  // polecenie MAIL FROM:
  $erreur = sendCommand($connexion, sprintf("MAIL FROM: <%s>", $infos["from"]), $verbose, TRUE);
  if ($erreur !== "") {
    // zamknięcie połączenia
    fclose($connexion);
    // powrót
    return $erreur;
  }
  // polecenie RCPT TO:
  $erreur = sendCommand($connexion, sprintf("RCPT TO: <%s>", $infos["to"]), $verbose, TRUE);
  if ($erreur !== "") {
    // zamknięcie połączenia
    fclose($connexion);
    // powrót
    return $erreur;
  }
  // polecenie DATA  
  $erreur = sendCommand($connexion, "DATA", $verbose, TRUE);
  if ($erreur !== "") {
    // zamknięcie połączenia
    fclose($connexion);
    // powrót
    return $erreur;
  }
  // przygotowanie wiadomości do wysłania
  // musi zawierać następujące wiersze
  // From: nadawca
  // Do: odbiorca
  // Temat:
  // pusta linia
  // Treść wiadomości
  // .
  $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 !== "") {
    // zamknięcie połączenia
    fclose($connexion);
    // powrót
    return $erreur;
  }
  // polecenie quit
  $erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
  if ($erreur !== "") {
    // zamknięcie połączenia
    fclose($connexion);
    // powrót
    return $erreur;
  }
  // koniec
  fclose($connexion);
  return "Message envoyé";
}

// --------------------------------------------------------------------------

function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF): string {
  // wysyła $commande do kanału $connexion
  // tryb szczegółowy, jeśli $verbose=1
  // jeśli $withRCLF=1, dodaje sekwencję RCLF do wymiany
  // dane
  if ($withRCLF) {
    $RCLF = "\r\n";
  } else {
    $RCLF = "";
  }
  // wysyłanie polecenia, jeśli $commande nie jest puste
  if ($commande!=="") {
    fputs($connexion, "$commande$RCLF");
    // ewentualne echo
    if ($verbose) {
      affiche($commande, 1);
    }
  }//if
  // odczyt odpowiedzi
  $réponse = fgets($connexion, 1000);
  // ewentualne echo
  if ($verbose) {
    affiche($réponse, 2);
  }
  // odzyskanie kodu błędu
  $codeErreur = (int) substr($réponse, 0, 3);
  // ostatni wiersz odpowiedzi?
  while (substr($réponse, 3, 1) === "-") {
    // odczyt odpowiedzi
    $réponse = fgets($connexion, 1000);
    // ewentualne echo
    if ($verbose) {
      affiche($réponse, 2);
    }
  }//while
  // odpowiedź zakończona
  // błąd zwrócony przez serwer?
  if ($codeErreur >= 500) {
    return substr($réponse, 4);
  }
// powrót bez błędu
  return "";
}

// --------------------------------------------------------------------------

function affiche($échange, $sens) {
  // wyświetla na ekranie $échange
  // jeśli $sens=1, wyświetla -->$echange
  // jeśli $sens=2, wyświetla <-- $échange bez dwóch ostatnich znaków RCLF
  switch ($sens) {
    case 1:
      print "--> [$échange]\n";
      break;
    case 2:
      $L = strlen($échange);
      print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
      break;
  }//przełącznik
}

Uwagi

  • wiersz 39: odczytujemy plik konfiguracyjny;
  • wiersz 42: odbywa się pętla po elementach tablicy [mails]. Każdy element jest słownikiem o nazwie [name=>infos], gdzie [name] to dowolna nazwa, a [infos] to słownik zawierający informacje niezbędne do wysłania wiadomości e-mail;
  • wiersz 46: wysłanie wiadomości e-mail zapewnia funkcja [sendmail], która przyjmuje trzy parametry:
    • $name: nazwa nadana tej wiadomości;
    • $infos: słownik zawierający informacje niezbędne do wysłania wiadomości;
    • verbose: wartość logiczna wskazująca, czy komunikacja klient–serwer ma być rejestrowana w konsoli;
  • wiersz 46: funkcja [sendmail] zwraca komunikat o błędzie, który jest pusty, jeśli nie wystąpił żaden błąd;
  • wiersz 56: funkcja [sendmail] wysyła różne polecenia, które musi wysłać klient SMTP:
    • wiersze 77–84: polecenie EHLO;
    • wiersze 85–92: polecenie MAIL FROM: ;
    • wiersze 93–100: zamówienie RCPT TO: ;
    • wiersze 101–108: polecenie DATA;
    • wiersze 117–124: wysłanie wiadomości (Od, Do, Temat, treść);
    • wiersze 125–132: polecenie QUIT;
  • wiersz 140: funkcja [sendCommand] odpowiada za wysyłanie poleceń klienta do serwera SMTP. Przyjmuje cztery parametry:
    • [$connexion]: połączenie łączące klienta z serwerem;
    • [$commande]: polecenie do wysłania;
    • [$verbose]: jeśli TRUE, to komunikacja między klientem a serwerem jest rejestrowana w konsoli;
    • [$withRCLF]: jeśli TRUE, wysyła polecenie zakończone sekwencją \r\n. Jest to wymagane dla wszystkich poleceń protokołu SMTP, ale [sendCommand] służy również do wysyłania komunikatu. W tym przypadku nie dodaje się sekwencji \r\n;
  • wiersze 150–157: polecenie jest wysyłane do serwera;
  • wiersze 158–163: odczyt pierwszego wiersza odpowiedzi. Odpowiedź może składać się z kilku wierszy. Każdy wiersz ma postać XXX-YYY, gdzie XXX jest kodem numerycznym, z wyjątkiem ostatniego wiersza odpowiedzi, który ma postać XXX YYY (brak znaku -);
  • wiersze 167–174: odczyt wszystkich wierszy odpowiedzi;
  • wiersz 177: jeśli kod numeryczny XXX jest większy niż 500, oznacza to, że serwer zwrócił błąd;

Wyniki

Wykonanie skryptu daje następujące wyniki w konsoli:


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.
  • wiersze 1–26: korzystanie z serwerów SMTP i [hMailServer] w celu wysłania wiadomości e-mail na adres [guest@localhost] przebiega pomyślnie;
  • wiersze 27–52: korzystanie z serwerów SMTP i [hMailServer] w celu wysłania wiadomości e-mail do serwera [php7parlexemple@gmail.com] przebiega pomyślnie;
  • wiersze 53–65: korzystanie z serwera SMTP i [Gmail] w celu wysłania wiadomości e-mail do serwera [php7parlexemple@gmail.com] nie przebiega prawidłowo: w wierszu 65 serwer SMTP wysyła kod błędu 530 wraz z komunikatem o błędzie. Komunikat ten wskazuje, że klient SMTP musi najpierw uwierzytelnić się za pośrednictwem bezpiecznego połączenia. Nasz klient tego nie zrobił i w związku z tym jego żądanie zostało odrzucone;

16.5.6. Drugi klient SMTP korzysta z biblioteki [SwiftMailer]

Poprzedni klient ma co najmniej dwie wady:

  • nie potrafi korzystać z bezpiecznego połączenia, jeśli serwer tego wymaga;
  • nie potrafi dołączać załączników do wiadomości;

W naszym nowym skrypcie wykorzystamy bibliotekę [SwiftMailer] [https://swiftmailer.symfony.com/] (maj 2019). Sposób instalacji biblioteki [SwiftMailer] opisano w URL [https://swiftmailer.symfony.com/docs/introduction.HTML] (maj 2019).

Najpierw uruchom Laragon:

Image

  • w [1] otwórz terminal;

Image

  • w [3], sprawdź, czy znajdujesz się w folderze [<laragon>/www], gdzie <laragon> to folder instalacyjny Laragon;
  • w [3] wpisz podane polecenie (maj 2019 r.). Sprawdź w URL i [https://swiftmailer.symfony.com/docs/introduction.HTML] dokładną treść polecenia;
  • w pliku [4] widnieje informacja, że nie przeprowadzono żadnej instalacji ani aktualizacji. Wynika to z faktu, że biblioteka była już zainstalowana na tym komputerze;
  • w [5] znajduje się folder instalacyjny z [swiftmailer] i [6];
  • w pliku [7] znajduje się plik, który będzie nam potrzebny w naszym skrypcie;

Po wykonaniu tych czynności sprawdź, czy folder [<laragon>/www/vendor] [5] rzeczywiście znajduje się w gałęzi [Include Path] programu NetBeans (zob. akapit „link”).

Wreszcie biblioteka [SwiftMailer] wymaga, aby rozszerzenie PHP [mbstring] było aktywne. W tym celu należy sprawdzić plik [php.ini] (patrz akapit „link”):

Image

Skrypt [smtp-02.php] będzie korzystał z następującego pliku konfiguracyjnego jSON [config-smtp-02.json]:

{
    "mail to localhost via localhost": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "test-localhost",
        "message": "ligne 1\nligne 2\nligne 3",
        "tls": "FALSE",
        "attachments": ["/attachments/Hello from SwiftMailer.docx",
            "/attachments/Hello from SwiftMailer.pdf",
            "/attachments/Hello from SwiftMailer.odt",
            "/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
            "/attachments/test-localhost.eml"
        ]
    },
    "mail to gmail via gmail": {
        "smtp-server": "smtp.gmail.com",
        "smtp-port": "587",
        "from": "php7parlexemple@gmail.com",
        "to": "php7parlexemple@gmail.com",
        "subject": "test-gmail-via-gmail",
        "message": "ligne 1\nligne 2\nligne 3",
        "tls": "TRUE",
        "user": "php7parlexemple@gmail.com",
        "password": "PHP7parlexemple",
        "attachments": ["/attachments/Hello from SwiftMailer.docx",
            "/attachments/Hello from SwiftMailer.pdf",
            "/attachments/Hello from SwiftMailer.odt",
            "/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
            "/attachments/test-localhost.eml"
        ]
    },
    "mail to gmail via localhost": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "php7parlexemple@gmail.com",
        "subject": "test-gmail-via-localhost",
        "message": "ligne 1\nligne 2\nligne 3",
        "tls": "FALSE",
        "attachments": ["/attachments/Hello from SwiftMailer.docx",
            "/attachments/Hello from SwiftMailer.pdf",
            "/attachments/Hello from SwiftMailer.odt",
            "/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
            "/attachments/test-localhost.eml"
        ]
    }
}

Znajdują się tu te same pozycje, co w pliku [config-smtp-01.json], z dwoma dodatkowymi pozycjami:

  • [tls]: w pliku TRUE wskazuje, że należy używać bezpiecznego połączenia z serwerem SMTP. W przypadku, gdy wartość [tls] jest równa TRUE, należy dodać dwie pozycje:
    • [user]: nazwa użytkownika uwierzytelniającego połączenie;
    • [password]: jego hasło;

W naszym przykładzie użyliśmy danych logowania użytkownika [php7parlexemple@gmail.com], aby połączyć się z serwerem Gmaila. Proszę użyć własnych danych;

  • [attachments]: określa nazwy plików, które mają zostać załączone do wiadomości e-mail;

Kod skryptu [smtp-02.php] wygląda następująco:


<?php

// klient SMTP (protokół transferu SendMail) umożliwiający wysłanie wiadomości
//
// obsługa błędów
//ini_set („error_reporting”, E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
//
// parametry wysyłania wiadomości
const CONFIG_FILE_NAME = "config-smtp-02.json";

// pobieranie konfiguracji
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);

// wysyłanie wiadomości
foreach ($mails as $name => $infos) {
  // śledzenie
  print "Envoi du mail [$name]\n";
  // wysyłanie wiadomości
  $résultat = sendmail($name, $infos);
  // wyświetlanie wyniku
  print "$résultat\n";
}//dla
// koniec
exit;

//-----------------------------------------------------------------------

function sendmail($name, $infos) {

  // wysyła $infos[message] na serwer SMTP $infos[smtp-server] na porcie $infos[smt-port]
  // jeśli $infos[tls] ma wartość prawdziwą, zostanie użyty format TLS
  // wiadomość e-mail jest wysyłana w imieniu $infos[from]
  // do odbiorcy $infos['to']
  // Do wiadomości dołączono dokument $info[attachment]
  // wiadomość o temacie $infos[subject]
  //
  // wiadomość w formacie HTML
  $messageHTML = str_replace("\n", "<br/>", $infos["message"]);
  try {
    // utworzenie wiadomości
    $message = (new \Swift_Message())
      // temat wiadomości
      ->setSubject($infos["subject"])
      // nadawca
      ->setFrom($infos["from"])
      // odbiorcy z wykorzystaniem słownika (setTo/setCc/setBcc)
      ->setTo($infos["to"])
      // treść wiadomości
      ->setBody($infos["message"])
      // wersja HTML
      ->addPart("<b>$messageHTML</b>", 'text/html')
    ;
    // załączniki
    foreach ($infos["attachments"] as $attachment) {
      // ścieżka do załącznika
      $fileName = __DIR__ . $attachment;
      // sprawdzamy, czy plik istnieje
      if (file_exists($fileName)) {
        // dołączamy dokument do wiadomości
        $message->attach(\Swift_Attachment::fromPath($fileName));
      } else {
        // błąd
        print "L'attachement [$fileName] n'existe pas\n";
      }
    }
    // protokół TLS?
    if ($infos["tls"] === "TRUE") {
      // TLS
      $transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"], 'tls'))
        ->setUsername($infos["user"])
        ->setPassword($infos["password"]);
    } else {
      // brak TLS
      $transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"]));
    }
    // menedżer wysyłania
    $mailer = new \Swift_Mailer($transport);
    // wysłanie wiadomości
    $result = $mailer->send($message);
    // koniec
    return "Message [$name] envoyé";
  } catch (\Throwable $ex) {
    // błąd
    return "Erreur lors de l'envoi du message [$name] : " . $ex->getMessage();
  }
}

Komentarze

  • wiersz 10: ładujemy plik [autoload.php] znajdujący się w folderze [<lagagon>/www/vendor], gdzie <laragon> to folder instalacyjny Laragon. Plik ten umożliwi załadowanie plików definicji klas z [SwiftMailer] już przy pierwszym użyciu tych klas. Dzięki temu nie musimy tworzyć tylu plików [require], ile jest klas i interfejsów z SwiftMailer, z których będziemy korzystać;
  • wiersz 32: nowa funkcja [sendmail], która ma dwa parametry:
    • [$name], służący do rozróżniania poszczególnych komunikatów;
    • [$infos]: informacje niezbędne do wysłania wiadomości do odbiorcy;
  • wiersz 42: będziemy mieli dwie wersje wiadomości: jedną w postaci zwykłego tekstu, a drugą w formacie HTML. W tym miejscu zamieniamy znaki końca linii na kod HTML <br/>;
  • wiersze 45–69: definiujemy wiadomość za pomocą klasy [\SwiftMessage];
  • wiersz 47: metoda [SwiftMessage→setSubject] służy do ustalenia tematu wiadomości;
  • wiersz 49: metoda [SwiftMessage→setFrom] służy do ustalenia nadawcy wiadomości;
  • wiersz 51: metoda [SwiftMessage→setTo] służy do ustalenia odbiorcy wiadomości;
  • wiersz 53: metoda [SwiftMessage→setBody] służy do ustalenia treści wiadomości;
  • wiersz 55: metoda [SwiftMessage→addPart] służy do ustalania różnych wersji wiadomości, w tym przypadku wiadomości w formacie HTML. Gdy wiadomość ma warianty, programy pocztowe wyświetlają wariant preferowany przez użytkownika;
  • wiersze 58–69: metoda [SwiftMessage→addAttachment] (64) umożliwia załączenie pliku do wiadomości;
  • wiersze 70–79: po zdefiniowaniu wiadomości do wysłania należy określić sposób jej wysłania. Sposób transportu wiadomości jest definiowany przez klasę [\Swift_SmtpTransport]. Należy podać co najmniej dwie informacje: nom oraz port serwera SMTP. Jest jeszcze trzecia informacja: czy serwer SMTP wymaga bezpiecznego uwierzytelniania?
  • wiersze 73–75: instancja [\Swift_SmtpTransport] służąca do bezpiecznego połączenia z serwerem SMTP;
  • wiersz 78: instancja [\Swift_SmtpTransport] do niezabezpieczonego połączenia z serwerem SMTP;
  • wiersz 81: to klasa [\SwiftMailer] wysyła komunikaty. Należy jej przekazać wybrany tryb transportu;
  • wiersz 83: komunikat [\SwiftMessage] jest wysyłany za pośrednictwem wybranego transportu [\Swift_SmtpTransport]. Metoda [SwiftMailer→send] zwraca wartość logiczną FALSE, jeśli nie udało się wysłać komunikatu;
  • wiersze 86–89: biblioteka [SwiftMailer] zgłasza wyjątek, gdy tylko coś pójdzie nie tak;

Uwaga: należy zauważyć, że przestrzenią nazw klas biblioteki [SwiftMailer] jest katalog główny \. Wyraźnie zaznaczono klasy [\SwiftMessage, \Swift_SmtpTransport, \SwiftMailer], aby o tym przypomnieć;

Wyniki

Po uruchomieniu skryptu [smtp-02.php] otrzymujemy następujące wyniki w konsoli:

1
2
3
4
5
6
Envoi du mail [mail to localhost via localhost]
Message [mail to localhost via localhost] envoyé
Envoi du mail [mail to gmail via gmail]
Message [mail to gmail via gmail] envoyé
Envoi du mail [mail to gmail via localhost]
Message [mail to gmail via localhost] envoyé

Jeśli zajrzymy na konto Gmail użytkownika [php7parlexemple], zobaczymy następujące informacje:

Image

  • w polu „[1]” – temat;
  • w [2] – temat;
  • w [3] – odbiorca;
  • w [4] – treść wiadomości;
  • w pliku [5-10] – załączniki;

Jeśli poprosimy o wyświetlenie oryginalnej wiadomości, otrzymamy następujący dokument:


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_=_--

  • wiersz 9: temat;
  • wiersz 10: nadawca;
  • wiersz 11: adresat;
  • wiersz 13: wiadomość zawiera kilka części oddzielonych znacznikami [--_=_swift_xx];
  • wiersze 19–24: wiadomość w postaci zwykłego tekstu;
  • wiersze 27–30: treść wiadomości w formacie HTML;
  • wiersze 34–36: załączony plik [Hello from SwiftMailer.docx];
  • wiersze 40–42: załączony plik [Hello from SwiftMailer.pdf];
  • wiersze 46–48: załączony plik [Hello from SwiftMailer.odt];
  • wiersze 58–60: załączony plik [Cours-Tutoriels-Serge-Tahé-1568x268.png];
  • wiersze 58–60: załączony plik [test-localhost.eml];
  • wiersze 62–114: załączony plik [test-localhost.eml] jest sam w sobie wiadomością, której treść wyświetlana jest w wierszach 62–114. Można zauważyć, że ta wiadomość sama w sobie zawiera załączniki;

16.6. Protokoły POP3 (Post Office Protocol) i IMAP (Internet Message Access Protocol)

16.6.1. Wprowadzenie

Aby odczytać wiadomości e-mail przechowywane na serwerze pocztowym, istnieją dwa protokoły:

  • protokół POP3 (Post Office Protocol) – historycznie pierwszy protokół, ale obecnie rzadko stosowany;
  • protokół IMAP (Internet Message Access Protocol) – nowszy od POP3 i obecnie najczęściej stosowany;

Aby zapoznać się z protokołem POP3, wykorzystamy następującą architekturę:

Image

  • [Serveur B] będzie lokalnym serwerem POP3 / IMAP, zaimplementowanym przez serwer pocztowy [hMailServer];
  • [Client A] będzie klientem POP3 / IMAP o różnych postaciach:
    • klient [RawTcpClient] służący do wykrywania protokołu POP3;
    • skrypt PHP odtwarzający protokół POP3 klienta [RawTcpClient];
    • skrypt PHP wykorzystujący bibliotekę IMAP z PHP, która umożliwia implementację zarówno klientów IMAP, jak i POP3;

16.6.2. Omówienie protokołu POP3

Najpierw używamy skryptu [smtp-01.php] do wysłania wiadomości e-mail do użytkownika [guest@localhost]. Jeśli przeprowadzili Państwo testy związane ze skryptem, użytkownik ten powinien normalnie otrzymać wiadomości e-mail, jednak nie udało nam się tego zweryfikować. Aby wysłać do niego nową wiadomość e-mail, proszę użyć na przykład następującego pliku konfiguracyjnego [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"
    }
}

Teraz zobaczmy, jak za pomocą klienta [RawTcpClient] można przeglądać skrzynkę pocztową użytkownika [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…
  • wiersz 1: serwer POP3 zazwyczaj korzysta z portu 110. Tak jest również w tym przypadku;
  • wiersz 5: polecenie [USER] służy do określenia użytkownika, którego skrzynkę pocztową chcemy odczytać;
  • wiersz 7: polecenie [PASS] służy do podania hasła tego użytkownika;
  • wiersz 9: polecenie [LIST] wyświetla listę wiadomości znajdujących się w skrzynce pocztowej użytkownika;
  • wiersz 14: polecenie [RETR] wyświetla wiadomość o podanym numerze;
  • wiersz 29: polecenie [DELE] powoduje usunięcie wiadomości o podanym numerze;
  • wiersz 40: polecenie [QUIT] informuje serwer o zakończeniu operacji;

Odpowiedź serwera może przybrać kilka form:

  • pojedynczy wiersz zaczynający się od [+OK], wskazujący, że poprzednie polecenie klienta zakończyło się powodzeniem;
  • pojedynczy wiersz zaczynający się od [-ERR], wskazujący, że poprzednie polecenie klienta zakończyło się niepowodzeniem;
  • kilka wierszy, w których:
    • pierwszy wiersz zaczyna się od [+OK];
    • ostatni wiersz składa się z pojedynczej kropki;

16.6.3. Prosty skrypt implementujący protokół POP3

Image

Ponieważ protokół POP3 ma taką samą strukturę jak protokół SMTP, skrypt [pop3-01.php] jest adaptacją skryptu [smtp-01.php]. Będzie on posiadał następujący plik konfiguracyjny [config-pop3-01.json]:

1
2
3
4
5
6
7
8
9
{
    "localhost:110": {
        "server": "localhost",
        "port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "maxmails":5
    }
}
  • wiersze 3–4: serwer POP3, do którego kierowane jest zapytanie, to lokalny serwer [hMailServer];
  • wiersze 5–6: chcemy odczytać skrzynkę pocztową użytkownika [guest@localhost];
  • wiersz 7: odczytanych zostanie maksymalnie 5 wiadomości e-mail;

Skrypt [pop3-01.php] wygląda następująco:


<?php

// klient POP3 (Post Office Protocol) umożliwiający odczytywanie wiadomości ze skrzynki pocztowej
// protokół komunikacji    komunikacji POP3 typu klient-serwer
// -> klient łączy się z serwerem SMTP na porcie 110
// <- serwer wysyła mu wiadomość powitalną
// -> klient wysyła polecenie USER użytkownik
// <- serwer odpowiada OK lub nie
// -> klient wysyła polecenie PASS mot_de_passe
// <- serwer odpowiada OK lub nie
// -> klient wysyła polecenie LIST
// <- serwer odpowiada OK lub nie
// -> klient wysyła polecenie RETR z numerem dla każdej wiadomości e-mail
// <- serwer odpowiada OK lub nie. Jeśli OK, wysyła treść żądanej wiadomości e-mail
// -> serwer wysyła wszystkie wiersze wiadomości e-mail i kończy wierszem zawierającym
// jedyny znak.
// -> klient wysyła polecenie DELE z numerem w celu usunięcia wiadomości e-mail
// <- serwer odpowiada OK lub nie
// // -> klient wysyła polecenie QUIT w celu zakończenia dialogu z serwerem
// <- serwer odpowiada OK lub nie
// odpowiedzi serwera mają postać +OK tekst lub -ERR tekst
// Odpowiedź może składać się z kilku wierszy. W takim przypadku ostatni wiersz składa się z pojedynczego kropki
// wymieniane wiersze tekstu muszą kończyć się znakami RC(#13) i LF(#10)
//
// klient POP3 (protokół transferu SendMail) umożliwiający odczytywanie wiadomości e-mail
//
// obsługa błędów
//ini_set („error_reporting”, E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
//
// parametry wysyłania wiadomości e-mail
const CONFIG_FILE_NAME = "config-pop3-01.json";

// pobieranie konfiguracji
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);

// odczyt skrzynek pocztowych
foreach ($mailboxes as $name => $infos) {
  // śledzenie
  print "Lecture de la boîte à lettres [$name]\n";
  // odczyt skrzynki pocztowej
  $résultat = readmail($name, $infos, TRUE);
  // wyświetlanie wyniku
  print "$résultat\n";
}//for
// koniec
exit;

//readmail
//-----------------------------------------------------------------------

function readmail(string $name, array $infos, bool $verbose = TRUE): string {
  // odczytuje zawartość skrzynki pocztowej [$name]
  // importuje wszystkie wiadomości
  // każda wiadomość jest usuwana po przeczytaniu
  // Jeśli $verbose=1, śledzi wymianę danych między klientem a serwerem
  //
  // nawiązanie połączenia z serwerem SMTP
  $connexion = fsockopen($infos["server"], (int) $infos["port"]);
  // powrót w przypadku błędu
  if ($connexion === FALSE) {
    return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
  }
  // $connexion reprezentuje dwukierunkowy przepływ komunikacji
  // między klientem (tym programem) a serwerem POP3, z którym nawiązano połączenie
  // kanał ten służy do wymiany poleceń i informacji
  // po nawiązaniu połączenia serwer wysyła wiadomość powitalną, którą odczytujemy
  $erreur = sendCommand($connexion, "", $verbose, TRUE);
  if ($erreur !== "") {
    // zakończenie połączenia
    fclose($connexion);
    // powrót
    return $erreur;
  }
  // polecenie USER
  $erreur = sendCommand($connexion, "USER {$infos["user"]}", $verbose, TRUE);
  if ($erreur !== "") {
    // zamknięcie połączenia
    fclose($connexion);
    // powrót
    return $erreur;
  }
  // polecenie PASS
  $erreur = sendCommand($connexion, "PASS {$infos["password"]}", $verbose, TRUE);
  if ($erreur !== "") {
    // zamknięcie połączenia
    fclose($connexion);
    // powrót
    return $erreur;
  }
  // polecenie LIST
  $premièreLigne = "";
  $erreur = sendCommand($connexion, "LIST", $verbose, TRUE, $premièreLigne);
  if ($erreur !== "") {
    // zamknięcie połączenia
    fclose($connexion);
    // powrót
    return $erreur;
  }
  // analiza pierwszego wiersza w celu ustalenia liczby wiadomości
  $champs = [];
  preg_match("/^\+OK (\d+)/", $premièreLigne, $champs);
  $nbMessages = (int) $champs[1];
  // przechodzimy przez pętlę nad wiadomościami
  $iMessage = 0;
  while ($iMessage < $nbMessages && $iMessage < $infos["maxmails"]) {
    // polecenie RETR  
    $erreur = sendCommand($connexion, "RETR " . ($iMessage + 1), $verbose, TRUE);
    if ($erreur !== "") {
      // zamknięcie połączenia
      fclose($connexion);
      // powrót
      return $erreur;
    }
    // polecenie DELE
    $erreur = sendCommand($connexion, "DELE " . ($iMessage + 1), $verbose, TRUE);
    if ($erreur !== "") {
      // zamknięcie połączenia
      fclose($connexion);
      // powrót
      return $erreur;
    }
    // następna wiadomość
    $iMessage++;
  }
  // polecenie QUIT
  $erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
  if ($erreur !== "") {
    // zamknięcie połączenia
    fclose($connexion);
    // powrót
    return $erreur;
  }
  // koniec
  fclose($connexion);
  return "Terminé";
}

// --------------------------------------------------------------------------

function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF, string &$premièreLigne = ""): string {
  // wysyła $commande do kanału $connexion
  // tryb szczegółowy, jeśli $verbose=1
  // jeśli $withRCLF=1, dodaje sekwencję RCLF do wymiany
  // umieszcza pierwszy wiersz odpowiedzi w [$premièreLigne
  // ]
  // dane
  if ($withRCLF) {
    $RCLF = "\r\n";
  } else {
    $RCLF = "";
  }
  // wysyła polecenie, jeśli $commande nie jest puste
  if ($commande !== "") {
    fputs($connexion, "$commande$RCLF");
    // ewentualne echo
    if ($verbose) {
      affiche($commande, 1);
    }
  }//if
  // odczyt odpowiedzi
  $réponse = fgets($connexion, 1000);
  // zapisujemy pierwszy wiersz
  $premièreLigne = $réponse;
  // ewentualne echo
  if ($verbose) {
    affiche($réponse, 2);
  }
  // odzyskiwanie kodu błędu
  $codeErreur = substr($réponse, 0, 1);
  if ($codeErreur === "-") {
    // wystąpił błąd
    return substr($réponse, 5);
  }
  // szczególne przypadki poleceń RETR i LIST, które mają odpowiedzi składające się z kilku wierszy
  $commande = substr(strtolower($commande), 0, 4);
  if ($commande === "list" || $commande === "retr") {
    // ostatni wiersz odpowiedzi?
    $champs = [];
    $match = preg_match("/^\.\s+$/", $réponse, $champs);
    while (!$match) {
      // odczyt odpowiedzi
      $réponse = fgets($connexion, 1000);
      // ewentualne echo
      if ($verbose) {
        affiche($réponse, 2);
      }
      // analiza odpowiedzi
      $champs = [];
      $match = preg_match("/^\.\s+$/", $réponse, $champs);
    }//while
  }
  // powrót bez błędu
  return "";
}

// --------------------------------------------------------------------------

function affiche($échange, $sens) {
  // wyświetla na ekranie $échange
  // jeśli $sens=1, wyświetla -->$echange
  // jeśli $sens=2, wyświetla <-- $échange bez dwóch ostatnich znaków RCLF
  switch ($sens) {
    case 1:
      print "--> [$échange]\n";
      break;
    case 2:
      $L = strlen($échange);
      print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
      break;
  }//przełącznik
}

Komentarze

Jak już wspomnieliśmy, [pop3-01.php] jest adaptacją skryptu [smtp-01.php], który już omówiliśmy. Omówimy tylko główne różnice:

  • wiersz 55: funkcja [readmail] odpowiada za odczytywanie wiadomości e-mail ze skrzynki pocztowej. Informacje dotyczące połączenia z tą skrzynką znajdują się w słowniku [$infos];
  • wiersze 61–66: nawiązanie połączenia z serwerem POP3;
  • wiersze 71–77: odczytanie wiadomości powitalnej wysłanej przez serwer;
  • wiersze 78–85: wysyłamy polecenie [USER] w celu zidentyfikowania użytkownika, którego wiadomości e-mail chcemy pobrać;
  • wiersze 86–93: wysyłamy polecenie [PASS] w celu podania hasła tego użytkownika;
  • wiersze 94–102: wysyłamy polecenie [LIST], aby sprawdzić, ile wiadomości e-mail znajduje się w skrzynce pocztowej tego użytkownika;
  • wiersz 96: dodajemy parametr [$premièreLigne] do parametrów funkcji [readmail]. W pierwszym wierszu odpowiedzi na polecenie LIST serwer podaje liczbę wiadomości w skrzynce pocztowej;
  • wiersze 104–106: pobieramy liczbę wiadomości z pierwszego wiersza odpowiedzi;
  • wiersze 109–128: przechodzi się cyklicznie przez każdą z wiadomości. Dla każdej z nich wysyła się dwa polecenia:
    • RETR i: w celu pobrania wiadomości nr i (wiersze 111–117);
    • DELE i: w celu usunięcia wiadomości po jej przeczytaniu (wiersze 118–125);
  • wiersze 129–136: wysyłamy polecenie [QUIT], aby poinformować serwer o zakończeniu operacji;
  • wiersze 178–194: w przypadku poleceń [LIST] i [RETR] odpowiedź serwera składa się z kilku wierszy, przy czym ostatni zawiera tylko jedną kropkę;

Wyniki

Po uruchomieniu otrzymujemy następujące wyniki:


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.

Mamy tu do czynienia z podstawowym klientem POP3, któremu brakuje pewnych funkcji:

  1. możliwości komunikacji z zabezpieczonym serwerem POP3;
  2. możliwość odczytu załączników do wiadomości;

Pierwszą z tych możliwości zaimplementujemy za pomocą funkcji [imap] z modułu PHP.

16.6.4. Klient POP3 / IMAP zaimplementowany przy użyciu funkcji [imap] z biblioteki PHP

Najpierw musimy sprawdzić, czy funkcje [imap] są dostępne w używanej przez nas wersji PHP. Otwieramy plik [php.ini] opisany w akapicie „link” i szukamy wierszy dotyczących [imap]:

Image

Wiersz 895 – sprawdź, czy rozszerzenie [imap] jest włączone.

Skrypt [imap-01.php] wykorzysta następujący plik jSON [config-imap-01.json]:

{

    "{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX": {
        "imap-server": "imap.gmail.com",
        "imap-port": "993",
        "user": "php7parlexemple@gmail.com",
        "password": "PHP7parlexemple",
        "output-dir": "output/gmail-imap",
        "prefix": "message-"
    },
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3",
        "prefix": "message-"
    }
}

Plik [config-imap-01.json] definiuje tablicę serwerów IMAP / POP3, z którymi należy się połączyć. Każdy element jest strukturą [clé:valeur], gdzie:

  • [clé]: to serwer, z którym należy się połączyć. Mamy tu dwa takie serwery:
    • [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]: oznacza serwer [imap.gmail.com], który nasłuchuje na porcie 993. Protokół klient-serwer to IMAP. Parametr /ssl wskazuje, że komunikacja między klientem a serwerem jest zabezpieczona. Parametr /novalidate-cert nakazuje klientowi, aby nie weryfikował certyfikatu bezpieczeństwa, który serwer mu prześle. Wreszcie serwer IMAP zarządza zestawem skrzynek pocztowych dla tego samego użytkownika. Podając INBOX w serwerze URL serwera IMAP, wskazujemy, że interesuje nas skrzynka pocztowa o nazwie INBOX, do której zazwyczaj trafiają nowe wiadomości;
    • [{localhost:110/pop3}INBOX]: oznacza serwer [localhost], który nasłuchuje na porcie 110. Protokół klient/serwer to w tym przypadku POP3;
  • [valeur]: jest to słownik określający następujące elementy:
    • [imap-server]: nazwa serwera IMAP lub POP3;
    • [imap-port]: port serwera IMAP lub POP3;
    • [user]: właściciel, którego skrzynkę pocztową chcemy przeczytać;
    • [password]: jego hasło;
    • [output-dir]: folder, w którym mają być zapisywane wiadomości;
    • [prefix]: nazwy plików, w których będą zapisywane wiadomości, będą miały postać prefixN, gdzie N to numer wiadomości;
    • [pop3]: wartość logiczna ustawiona na TRUE, wskazująca, że używanym protokołem jest POP3. W tym przypadku po przeczytaniu wiadomości zostanie ona usunięta. Tak zazwyczaj działają serwery POP3: przeczytana wiadomość nie jest przechowywana na serwerze;

Skrypt [imap-01.php] wygląda następująco:


<?php

// klient IMAP (Internet Message Access Protocol) umożliwiający odczytywanie wiadomości e-mail
//
// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
// obsługa błędów
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
//
// parametry odczytu wiadomości e-mail
const CONFIG_FILE_NAME = "config-imap-01.json";

// pobieranie konfiguracji
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);

// odczyt skrzynek pocztowych
foreach ($mailboxes as $name => $infos) {
  // monitorowanie
  print "------------Lecture de la boîte à lettres [$name]\n";
  // odczyt skrzynki pocztowej
  readmailbox($name, $infos);
}
// koniec
exit;

//-----------------------------------------------------------------------

function readmailbox(string $name, array $infos): void {
  // Próba połączenia
  $imapResource = imap_open($name, $infos["user"], $infos["password"]);
  // Test zwrotu funkcji imap_open()
  if (!$imapResource) {
    // Niepowodzenie
    print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
  } else {
    // Połączenie nawiązane
    print "Connexion établie avec le serveur [$name].\n";
    // Łączna liczba wiadomości w skrzynce pocztowej
    $nbmsg = imap_num_msg($imapResource);
    print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
    // nieprzeczytane wiadomości w bieżącej skrzynce pocztowej
    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) {
          // pobierane są informacje o wiadomości nr $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);
          }
          // pobierana jest treść wiadomości nr $msgNumber
          getMailBody($imapResource, $msgNumber, $infos);

          // jeśli protokół to POP3, usuwa się wiadomość
          $pop3 = $infos["pop3"];
          if ($pop3 !== NULL) {
            // usuwamy wiadomość w dwóch etapach
            imap_delete($imapResource, $msgNumber);
            imap_expunge($imapResource);
          }
        }
      }
    }
  }
  // zamknięcie połączenia
  $imapClose = imap_close($imapResource);
  if (!$imapClose) {
    // Błąd
    print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
  } else {
    // powodzenie
    print "Fermeture de la connexion réussie.\n";
  }
}

function getMailBody($imapResource, int $msgNumber, array $infos): void {
  // pobieranie treści wiadomości nr $msgNumber
  $corpsMail = imap_body($imapResource, $msgNumber);

  print "Enregistrement du message dans le fichier {$infos["output-dir"]}/{$infos["prefix"]}$msgNumber\n";
  // w razie potrzeby tworzony jest folder
  if (!file_exists($infos["output-dir"])) {
    mkdir($infos["output-dir"]);
  }
  // zapisuje się wiadomość
  if (!file_put_contents($infos["output-dir"] . "/" . $infos["prefix"] . $msgNumber, $corpsMail)) {
    print "Echec de l'enregistrement\n";
  }
}

Komentarze

  • wiersze 19–24: pętla obejmuje wszystkie serwery znalezione w pliku konfiguracyjnym;
  • wiersz 32: funkcja [raedmailbox] odczytuje skrzynkę pocztową wskazaną w [$name];
  • wiersz 32: nawiązanie połączenia IMAP;
    • pierwszym parametrem jest identyfikator skrzynki pocztowej, która ma zostać odczytana;
    • drugim parametrem jest nazwa użytkownika będącego właścicielem tej skrzynki pocztowej;
    • trzecim parametrem jest jego hasło;

Funkcja [imap_open] zapewnia zabezpieczenie połączenia, jeśli adres skrzynki pocztowej zawiera parametr /ssl;

  • wiersz 41: funkcja [imap_num_msg] pozwala uzyskać całkowitą liczbę wiadomości w skrzynce pocztowej;
  • wiersz 46: funkcja [imap_search] umożliwia wyszukiwanie określonych wiadomości. W tym przypadku szukamy wiadomości, które nie zostały jeszcze przeczytane (UNSEEN). Drugi parametr stanowi kryterium wyboru. Istnieje ich około dwudziestu. Funkcja [imap_search] zwraca tablicę numerów wiadomości. Mogą one mieć dwie formy: numer sekwencyjny lub identyfikator wiadomości UID. Domyślnie funkcja [imap_search] zwraca tablicę numerów sekwencyjnych. Jeśli dodamy trzeci parametr [SE_UID], otrzymamy identyfikatory UID wiadomości;
  • wiersz 47: funkcja [imap_search] zwraca wartość logiczną FALSE, jeśli nie znalazła żadnej wiadomości;
  • wiersz 50: przeprowadzamy pętlę na wszystkich nieprzeczytanych wiadomościach;
  • wiersz 52: wiadomość posiada nagłówki, które można uzyskać za pomocą funkcji [imap_headerinfo]. Jej drugi parametr to zazwyczaj numer sekwencyjny wiadomości. Jeśli chcemy ustawić identyfikator wiadomości UID, należy ustawić trzeci parametr na [FT_UID];
  • wiersz 53: funkcja [imap_headerinfo] zwraca wartość logiczną FALSE, jeśli nie udało jej się wykonać zadania. W przeciwnym razie zwraca obiekt złożony, który wyświetla się za pomocą funkcji [print_r], wiersz 57;
  • wiersz 60: po nagłówkach pobieramy teraz treść wiadomości za pomocą funkcji [imap_body]. Funkcja ta zwraca kod błędu NULL, jeśli nie udało jej się wykonać zadania;
  • wiersze 84–87: treść wiadomości zapisujemy w pliku lokalnym;
  • wiersze 63–68: jeśli używanym protokołem był POP3, usuwamy właśnie odczytany komunikat:
    • funkcja [imap_delete] oznacza wiadomość jako „do usunięcia”, ale jej nie usuwa;
    • funkcja [imap_expunge] fizycznie usuwa wszystkie wiadomości, które zostały oznaczone jako „do usunięcia”;
  • wiersz 74: zamyka się połączenie z serwerem IMAP. W tym celu wykorzystuje się funkcję [imap_close];
  • wiersz 86: funkcja [imap_body] pozwala uzyskać treść wiadomości na podstawie jej numeru;

Uruchommy skrypt [smtp-02.json], aby użytkownik [php7parlexemple] z Gmaila oraz użytkownik [guest] z [localhost] otrzymali nowe wiadomości. Następnie uruchommy skrypt [imap-01.php], aby odczytać ich skrzynki pocztowe.

Wyniki wyświetlone w konsoli są następujące:


------------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.

Jeśli zaraz po wyświetleniu tych wyników ponownie uruchomimy skrypt [imap-01.php], wyniki będą następujące:


------------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.
  • wiersz 3: w skrzynce pocztowej Gmaila nadal znajduje się ta sama liczba wiadomości, ale nie ma już nowych, nieprzeczytanych wiadomości (wiersz 5). Świadczy to o tym, że poprzednie uruchomienie skryptu zmieniło status przeczytanych wiadomości z „nieprzeczytanych” na „przeczytane”;
  • wiersz 9: w skrzynce pocztowej użytkownika [guest@localhost] nie ma już żadnych wiadomości. Wynika to z faktu, że podczas poprzedniego uruchomienia przeczytane wiadomości w skrzynce [localhost] zostały następnie usunięte;

Wiadomości zostały zapisane lokalnie:

Image

Jeśli przyjrzymy się na przykład treści wiadomości nr 26 z Gmaila, widzimy następujące dane:



--_=_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_=_--

  • wiersze 11–13: treść wiadomości w postaci zwykłego tekstu;
  • wiersz 19: wiadomość HTML;
  • wiersz 25: załącznik;

Spróbujmy ulepszyć ten skrypt, aby uzyskać w oddzielnych plikach różne typy wiadomości oraz załączniki.

16.6.5. Ulepszony klient POP3 / IMAP

W skrypcie [imap-01.php] wyświetlamy treść wiadomości nr i jako plik tekstowy zawierający zarówno różne typy wiadomości, jak i zakodowaną zawartość poszczególnych załączników. Można uzyskać strukturę wiadomości, aby poznać jej poszczególne części. W skrypcie [imap-02.php] modyfikujemy funkcję [getMailBody] w następujący sposób:


function getMailBody($imapResource, int $msgNumber, array $infos): void {
  // pobieramy strukturę wiadomości
  $structure=imap_fetchstructure($imapResource, $msgNumber);
  // wyświetla się ją
  print_r($structure);
}
  • wiersz 3: żądamy struktury wiadomości;
  • wiersz 5: wyświetlamy ją;

Celem jest poznanie informacji zawartych w strukturze komunikatu, aby sprawdzić, w jaki sposób można uzyskać jego poszczególne części. W naszym przykładzie komunikat jest wysyłany przez skrypt [smtp-02.php] z następującą konfiguracją [config-smtp-02.json]:

{
    "mail to localhost via localhost": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "test-localhost",
        "message": "ligne 1\nligne 2\nligne 3",
        "tls": "FALSE",
        "attachments": [
            "/attachments/Hello from SwiftMailer.docx",
            "/attachments/Hello from SwiftMailer.pdf",
            "/attachments/Hello from SwiftMailer.odt",
            "/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
            "/attachments/test-localhost.eml"
        ]
    }
}

Jest to zatem komunikat z pięcioma załącznikami, który jest wysyłany do [guest@localhost] (wiersze 11–15). Skrypt [imap-02.php] jest uruchamiany z następującą konfiguracją [config-imap-01.json]:

{
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3"
    }
}

Wykorzystywana jest zatem skrzynka pocztowa [guest@localhost] (wiersz 5). Skrypt [imap-02.php] wyświetla następnie strukturę wiadomości wysłanej przez [smtp-02.php]. Struktura ta, wyświetlona na konsoli, wygląda następująco:


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
                        (

                        )

                )

        )

)

Komentarze

  • Dokumentacja funkcji PHP dotycząca funkcji [imap_fetchstructure] zawiera objaśnienia poszczególnych pól obiektu zwracanego przez tę funkcję:

Image

Wartości liczbowe pola [type] mają następujące znaczenie:

Image

Wartości liczbowe pola [encoding] mają następujące znaczenie:

Image

Komunikat zarejestrowany przez [imap-01.php] zaczynał się od następującego tekstu:


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_=_
  • wiersze 15) i 33) wyznaczają granice komunikatu typu [multipart/mixed] (wiersz m);
  • wiersze 18) i 16) wyznaczają pierwszą część komunikatu: komunikat w postaci zwykłego tekstu;
  • wiersze 26) i 32) wyznaczają granice drugiej części wiadomości: wiadomość HTML;

Różne informacje zawarte w powyższej wiadomości odnajdujemy w obiekcie zwracanym przez [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
                                                )

                                        )

                                )

                        )

                )

  • wiersz 3: wiadomość jest typu MIME (Multipurpose Internet Mail Extensions) [multipart];
  • wiersz 4: wiadomość jest zakodowana w 7 bitach;
  • wiersz 5: [ifsubtype]=1 oznacza, że w strukturze znajduje się pole [subtype];
  • wiersz 6: pole [subtype] określa podtyp MIME, w tym przypadku typ [mixed]. W sumie typ MIME dokumentu to [multipart/mixed];
  • wiersz 7: [ifdescription]=0 oznacza, że w strukturze nie ma pola [description];
  • wiersz 8: [ifid]=0 oznacza, że w strukturze nie ma pola [id];
  • wiersz 10: [ifdisposition]=0 oznacza, że w strukturze nie ma pola [disposition];
  • wiersz 11: [ifdparameters]=0 oznacza, że w strukturze nie ma pola [dparameters];
  • wiersz 12: [ifparameters]=1 oznacza, że w strukturze występuje pole [parameters];
  • wiersz 13: pole [parameters] opisuje parametry komunikatu. W tym przypadku występuje tylko jeden;
  • wiersze 15–19: ten obiekt opisuje następujący wiersz wiadomości tekstowej:
boundary="_=_swift_1558872295_5bc8ee2ca8b3723c0b39ca8bbfbebdeb_=_"

Wiersze te służą do wyznaczenia granic komunikatu. W komunikacie pobranym przez [imap-01.php] część komunikatu, która została właśnie opisana, odpowiada wierszowi m). Atrybut [boundary] nie jest taki sam, ponieważ zrzuty ekranu odpowiadają temu samemu komunikatowi, ale wysłanemu w różnych momentach;

  • wiersz 23: tutaj rozpoczyna się struktura poszczególnych części wiadomości;
  • wiersze 25–45: ta pierwsza część ma typ [multipart/alternative]. Odpowiada ona wierszowi p) tekstu wiadomości;
  • wiersz 47: ta pierwsza część zawiera z kolei podczęści;
  • wiersze 47–70: ta pierwsza podczęść ma typ [text/plain] (wiersze 51, 54), jest zakodowana w typie [ENCQUOTEDPRINTABLE] (wiersz 52) i posiada parametr [charset=utf-8] (wiersze 66–67);
  • wiersze 49–72 opisują wiersze s–x komunikatu tekstowego;
  • wiersze 74–99: opisują drugą podczęść części [multipart/alternative];
  • wiersze 74–99: ta druga podczęść ma typ [text/HTML] (wiersze 76, 79), jest zakodowana jako typ [ENCQUOTEDPRINTABLE] (wiersz 77) i posiada parametr o typie [charset=utf-8] (wiersze 89–93);
  • wiersze 74–99 opisują wiersze aa–ad komunikatu tekstowego;

Część [multipart/alternative] została zakończona. Rozpoczyna się część [application/vnd.openxmlformats-officedocument.wordprocessingml.document] opisana następującym tekstem:

1
2
3
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"

Również w tym przypadku informacje te znajdują się w obiekcie zwracanym przez funkcję [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
                                )

                        )

                )

            
  • wiersz 1: jest to druga część wiadomości ogólnej. Przypominamy, że pierwsza część miała typ [multipart/alternative];
  • wiersze 3–6: ta druga część ma typ [application/vnd.openxmlformats-officedocument.wordprocessingml.document] (wiersze 3 i 6) i jest zakodowana w Base 64 (wiersz 4);
  • wiersz 11: ta druga część jest załącznikiem (wiersz 11) i ma dwa parametry: [filename=Hello from SwiftMailer.docx] (wiersze 15–21) oraz [name=Hello from SwiftMailer.docx] (wiersze 26–32). Należy zauważyć, że ten ostatni parametr nie występuje w treści wiadomości. Został on zatem dodany w funkcji [imap_fetchstructure];

Wiersze 1–36 są powtarzane dla każdego z pięciu załączników wiadomości.

Funkcja [imap_fetch_structure] pozwala nam zatem uzyskać strukturę wiadomości. Struktura ta definiuje części, które same w sobie mogą zawierać podczęści. Aby uzyskać tekst części lub podczęści, używamy funkcji [imap_fetchbody].

Modyfikujemy funkcję [getMailBody], która pozwala nam uzyskać treść wiadomości, w następujący sposób:


function getMailBody($imapResource, int $msgNumber, array $infos, object $infosMail): void {
  // pobieramy strukturę wiadomości
  $structure = imap_fetchstructure($imapResource, $msgNumber);
  if ($structure !== FALSE) {
    // pobieramy te różne części
    getParts($imapResource, $msgNumber, $infos, $infosMail, $structure);
  }
}

function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
  // obliczanie numeru sekcji
  if (substr($sectionNumber, 0, 2) === "0.") {
    $sectionNumber = substr($sectionNumber, 2);
  }
  print "-----contenu de la partie n° [$sectionNumber]\n";
  // typ zawartości
  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;
  }
  // typ kodowania
  $encodings=["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
  print "Transfer-Encoding : ".$encodings[$part->encoding]."\n";
   
  // przechodzimy do ewentualnych podczęści
  if (isset($part->parts)) {
    for ($i = 1; $i <= count($part->parts); $i++) {
      // nowa część wiadomości
      $subpart = $part->parts[$i - 1];
      // wywołanie rekurencyjne – żądamy treści części [$subpart]
      getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
    }
  }
}

Komentarze

  • wiersz 3: pobieramy strukturę wiadomości;
  • wiersz 6: żądamy wyświetlenia poszczególnych części, które znajdują się w tablicy [parts] struktury;
  • wiersz 10: funkcja [getParts] otrzymuje następujące parametry:
    • [$imapResource]: połączenie z serwerem IMAP;
    • [$msgNumber]: numer sekwencyjny komunikatu, z którego chcemy uzyskać części;
    • [$infos]: informacje określające miejsce przechowywania znalezionych części w lokalnym systemie plików;
    • [$infosMail]: ogólne informacje o wiadomości e-mail (nadawca, odbiorca(-y), temat…;
    • [$part]: obiekt reprezentujący część wiadomości;
    • [$sectionNumber]: numer sekcji (lub części) wiadomości;
  • wiersze 17–34: wyświetlany jest typ zawartości części nr [$section] wiadomości. W tym celu wykorzystuje się pola [$part→type] i [$part→subtype] z części [$part];
  • wiersze 36–37: wyświetlany jest typ kodowania części [$sectionNumber];
  • wiersze 40–47: być może część, której informacje właśnie wyświetliliśmy, sama zawiera podczęści;
  • wiersze 41–46: jeśli tak jest, żądamy wyświetlenia typu zawartości poszczególnych podczęści części, którą właśnie wyświetliliśmy. Wykonujemy tutaj rekurencywne wywołanie funkcji [getParts];

Ponownie wysyłamy wiadomość e-mail do użytkownika Gmaila [php7parlexemple@gmail.com] ze skryptem [smtp-02.php] i odczytujemy ją za pomocą poprzedniego skryptu [imap-02.php]. Daje to następujące wyniki w konsoli:


------------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.

Udało nam się pobrać różne rodzaje treści wiadomości, a także ich typy kodowania. Numeracja części przebiega zgodnie z następującą zasadą:

  • wiersze 6–7: część [multipart/mixed], która stanowi całość wiadomości, ma numer 0. Poszczególne części tego obiektu będą miały numery 1, 2…

Komunikat składa się łącznie z pięciu części:

  • wiersze 9–10: część [multipart/alternative], oznaczona numerem 1;
  • wiersze 17–18: część [APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT] o numerze 2. Jest to załącznik w postaci pliku Word;
  • wiersze 20–21: część [APPLICATION/PDF] oznaczona numerem 3. Jest to załącznik w postaci pliku PDF;
  • wiersze 23–24: część [APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT] oznaczona numerem 4. Jest to załącznik pliku OpenOffice;
  • wiersze 26–27: część [UNKNOWN/PNG] oznaczona numerem 5. Jest to załącznik w postaci pliku graficznego;
  • wiersze 30–31: część [MESSAGE/RFC822] oznaczona numerem 6. Jest to załącznik do wiadomości e-mail;

Gdy część zawiera podczęści, są one numerowane jako x.1, x.2…, gdzie x oznacza numer części nadrzędnej. A zatem:

  • wiersze 11–12: pierwsza część części [multipart/alternative] ma numer 1.1. Jest to treść typu [text/plain]: treść wiadomości e-mail;
  • wiersze 14–15: druga część elementu [multipart/alternative] ma numer 1.2. Jest to zawartość typu [text/HTML]: treść wiadomości e-mail w formacie HTML;
  • wiersze 32–33: pierwsza część załącznika [MESSAGE/RFC822] ma numer 6.1. Jest to treść typu [text/plain]. W rzeczywistości, zgodnie ze standardem MIME, numeracja części załącznika wiadomości e-mail [MESSAGE/RFC822] różni się od opisanej wcześniej zasady. W związku z tym pierwsza część załącznika [MESSAGE/RFC822] nie ma numeru 6.1, lecz inny numer;

Teraz, gdy wiemy już, jak zidentyfikować poszczególne części i podczęści wiadomości e-mail, pozostaje nam jeszcze pobrać ich zawartość.

Kod skryptu zmienia się w następujący sposób:


function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
  // obliczanie numeru sekcji
  if (substr($sectionNumber, 0, 2) === "0.") {
    $sectionNumber = substr($sectionNumber, 2);
  }
  print "-----contenu de la partie n° [$sectionNumber]\n";
  // typ zawartości
  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;
  }
  // typ kodowania
  $encodings = ["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
  print "Transfer-Encoding : " . $encodings[$part->encoding] . "\n";

  // czy to jest wiadomość?
  if ($part->type === TYPEMESSAGE) {
    // nie będziemy obsługiwać części składowych tej wiadomości (załącznik e-mailowy)
    // wyświetlamy treść załączonego e-maila
    print imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
  } else {
    // przechodzimy do ewentualnych części składowych
    if (isset($part->parts)) {
      for ($i = 1; $i <= count($part->parts); $i++) {
        // nowa część wiadomości
        $subpart = $part->parts[$i - 1];
        // wywołanie rekurencyjne – żądana jest treść części [$subpart]
        getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
      }
    } else {
      // nie ma podczęści – wyświetlana jest zatem treść wiadomości
      print imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
    }
  }
}

Komentarze

  • wiersz 46: funkcja [imap_fetchbody] pobiera treść części o numerze [$sectionNumber] wiadomości. Numeracja części wiadomości przebiega zgodnie z zasadą wyjaśnioną powyżej;
  • wiersz 1: zaczynamy od sekcji „0”;
  • wiersz 41: podczęści tej sekcji zostaną zatem ponumerowane jako „0.1”, „0.2”, podczas gdy powinny być ponumerowane jako „1”, „2”…
  • wiersze 3–5: korygujemy tę nieprawidłowość;
  • wiersze 37–43: jeśli bieżąca część ma podczęści, to przechodzi się cyklicznie przez każdą z nich (wiersze 38–43). Ich numer sekcji to [$sectionNumber.$i];
  • wiersze 44–47: gdy nie ma już podczęści, wyświetla się treść bieżącej części za pomocą funkcji [imap_fetchbody]. W naszym przykładzie są to części [text/plain], [text/HTML] oraz załączniki;

Wykonanie tego skryptu daje następujące wyniki:


------------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.

Komentarze

  • wiersze 14–16: treść wiadomości tekstowej zakodowana w formacie [quoted-printable] (wiersz 13);
  • wiersz 20: treść wiadomości HTML zakodowanej w formacie [quoted-printable] (wiersz 19);
  • wiersze 24–28: treść pliku Word zakodowanego jako [base64] (wiersz 23);
  • wiersze 32–37: zawartość pliku PDF zakodowana jako [base64] (wiersz 31);
  • wiersze 41–45: zawartość pliku OpenOffice zakodowanego jako [base64] (wiersz 40);
  • wiersze 50–55: zawartość pliku graficznego zakodowanego jako [base64] (wiersz 49);
  • wiersze 59–63: zawartość załączonej wiadomości e-mail zakodowana jako [base64] (wiersz 58);

Teraz, gdy:

  • wiemy, jak odzyskać teksty z różnych części wiadomości e-mail;
  • znamy kodowanie tych tekstów;

możemy zapisać te teksty w plikach.

Kod zmienia się w następujący sposób:


function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
  // obliczanie numeru sekcji
  if (substr($sectionNumber, 0, 2) === "0.") {
    $sectionNumber = substr($sectionNumber, 2);
  }
  print "-----contenu de la partie n° [$sectionNumber]\n";
  // typ zawartości
  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;
  }
  // typ kodowania
  $encodings = ["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
  print "Transfer-Encoding : " . $encodings[$part->encoding] . "\n";

  // czy to jest wiadomość?
  if ($part->type === TYPEMESSAGE) {
    // nie będziemy obsługiwać podczęści tej wiadomości
    savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
  } else {
    // przechodzimy do ewentualnych podczęści
    if (isset($part->parts)) {
      for ($i = 1; $i <= count($part->parts); $i++) {
        // nowa część komunikatu
        $subpart = $part->parts[$i - 1];
        // wywołanie rekurencyjne – żądamy treści części [$subpart]
        getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
      }
    } else {
      // nie ma podczęści – zapisujemy więc treść wiadomości
      savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
    }
  }
}
  • wiersze 33 i 45: wyświetlanie tekstu części wiadomości e-mail o nazwie [$imapResource, $msgNumber, $sectionNumber] zostało zastąpione przez zapisywanie go w pliku;

Funkcja [savePart] działa w następujący sposób:


// zapisanie części wiadomości
function savePart($imapResource, int $msgNumber, string $sectionNumber, array $infos, object $infosMail): void {
  // folder zapisu
  $outputDir = $infos["output-dir"] . "/message-$msgNumber";
  // jeśli folder nie istnieje, tworzy się go
  if (!file_exists($outputDir)) {
    mkdir($outputDir);
  }
  // struktura części do zapisania
  $struct = imap_bodystruct($imapResource, $msgNumber, $sectionNumber);
  // typ dokumentu
  $type = $struct->type;
  // podtyp dokumentu
  $subtype = "";
  if (isset($struct->subtype)) {
    $subtype = strtolower($struct->subtype);
  }
  // analizowany jest typ części
  switch ($type) {
    case TYPETEXT:
      // w przypadku wiadomości tekstowej: 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:
      // inne przypadki – interesują nas wyłącznie załączniki
      if (isset($struct->disposition)) {
        $disposition = strtolower($struct->disposition);
        if ($disposition === "attachment") {
          // mamy do czynienia z załącznikiem – zapisujemy go
          saveAttachment($imapResource, $msgNumber, $sectionNumber, $outputDir, $struct);
        }
      } else {
        // ta część nie zostanie przetworzona
        print "Partie [$sectionNumber] ignorée\n";
      }
      break;
  }
}
  • wiersze 3–8: utworzenie folderu zapasowego. Folder ten nosi numer wiadomości, której części są analizowane;
  • wiersz 10: część wiadomości, która ma zostać zapisana, jest jednoznacznie określona przez trzy parametry [$imapResource, $msgNumber, $sectionNumber]. Struktura tej części jest pobierana za pomocą funkcji [imap_bodystruct];
  • wiersz 12: pobierany jest główny typ części wiadomości;
  • wiersze 13–17: pobierany jest jej podtyp;
  • wiersze 20–30: przetwarzane są dwa typy treści: [text/plain] (wiersze 23–25) oraz [text/HTML] (wiersze 26–28). Pozostałe typy [text/xx] są ignorowane;
  • wiersz 24: tekst z części [text/plain] zostanie zapisany w pliku [message.txt];
  • wiersz 27: tekst części [text/HTML] zostanie zapisany w pliku [message.HTML];
  • wiersze 31–43: rozpatrujemy przypadki części, których typ główny nie jest [text];
  • wiersz 35: uwzględniane są wyłącznie załączniki wiadomości;
  • wiersz 37: są one zapisywane w pliku za pomocą funkcji [saveAttachment];

Podsumowując powyższy kod:

  • zapisuje części [text/plain] i [text/HTML] za pomocą funkcji [saveText]. Części te stanowią treść wiadomości e-mail;
  • zapisuje różne załączniki za pomocą funkcji [saveAttachment];

Funkcja [saveText] działa w następujący sposób:


// zapis tekstu wiadomości [$text]
function saveText(string $fileName, int $type, string $text, object $infosMail, object $struct) {
  // przygotowanie tekstu do zapisania
  // $text jest zakodowany – dekodujemy go
  switch ($struct->encoding) {
    case ENCBASE64:
      $text = base64_decode($text);
      break;
    case ENCQUOTEDPRINTABLE:
      $text = quoted_printable_decode($text);
      break;
  }
  // nagłówki wiadomości
  // od
  $from = "From: ";
  foreach ($infosMail->from as $expéditeur) {
    $from .= $expéditeur->mailbox . "@" . $expéditeur->host . ";";
  }
  // do
  $to = "To: ";
  foreach ($infosMail->to as $destinataire) {
    $to .= $destinataire->mailbox . "@" . $destinataire->host . ";";
  }
  // temat
  $subject = "Subject: " . $infosMail->subject;
  // utworzenie tekstu do zapisania
  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;
  }
  // tworzenie pliku
  print "sauvegarde d'un message dans [$fileName]\n";
  // tworzenie pliku
  if (! file_put_contents($fileName, $contents)) {
    // niepowodzenie podczas tworzenia pliku
    print "Impossible de créer le fichier [$fileName]\n";
  }
}

Komentarze

  • wiersz 1:
    • [$fileName] to nazwa pliku, w którym zostanie zapisany tekst [$text];
    • [$type]: przyjmuje wartość 0 dla pliku tekstowego, 1 dla pliku HTML;
    • [$text]: to tekst, który ma zostać zapisany. Najpierw jednak należy go zdekodować, ponieważ jest zakodowany;
    • [$infosMail]: zawiera ogólne informacje o wiadomości e-mail. Wykorzystamy pola [from, to, subject];
    • [$struct]: to struktura opisująca tę część wiadomości e-mail, którą właśnie zapisujemy. Pozwoli nam to ustalić rodzaj kodowania tekstu, który ma zostać zapisany;
  • wiersze 4–12: dekodujemy tekst, który ma zostać zapisany;
  • wiersze 13–25: pobieramy informacje [from, to, subject] z wiadomości e-mail;
  • wiersze 27–36: w zależności od typu (0 lub 1) tekstu, który ma zostać zapisany, tworzy się tekst zwykły (wiersz 30) lub tekst w formacie HTML (wiersz 34);
  • wiersz 40: cały tekst jest zapisywany w pliku [$fileName];

Załączniki są natomiast zapisywane za pomocą następującej funkcji [saveAttachment]:


// zapisanie załącznika
function saveAttachment($imapResource, int $msgNumber, string $sectionNumber, string $outputDir, object $struct) {
  // analiza struktury załącznika
  // próba pobrania nazwy pliku, w którym ma zostać zapisany załącznik
  // nazwa ta znajduje się w elementach [dparameters] struktury
  if (isset($struct->dparameters)) {
    // pobieramy elementy [dparameters]
    $dparameters = $struct->dparameters;
    $fileName = "";
    // przeglądamy tablicę elementów o nazwie [dparameters]
    foreach ($dparameters as $dparameter) {
      // każdy element [dparameter] jest obiektem posiadającym dwa atrybuty [attribute, value]
      $attribute = strtolower($dparameter->attribute);
      // atrybut [filename] odpowiada nazwie pliku, który ma zostać utworzony
      // w tym przypadku nazwa pliku znajduje się w [$dparameter->value]
      if ($attribute === "filename") {
        $fileName = $dparameter->value;
        break;
      }
    }
    // jeśli nie znaleziono nazwy pliku, sprawdzamy atrybut [parameters] w strukturze
    if ($fileName === "" && isset($struct->parameters)) {
      // pobieramy [parameters]
      $parameters = $struct->parameters;
      foreach ($parameters as $parameter) {
        // każdy parametr jest słownikiem zawierającym dwa klucze [attribute, value]
        $attribute = strtolower($parameter->attribute);
        // jeśli atrybutem jest [name], to [value] jest nazwą pliku
        if ($attribute === "name") {
          $fileName = $parameter->value;
          // nazwa pliku może być zakodowana
          // na przykład =?utf-8?Q?Kursy-Poradniki-Serge-Tah=C3=A9-1568x268=2Ep
          // kodowanie odzyskuje się za pomocą wyrażenia regularnego
          $champs = [];
          $match = preg_match("/=\?(.+?)\?/", $fileName, $champs);
          // jeśli występuje zgodność, to dekodujemy nazwę pliku
          if ($match) {
            $fileName = iconv_mime_decode($fileName, 0, $champs[1]);
          }
          break;
        }
      }
    }
  }
  // jeśli znaleziono nazwę pliku, zapisujemy załącznik
  if ($fileName !== "") {
    // zapisanie załącznika
    $fileName = "$outputDir/$fileName";
    print "sauvegarde de l'attachement dans [$fileName]\n";
    // utworzenie pliku
    if ($file = fopen($fileName, "w")) {
      // pobieramy zakodowany tekst z załącznika
      $text = imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
      // załącznik jest zakodowany – dekodujemy go
      switch ($struct->encoding) {
        // base64
        case ENCBASE64:
          $text = base64_decode($text);
          break;
        // quoted printable
        case ENCQUOTEDPRINTABLE:
          $text = quoted_printable_decode($text);
          break;
        default:
          // pozostałe przypadki są ignorowane
          break;
      }
      // zapis tekstu do pliku
      fputs($file, $text);
      // zamknięcie pliku
      fclose($file);
    } else {
      // nie udało się utworzyć pliku
      print "L'attachement n'a pu être sauvegardé dans [$fileName]\n";
    }
  }
}

Uwagi

  • wiersz 2: funkcja [saveAttachment] przyjmuje następujące parametry:
    • [$imapResource, int $msgNumber, string $sectionNumber] jednoznacznie definiują część IMAP, która ma zostać zapisana;
    • [string $outputDir] to folder zapisu;
    • [object $struct] opisuje strukturę części wiadomości, która ma zostać zapisana;
  • wiersze 6–44: wyszukiwana jest nazwa pliku powiązanego z załącznikiem. Ta sama nazwa pliku zostanie użyta do jego zapisania. Nazwa pliku załącznika znajduje się w tabeli [$struct→dparameters] lub w tabeli [$struct→parameters], a nawet w obu;
  • wiersze 30–40: jeśli nazwa pliku zawiera znaki niekodowane w 7 bitach, oznacza to, że została zakodowana w tabeli [quoted-printable]. W takim przypadku w tabeli [$struct→dparameters] atrybut nosi nazwę [fileName*] zamiast [fileName]. Oznacza to, że nie spełnił on warunku z wiersza 16. Nazwa pliku jest następnie wyszukiwana w tabeli [$struct→parameters];
  • wiersz 32: przykład zakodowanej nazwy pliku. Ma ona następującą postać: =?codage_original?codage_actuel?nom_encodé. Zatem nazwa [=?utf-8?Q?Cours-Tutoriels-Serge-Tah=C3=A9-1568x268=2Ep] oznacza, że nazwa pliku brzmiała wcześniej UTF-8, a obecnie brzmi [quoted-printable] (Q);
  • wiersz 38: nazwa pliku jest dekodowana za pomocą funkcji [iconv_mime_decode], która przyjmuje tutaj trzy parametry:
    • ciąg znaków do dekodowania;
    • domyślnie pozostawić na 0;
    • zestaw znaków, który ma być użyty do przedstawienia zdekodowanego ciągu znaków. Parametr ten występuje w ciągu znaków do zdekodowania. Uzyskuje się go za pomocą wyrażenia regularnego w wierszach 34–35;
  • wiersze 45–75: załącznik zapisujemy w pliku o znalezionej nazwie;

Aby przetestować skrypt [imap-02.php], najpierw wysyłamy wiadomość e-mail do [guest@localhost] z następującą konfiguracją:

{
    "mail to localhost via localhost": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "test-localhost",
        "message": "ligne 1\nligne 2\nligne 3",
        "tls": "FALSE",
        "attachments": [
            "/attachments/Hello from SwiftMailer.docx",
            "/attachments/Hello from SwiftMailer.pdf",
            "/attachments/Hello from SwiftMailer.odt",
            "/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
            "/attachments/test-localhost.eml"
        ]
    }
}

Mamy więc pięć załączników.

Otwieramy wiadomość e-mail wysłaną za pomocą skryptu [imap-02.php] z następującą konfiguracją:

{
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3"
    }
}

Wyniki wyświetlane w konsoli są następujące:


------------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.

Pliki zapisane w folderze [output/localhost-pop3/message-N] to:

Image

16.6.6. Klient POP3 / IMAP z biblioteką [php-mime-mail-parser]

W poprzednim skrypcie [imap-02.php] udało nam się zapisać:

  • treści wiadomości e-mail [text/plain] i [text/HTML];
  • załączniki wiadomości e-mail;

W przypadku załącznika typu [message/rfc822] zapisaliśmy również zawartość tego załącznika. Jednak ten typ załącznika sam w sobie jest wiadomością e-mail, która z kolei zawiera treści o typach [text/plain] i [text/HTML], a także załączniki. Możemy zatem znaleźć się w następującej sytuacji:

  • plik [mail 1], którego struktura jest analogiczna do struktury załącznika typu [message/rfc822];
  • plik [mail 2] załączony do wiadomości e-mail nr 1;
  • plik [mail 3] dołączony do wiadomości e-mail nr 2;
  • itd…

Skrypt [imap-02.php] zapisuje zawartość pliku [mail 1] (teksty i załączniki). Zapisuje plik [mail 2] jako załącznik, ale na tym kończy działanie. Nie próbuje on analizować pliku [mail 2] w celu wyodrębnienia z niego tekstów i załączników. Można by pomyśleć, że wystarczy zastosować do pliku [mail 2] to samo, co zrobiono w przypadku pliku [mail 1]. Wystarczyłoby wówczas rekurencyjne wywołanie metody, która przetwarzała plik [mail 1], aby uzyskać treść wszystkich wiadomości e-mail zagnieżdżonych jedna w drugiej. Niestety części wiadomości [mail 2] są numerowane według logiki odmiennej od tej zastosowanej w przypadku [mail 1], co uniemożliwia zastosowanie tego samego algorytmu w obu przypadkach, chyba że zastosuje się dość złożoną logikę do obliczania numerów części wiadomości, niezależnie od jego pozycji w zbiorze wiadomości zagnieżdżonych.

Skrypt [imap-02.php] był już skomplikowany. Aby uniknąć jeszcze większego jego skomplikowania w celu obsługi treści wiadomości zagnieżdżonych, wykorzystamy bibliotekę [php-mime-mail-parser] dostępną na GitHubie (maj 2019 r.) pod adresem URL [https://github.com/php-mime-mail-parser/php-mime-mail-parser], napisaną przez Vincenta Dauce’a.

16.6.6.1. Instalacja biblioteki [php-mime-mail-parser]

Strona prezentacyjna biblioteki zawiera instrukcję jej instalacji w systemie Windows:

Image

Instalacja biblioteki OS w systemie Windows składa się z dwóch etapów:


télécharger une DLL ;
modifier le fichier [php.ini] qui configure PHP ;

LA DLL z biblioteki [mailparse] jest dostępny w URL [http://pecl.php.net/package/mailparse] (maj 2019 r.);

Image

  • w [2] należy wybrać najnowszą i najbardziej stabilną wersję biblioteki;

Image

  • w [3] należy wybrać wersję PHP, z której korzystasz (w tym dokumencie jest to PHP 7.2);
  • w [4] należy wybrać wersję systemu Windows (tutaj jest to 64-bitowy system Windows). Wybieramy wersję [Thread Safe];

Aby sprawdzić wersję pliku PHP pobranego wraz z Laragonem, otwórz plik [Terminal] z okna Laragona i wpisz następujące polecenie:


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                      

Wersja pliku PHP 7.2.11 jest podana w wierszu 3. W tym samym wierszu podana jest również wersja systemu Windows użyta do kompilacji (32- lub 74-bitowa).

Po uzyskaniu pliku DLL należy go skopiować do folderu [<laragon>/bin/php/<version-php>/ext] [5]:

Image

Następnie należy aktywować to rozszerzenie w pliku [php.ini], który konfiguruje PHP (patrz akapit dotyczący linku):

Image

Prawdopodobnie wiersz [7] nie będzie istniał i trzeba będzie go dodać samodzielnie.

Po włączeniu rozszerzenia można sprawdzić jego poprawność, wpisując następujące polecenie w terminalu 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)                                                              

Polecenie [php –-ini] ładuje plik konfiguracyjny z linii 4. Następnie załaduje ono DLL wszystkich aktywnych rozszerzeń do [php.ini]. Jeśli któryś z nich jest nieprawidłowy, zostanie to zgłoszone. W ten sposób sprawdzona zostanie poprawność pliku DLL dodanego do [php_mailparse.dll]. Może on zostać uznany za nieprawidłowy z różnych powodów, z których najczęstsze to:

  • pobrano plik DLL, który nie odpowiada używanej wersji PHP;
  • pobrano plik DLL w wersji 32-bitowej, podczas gdy posiadany jest plik PHP w wersji 64-bitowej lub odwrotnie;

Po aktywowaniu i sprawdzeniu rozszerzenia można przejść do instalacji biblioteki [php-mime-mail-parser]:

Image

Polecenie [8] należy wpisać w terminalu Laragon (patrz link w akapicie):

Image

  • w przypadku [1] należy sprawdzić, czy znajdujesz się w folderze [<laragon>/www];
  • po wprowadzeniu [2] uruchom polecenie instalacji biblioteki [php-mime-mail-parser];
  • w [3] nic nie zostało zainstalowane, ponieważ biblioteka [php-mime-mail-parser] była już zainstalowana;

Instalacja biblioteki [php-mime-mail-parser] odbywa się w folderze [<laragon>/www/vendor]:

Image

Image

  • w [2-3], źródła biblioteki [php-mime-mail-parser];

Teraz, gdy środowisko pracy zostało zainstalowane, można przejść do pisania skryptu [imap-03.php].

16.6.6.2. Skrypt [imap-03.php]

Skrypt [imap-03.php] wykorzystuje ten sam plik konfiguracyjny [config-imap-01.json], co poprzednie skrypty:

{
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3"
    }
}

Skrypt [imap-03.php] ma następującą postać:


<?php

// klient IMAP (Internet Message Access Protocol) umożliwiający odczyt wiadomości e-mail
// napisany przy użyciu biblioteki [php-mime-mail-parser]
// dostępny pod adresemURL [https://github.com/php-mime-mail-parser/php-mime-mail-parser] (maj 2019)
//
// ścisłe przestrzeganie zadeklarowanych typów parametrów funkcji
declare (strict_types=1);
// obsługa błędów
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// zależności
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
// parametry odczytu poczty
const CONFIG_FILE_NAME = "config-imap-01.json";

// pobieranie konfiguracji
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);

// odczyt skrzynek pocztowych
foreach ($mailboxes as $name => $infos) {
  // śledzenie
  print "------------Lecture de la boîte à lettres [$name]\n";
  // odczyt skrzynki pocztowej
  readmailbox($name, $infos);
}
// koniec
exit;

Komentarze

  • wiersze 18–23: zawartość pliku konfiguracyjnego jest umieszczana w słowniku [$mailboxes];
  • wiersze 26–31: każda skrzynka pocztowa jest odczytywana przez funkcję [readmailbox] (wiersz 30). Funkcja ta odczytuje w rzeczywistości nieprzeczytane wiadomości ze skrzynki pocztowej. Skrzynka pocztowa odpowiada adresowi e-mail danego użytkownika;

Funkcja [readmailbox] ma następujący wygląd:


function readmailbox(string $name, array $infos): void {
  // nawiązywanie połączenia
  $imapResource = imap_open($name, $infos["user"], $infos["password"]);
  if (!$imapResource) {
    // niepowodzenie
    print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
    exit;
  }
  // Połączenie nawiązane
  print "Connexion établie avec le serveur [$name].\n";
  // łączna liczba wiadomości w skrzynce pocztowej
  $nbmsg = imap_num_msg($imapResource);
  print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
  // nieprzeczytane wiadomości w bieżącej skrzynce pocztowej
  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 {
      // przeglądanie listy nieprzeczytanych wiadomości
      foreach ($msgNumbers as $msgNumber) {
        print "---message n° [$msgNumber]\n";
        // pobierana jest treść wiadomości nr $msgNumber
        getMailBody($imapResource, $msgNumber, $infos);
        // jeśli protokół to POP3, wiadomość jest usuwana po jej pobraniu
        $pop3 = $infos["pop3"];
        if ($pop3 !== NULL) {
          // oznacza się wiadomość jako „do usunięcia”
          imap_delete($imapResource, $msgNumber);
        }
      }
      // koniec przeglądania nieprzeczytanych wiadomości
      if ($pop3 !== NULL) {
        // usuwa się wiadomości oznaczone jako „do usunięcia”
        imap_expunge($imapResource);
      }
    }
  }
  // zamknięcie połączenia
  $imapClose = imap_close($imapResource);
  if (!$imapClose) {
    // niepowodzenie
    print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
  } else {
    // powodzenie
    print "Fermeture de la connexion réussie.\n";
  }
}

Komentarze

Kod funkcji [readmailbox] jest taki sam jak w poprzednich skryptach.

Funkcja [getMailBody] (wiersz 25), która analizuje treść wiadomości (treść + załączniki), wygląda następująco:


// analiza treści wiadomości
function getMailBody($imapResource, int $msgNumber, array $infos): void {
  // pobieranie pełnej treści wiadomości
  $text = imap_fetchbody($imapResource, $msgNumber, "");
  if ($text === FALSE) {
    print "Le corps du message [$msgNumber] n'a pu être récupéré";
    return;
  }
  // tworzy się parser, który przeanalizuje tekst wiadomości
  $parser = (new PhpMimeMailParser\Parser())->setText($text);
  // pobieranie poszczególnych części wiadomości
  $outputDir = $infos["output-dir"] . "/message-$msgNumber";
  getParts($parser, $msgNumber, $outputDir);
}

Komentarze

  • wiersz 2: funkcja [getMailBody] przyjmuje trzy parametry:
    • [$imapResource]: zasób IMAP, z którym nawiązano połączenie;
    • [$msgNumber]: numer wiadomości (w skrzynce pocztowej), która ma zostać przetworzona;
    • [$infos]: różne informacje o obsługiwanej skrzynce pocztowej;
  • wiersz 4: pobierana jest cała treść wiadomości o numerze [$msgNumber];
  • wiersze 5–8: sytuacja, w której nie udało się pobrać treści wiadomości;
  • wiersz 10: rozpoczynamy korzystanie z biblioteki [php-mime-mail-parser]. Obiekt [$parser] zostanie odpowiedzialny za analizę tekstu wiadomości;
  • wiersz 12: [$outputDir] będzie folderem, w którym zostaną zapisane treści tekstowe i załączniki wiadomości nr [$msgNumber];
  • wiersz 13: funkcja [getParts] ma za zadanie znaleźć poszczególne części (treść tekstową i załączniki) wiadomości nr [$msgNumber] i zapisać je w folderze [$outputDir];

Funkcja [getParts] wygląda następująco:


// pobieranie poszczególnych części wiadomości
function getParts(PhpMimeMailParser\Parser $parser, int $msgNumber, string $outputDir): void {
  // w razie potrzeby tworzy się folder do zapisania wiadomości
  if (!file_exists($outputDir)) {
    if (!mkdir($outputDir)) {
      print "Le dossier [$outputDir] n'a pu être créé\n";
      return;
    }
  }
  // pobieramy nagłówki wiadomości
  $arrayHeaders = $parser->getHeaders();
  // zapisywanie wiadomości tekstowych
  $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");
  }
  // zapisywanie wiadomości 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");
  }
  // pobierane są załączniki wiadomości
  $attachments = $parser->getAttachments();
  // numer załącznika
  $iAttachment = 0;
  // przeglądanie listy załączników
  foreach ($attachments as $attachment) {
    // typ załącznika
    $fileType = $attachment->getContentType();
    print "-- Sauvegarde d'un attachement de type [$fileType] dans le fichier [$outputDir/{$attachment->getFilename()}]\n";
    // zapisywanie załącznika
    try {
      $attachment->save($outputDir, PhpMimeMailParser\Parser::ATTACHMENT_DUPLICATE_SUFFIX);
    } catch (Exception $e) {
      print "L'attachement n'a pu être sauvegardé : " . $e->getMessage() . "\n";
    }
    // szczególny przypadek typu message/rfc822
    if ($fileType === "message/rfc822") {
      // załącznik sam w sobie jest wiadomością – należy go również przeanalizować
      // zmiana katalogu zapisu
      $iAttachment++;
      $outputDir = $outputDir . "/rfc822-$iAttachment";
      // zmieniamy treść do analizy
      $parser->setText($attachment->getContent());
      // analizujemy wiadomość rekurencyjnie
      getParts($parser, $msgNumber, $outputDir);
    }
  }
}

Uwagi

  • wiersz 2: funkcja [getParts] przyjmuje trzy parametry:
    • parser [$parser], do którego przekazano cały tekst analizowanej wiadomości;
    • [$msgNumber] to numer analizowanej wiadomości;
    • [$outputDir] to folder, w którym należy zapisać treść i załączniki wiadomości;
  • wiersze 4–9: utworzenie folderu [$outputDir];
  • wiersz 11: pobieranie nagłówków analizowanej wiadomości (od, do, temat…);
  • wiersz 13: pobierane są części wiadomości e-mail o typie [text/plain]. Pobierana jest tablica;
  • wiersze 14–17: zapisujemy wszystkie elementy pobranej tablicy, nadając każdemu z nich inną nazwę pliku;
  • wiersz 19: pobieramy części wiadomości e-mail o typie [text/html]. Otrzymujemy tablicę;
  • wiersze 20–23: zapisujemy wszystkie elementy pobranej tablicy, nadając każdemu z nich inną nazwę pliku;
  • wiersz 25: pobieramy listę załączników analizowanej wiadomości;
  • wiersz 29: przeglądamy tę listę;
  • wiersz 24: pobieramy typ załącznika (atrybut Content-Type);
  • wiersze 34–38: zapisywanie załącznika w folderze [$outputDir]. Drugi parametr [PhpMimeMailParser\Parser::ATTACHMENT_DUPLICATE_SUFFIX] określa strategię nazewniczą załączników. Jeśli wartość [$attachment→getFilename()] wynosi X, a plik X już istnieje, wówczas biblioteka [php-mime-mail-parser] próbuje nazw [X_1], [X_2], itd., aż znajdzie nazwę pliku, która jeszcze nie istnieje;
  • wiersz 40: sprawdzane jest, czy załącznik jest wiadomością e-mail;
  • wiersze 41–48: jeśli tak, to wiadomość ta jest z kolei analizowana w celu wyodrębnienia treści i załączników;
  • wiersz 44: jeśli wartość [$outputDir] wynosi X, a wśród załączników analizowanej wiadomości znajdują się dwa e-maile, to pierwszy z nich zostanie zapisany w folderze [$outputDir/rfc822-1], a drugi w folderze [$outputDir/rfc822-2];
  • wiersz 46: treść załączonego e-maila staje się nowym tekstem do analizy;
  • wiersz 48: wywołuje się rekurencyjnie funkcję [getParts] w celu analizy nowego tekstu;

Funkcja [saveMessage] zapisuje treść wiadomości, która ma zostać przeanalizowana:


// zapis wiadomości tekstowej
function saveMessage(string $text, int $type, array $arrayHeaders, string $filename): void {
  // treść do zapisania
  $contents = "";
  // dodawanie nagłówków
  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";
  }
  // dodanie treści wiadomości
  $contents .= $text;
  // zapisanie całości
  if (!file_put_contents($filename, $contents)) {
    // niepowodzenie
    print "Le message n'a pu être sauvegardé dans le fichier [$filename]\n";
  } else {
    // powodzenie
    print "Le message a été sauvegardé dans le fichier [$filename]\n";
  }
}

Komentarze

  • funkcja [saveMessage] przyjmuje następujące parametry:
    • [$text]: tekst do zapisania;
    • [$type]: typ tekstu (0: text/plain, 1: text/HTML);
    • [$arrayHeaders]: nagłówki analizowanej wiadomości;
    • [$filename]: nazwa pliku, w którym ma zostać zapisany [$text];
  • wiersz 4: [$contents] będzie reprezentować całość tekstu do zapisania;
  • wiersze 6–20: najpierw zapisane zostaną wszystkie nagłówki wiadomości (from, to, subject…);
  • wiersze 16–19: w przypadku tekstu HTML każdy wiersz kończy się tagiem <br/>, aby każdy nagłówek pojawiał się osobno w swoim wierszu w przeglądarce;
  • wiersz 22: do nagłówków dodaje się tekst wiadomości, który ma zostać zapisany;
  • wiersze 24–30: całość jest zapisywana w pliku [$filename];

Wykorzystanie biblioteki [php-mime-mail-parser] znacznie ułatwia pisanie skryptu do odczytu wiadomości e-mail.

Skrypt [smtp-02.php] służy do wysyłania wiadomości e-mail do użytkownika [guest@localhost] z następującą konfiguracją:

{
    "mail to localhost via localhost": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "test-localhost",
        "message": "ligne 1\nligne 2\nligne 3",
        "tls": "FALSE",
        "attachments": [
            "/attachments/Hello from SwiftMailer.docx",
            "/attachments/Hello from SwiftMailer.pdf",
            "/attachments/Hello from SwiftMailer.odt",
            "/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
            "/attachments/test-localhost-2.eml"
        ]
    }
}
  • wiersze 11–15: jest pięć załączników;
  • wiersz 15: [test-localhost-2.eml] to wiadomość e-mail o następującej strukturze:
    • [test-localhost-2.eml] zawiera 4 załączniki (te same, co w wierszach 11–14) oraz załączoną wiadomość e-mail;
    • wiadomość dołączona do [test-localhost-2.eml] zawiera 4 załączniki (te same, co w wierszach 11–14);

Skrypt [imap-03.php] służy do odczytu skrzynki pocztowej użytkownika [guest@localhost] przy następującej konfiguracji:

{
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3"
    }
}

Po uruchomieniu struktura folderów [output/localhost-pop3] wygląda następująco:

Image

  • w [1] – 5 załączników z wiadomości e-mail otrzymanej przez [guest@localhost];
  • w [2] – 5 załączników z wiadomości e-mail [test-localhost-2.eml] od [1];
  • w [3] – 4 załączniki z wiadomości e-mail [test-localhost.eml] od [2];

Wyniki wyświetlone na konsoli są następujące:


------------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.

Jeśli wyświetlimy plik [message_1.HTML] z pliku [3] w przeglądarce, otrzymamy następujący wynik:

Image