Skip to content

16. Funzioni di rete

Passiamo ora alle funzioni di rete di PHP che ci consentono di effettuare la programmazione TCP / IP (Transfer Control Protocol / Internet Protocol).

Image

16.1. Nozioni di base sulla programmazione Internet

16.1.1. Informazioni generali

Consideriamo la comunicazione tra due macchine remote A e B:

Image

Quando un'applicazione AppA su un computer A vuole comunicare con un'applicazione AppB su un computer B in Internet, deve conoscere diverse informazioni:

  • l’indirizzo IP (Internet Protocol) o il nome del computer B;
  • il numero della porta su cui opera l’applicazione AppB. Infatti, il computer B può ospitare numerose applicazioni che operano su Internet. Quando riceve informazioni provenienti dalla rete, deve sapere a quale applicazione sono destinate tali informazioni. Le applicazioni del computer B accedono alla rete tramite interfacce denominate anche porte di comunicazione. Questa informazione è contenuta nel pacchetto ricevuto dal computer B affinché possa essere consegnato all’applicazione corretta;
  • i protocolli di comunicazione supportati dal computer B. Nel nostro studio utilizzeremo esclusivamente i protocolli TCP-IP;
  • il protocollo di dialogo accettato dall’applicazione AppB. Infatti, i computer A e B «comunicheranno» tra loro. Ciò che si scambieranno sarà incapsulato nei protocolli TCP-IP. Tuttavia, quando, alla fine della catena, l’applicazione AppB riceverà le informazioni inviate dall’applicazione AppA, dovrà essere in grado di interpretarle. Ciò è analogo alla situazione in cui due persone, A e B, comunicano al telefono: il loro dialogo viene trasportato dal telefono. La voce verrà codificata sotto forma di segnali dal telefono A, trasportata attraverso le linee telefoniche, arriverà al telefono B per essere decodificata. La persona B sente quindi le parole. È qui che entra in gioco il concetto di protocollo di dialogo: se A parla francese e B non capisce questa lingua, A e B non potranno dialogare in modo efficace;

Pertanto, le due applicazioni che comunicano tra loro devono concordare sul tipo di dialogo che adotteranno. Ad esempio, il dialogo con un servizio ftp non è lo stesso di quello con un servizio pop: questi due servizi non accettano gli stessi comandi. Hanno un protocollo di dialogo diverso;

16.1.2. Le caratteristiche del protocollo TCP

In questa sede esamineremo solo le comunicazioni di rete che utilizzano il protocollo di trasporto TCP, di cui riportiamo le caratteristiche principali:

  • il processo che intende trasmettere stabilisce innanzitutto una connessione con il processo destinatario delle informazioni che sta per inviare. Tale connessione avviene tra una porta del computer mittente e una porta del computer ricevente. Tra le due porte viene così creato un percorso virtuale che sarà riservato esclusivamente ai due processi che hanno stabilito la connessione;
  • tutti i pacchetti inviati dal processo sorgente seguono questo percorso virtuale e arrivano nell’ordine in cui sono stati inviati;
  • le informazioni trasmesse hanno un carattere continuo. Il processo mittente invia le informazioni al proprio ritmo. Queste non vengono necessariamente inviate immediatamente: il protocollo TCP attende di averne una quantità sufficiente per inviarle. Vengono memorizzate in una struttura denominata segmento TCP. Una volta riempito, questo segmento verrà trasmesso al livello IP, dove verrà incapsulato in un pacchetto IP;
  • ogni segmento inviato dal protocollo TCP è numerato. Il protocollo TCP destinatario verifica di ricevere correttamente i segmenti in sequenza. Per ogni segmento ricevuto correttamente, invia una conferma di ricezione al mittente;
  • quando quest’ultimo lo riceve, lo comunica al processo mittente. Quest’ultimo può quindi sapere che un segmento è giunto a destinazione;
  • se, trascorso un certo tempo, il protocollo TCP che ha trasmesso un segmento non riceve una conferma di ricezione, ritrasmette il segmento in questione, garantendo così la qualità del servizio di inoltro delle informazioni;
  • il circuito virtuale stabilito tra i due processi che comunicano è full-duplex: ciò significa che le informazioni possono transitare in entrambe le direzioni. In questo modo il processo di destinazione può inviare conferme di ricezione anche mentre il processo di origine continua a inviare informazioni. Ciò consente, ad esempio, al protocollo di origine TCP di inviare più segmenti senza attendere la conferma di ricezione. Se dopo un certo tempo si accorge di non aver ricevuto la conferma di ricezione di un determinato segmento n. n, riprenderà l’invio dei segmenti da quel punto;

16.1.3. La relazione client-server

Spesso la comunicazione su Internet è asimmetrica: la macchina A avvia una connessione per richiedere un servizio alla macchina B, specificando che desidera aprire una connessione con il servizio SB1 della macchina B. Quest’ultima accetta o rifiuta. Se accetta, il computer A può inviare le proprie richieste al servizio SB1. Queste devono essere conformi al protocollo di dialogo supportato dal servizio SB1. Si instaura così un dialogo domanda-risposta tra la macchina A, detta macchina client, e la macchina B, detta macchina server. Uno dei due partner chiuderà la connessione.

16.1.4. Architettura di un client

L’architettura di un programma di rete che richiede i servizi di un’applicazione server sarà la seguente:

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. Architettura di un server

L'architettura di un programma che offre servizi sarà la seguente:

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

Il programma server gestisce in modo diverso la richiesta di connessione iniziale di un client rispetto alle sue successive richieste volte a ottenere un servizio. Il programma non fornisce direttamente il servizio. Se lo facesse, durante la durata del servizio non sarebbe più in ascolto delle richieste di connessione e i client non verrebbero quindi serviti. Proceda quindi in altro modo: non appena una richiesta di connessione viene ricevuta sulla porta di ascolto e poi accettata, il server crea un’attività incaricata di fornire il servizio richiesto dal cliente. Tale servizio viene fornito su un’altra porta del server, denominata porta di servizio. In questo modo è possibile servire più clienti contemporaneamente.

Un'attività di servizio avrà la seguente struttura:

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. Scopri i protocolli di comunicazione di Internet

16.2.1. Introduzione

Quando un client si connette a un server, si instaura un dialogo tra i due. La natura di tale dialogo costituisce ciò che viene definito protocollo di comunicazione del server. Tra i protocolli più diffusi su Internet figurano i seguenti:

  • HTTP: HyperText Transfer Protocol – il protocollo di comunicazione con un server web (server HTTP);
  • SMTP: Simple Mail Transfer Protocol – il protocollo di comunicazione con un server di invio della posta elettronica (server SMTP);
  • POP: Post Office Protocol – il protocollo di comunicazione con un server di archiviazione della posta elettronica (server POP). In questo caso si tratta di recuperare i messaggi di posta elettronica ricevuti e non di inviarne;
  • IMAP: Internet Message Access Protocol – il protocollo di comunicazione con un server di archiviazione della posta elettronica (server IMAP). Questo protocollo ha progressivamente sostituito il precedente protocollo POP;
  • FTP: File Transfer Protocol – il protocollo di comunicazione con un server di archiviazione file (server FTP);

Tutti questi protocolli hanno la particolarità di essere protocolli basati su righe di testo: il client e il server si scambiano righe di testo. Se si dispone di un client in grado di:

  • creare una connessione con un server TCP;
  • visualizzare sulla console le righe di testo che il server gli invia;
  • inviare al server le righe di testo che un utente digiterebbe sulla tastiera;

in questo modo è possibile comunicare con un server TCP che utilizza un protocollo basato su righe di testo, purché si conoscano le regole di tale protocollo.

16.2.2. Utilità TCP

Image

Nei codici associati a questo documento si trovano due utilità di comunicazione TCP:

  • [RawTcpClient] consente di connettersi alla porta P di un server S;
  • [RawTcpServer] consente di creare un server che attende i client su una porta P;

Il server TCP [RawTcpServer]viene chiamato con la sintassi [RawTcpServeur port] per creare un servizio TCP sulla porta [port] della macchina locale (il computer su cui si sta lavorando):

  • il server può servire più client contemporaneamente;
  • il server esegue i comandi digitati dall’utente tramite la tastiera. Questi sono i seguenti:
    • list: elenca i client attualmente connessi al server. Questi vengono visualizzati nel formato [id=x-nom=y]. Il campo [id] serve a identificare i client;
    • send x [texte]: invia un testo al client n. x (id=x). Le parentesi quadre [] non vengono inviate. Sono necessarie nel comando e servono a delimitare visivamente il testo inviato al client;
    • close x: chiude la connessione con il cliente n. x;
    • quit: chiude tutte le connessioni e arresta il servizio;
  • le righe inviate dal cliente al server vengono visualizzate sulla console;
  • tutte le comunicazioni vengono registrate in un file di testo denominato [machine-portService.txt], dove
    • [machine] è il nome della macchina su cui viene eseguito il codice;
    • [port] è la porta del servizio che risponde alle richieste del client;

Il client TCP [RawTcpClient] viene chiamato con la sintassi [RawTcpClient serveur port] per connettersi alla porta [port] del server [serveur]:

  • le righe digitate dall’utente sulla tastiera vengono inviate al server;
  • le righe inviate dal server vengono visualizzate sulla console;
  • tutte le comunicazioni vengono registrate in un file di testo denominato [serveur-port.txt];

Vediamo un esempio. Apriamo due finestre di comando di Windows e in ciascuna di esse ci posizioniamo nella cartella delle utilità. In una delle finestre avviamo il server [RawTcpServer] sulla porta 100:

Image

  • in [1], ci troviamo nella cartella delle utility;
  • in [2], avviamo il server TCP sulla porta 100;
  • in [3], il server entra in modalità di attesa di un client TCP;
  • in [4], il server attende un comando digitato dall’utente sulla tastiera;

Nell'altra finestra di comando, si avvia il client TCP:

Image

  • in [5], ci troviamo nella cartella delle utilità;
  • in [6], avviamo il client TCP: gli indichiamo di connettersi alla porta 100 della macchina locale (quella su cui state lavorando);
  • in [7], il client è riuscito a connettersi al server. Vengono indicate le coordinate del client: si trova sulla macchina [DESKTOP-528I5CU] (la macchina locale in questo esempio) e utilizza la porta [50405] per comunicare con il server:
  • in [8], il client attende un comando digitato dall’utente sulla tastiera;

Torniamo alla finestra del server. Il suo contenuto è cambiato:

Image

  • in [9], è stato rilevato un client. Il server gli ha assegnato il numero 1. Il server ha identificato correttamente il client remoto (macchina e porta);
  • in [10], il server torna in attesa di un nuovo client;

Torniamo alla finestra del client e inviamo un comando al server:

Image

  • in [11], il comando inviato al server;

Torniamo alla finestra del server. Il suo contenuto è cambiato:

Image

  • in [12], tra parentesi, il messaggio ricevuto dal server;

Inviiamo una risposta al client:

Image

  • in [13], la risposta inviata al cliente 1. Viene inviato solo il testo tra le parentesi quadre, non le parentesi stesse;

Torniamo alla finestra del cliente:

Image

  • in [14], la risposta ricevuta dal cliente. Il testo ricevuto è quello tra parentesi quadre;

Torniamo alla finestra del server per vedere altri comandi:

Image

  • in [15], richiediamo l’elenco dei client;
  • in [16], la risposta;
  • con [17], chiudiamo la connessione con il cliente n. 1;
  • in [18], la conferma del server;
  • in [19], arrestiamo il server;
  • in [20], la conferma del server;

Torniamo alla finestra del client:

Image

  • in [21], il client ha rilevato la fine del servizio;

Sono stati creati due file di log, uno per il server e uno per il client:

Image

  • in [25], i log del server: il nome del file corrisponde al nome del client [machine-port];
  • [26], i log del client: il nome del file corrisponde al nome del server [machine-port];

I log del server sono i seguenti:

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

I log del client sono i seguenti:

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

16.3. Ottenere il nome o l'indirizzo IP di un computer su Internet

Image

I computer su Internet sono identificati da un indirizzo IP (IPv4 o IPv6) e, nella maggior parte dei casi, da un nome. Tuttavia, in definitiva viene utilizzato solo l’indirizzo IP. A volte è quindi necessario conoscere l’indirizzo IP di un computer identificato tramite il proprio nome.

Lo script [ip-01.php] è il seguente:


<?php

// rigoroso rispetto dei tipi dichiarati dei parametri delle funzioni
declare (strict_types=1);
//
// gestione degli errori
error_reporting(E_ALL & E_STRICT);
ini_set("display_errors", "on");
//
// costanti
$HOTES = array("istia.univ-angers.fr", "www.univ-angers.fr", "www.ibm.com", "localhost", "", "xx");
// indirizzi IP e nomi delle macchine di $HOTES
for ($i = 0; $i < count($HOTES); $i++) {
  getIPandName($HOTES[$i]);
}
// fine
print "Terminé\n";
exit;

//------------------------------------------------
function getIPandName(string $nomMachine): void {
  //$nomMachine: nome del computer di cui si desidera l'indirizzo IP
  //
  // nomMachine-->indirizzo IP
  $ip = gethostbyname($nomMachine);
  print "---------------\n";
  if ($ip !== $nomMachine) {
    print "ip[$nomMachine]=$ip\n";
    // indirizzo 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";
  }
}

Commenti

  • righe 7-8: si richiede che PHP segnali tutti gli errori (E_ALL e E_STRICT) e che questi vengano visualizzati. Questa modalità è consigliata solo in modalità sviluppo per migliorare il codice grazie agli avvisi di PHP. In modalità produzione, alla riga 8, si imposterebbe «off». A partire dalla versione 5.4 di PHP, il livello E_STRICT è incluso in E_ALL;
  • riga 11: l’elenco delle macchine di cui si desidera il nome e l’indirizzo IP;

Le funzioni di rete di PHP vengono utilizzate nella funzione getIpandName della riga 21.

  • riga 25: la funzione gethostbyname($nom) consente di ottenere l'indirizzo IP "ip3.ip2.ip1.ip0" del computer denominato $nom. Se la macchina $nom non esiste, la funzione restituisce $nom come risultato;
  • riga 30: la funzione gethostbyaddr($ip) consente di ottenere il nome del computer con indirizzo $ip nella forma "ip3.ip2.ip1.ip0". Se il computer $ip non esiste, la funzione restituisce $ip come risultato;

Risultati:


---------------
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. Il protocollo HTTP (HyperText Transfer Protocol)

16.4.1. Esempio 1

Image

Quando un browser visualizza un URL, funge da client di un server web o, in altre parole, di un server HTTP. È il browser a prendere l’iniziativa e a inviare per primo una serie di comandi al server. Per questo primo esempio:

  • il server sarà l’utilità [RawTcpServer];
  • il client sarà un browser;

Per prima cosa avviamo il server sulla porta 100:

Image

Poi, con un browser, richiediamo URL [localhost:100], ovvero specifichiamo che il server HTTP interpellato opera sulla porta 100 del computer locale:

Image

Torniamo alla finestra del server:

Image

  • in [3], il client che si è connesso;
  • in [4-7], la serie di righe di testo che ha inviato:
    • in [4]: questa riga ha il formato [GET URL HTTP/1.1]. Richiede l’URL / e chiede al server di utilizzare il protocollo HTTP 1.1;
    • in [5]: questa riga ha il formato [Host: serveur:port]. La maiuscola o minuscola del comando [Host] non ha importanza. Si ricorda che il client interroga un server locale operante sulla porta 100;
    • il comando [User-Agent] fornisce l’identità del client;
    • il comando [Accept] indica quali tipi di documento sono accettati dal cliente;
    • il comando [Accept-Language] indica in quale lingua si desiderano i documenti richiesti, qualora esistano in più lingue;
    • il comando [Connection] indica la modalità di connessione desiderata: [keep-alive] indica che la connessione deve essere mantenuta fino al termine degli scambi;
    • in [7]: il cliente termina i propri comandi con una riga vuota;

Terminiamo la connessione chiudendo il server:

Image

16.4.2. Esempio 2

Ora che conosciamo i comandi inviati da un browser per richiedere un URL, richiederemo questo URL con il nostro client TCP [RawTcpClient]. Il server Apache di Laragon fungerà da nostro server web.

Avviamo Laragon e poi il server web Apache:

Image

Image

Ora, utilizzando un browser, accediamo a URL e [http://localhost:80]. Qui specifichiamo solo il server [localhost:80] e non il documento URL. In questo caso viene richiesto il URL, ovvero la radice del server web:

Image

  • in [1], ovvero il URL richiesto. Inizialmente era stato digitato [http://localhost:80] e il browser (in questo caso Firefox) l’ha semplicemente trasformata in [localhost] poiché il protocollo [http] è implicito quando non viene specificato alcun protocollo e la porta [80] è implicita quando la porta non è specificata;
  • in [2], la pagina radice / del server web interpellato;

Ora visualizziamo il testo ricevuto dal browser:

Image

  • Si fa clic con il tasto destro sulla pagina ricevuta e si seleziona l'opzione [2]. Si ottiene il seguente codice sorgente:
<!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>

Ora richiediamo URL e [http://localhost:80] con il nostro client TCP:

Image

  • in [1], ci colleghiamo alla porta 80 del server localhost. È qui che opera il server web di Laragon;

Ora digitiamo i comandi che abbiamo scoperto nel paragrafo precedente:

Image

  • su [1], il comando [GET]. Richiediamo la radice / del server web;
  • in [2], il comando [Host];
  • questi sono gli unici due comandi indispensabili. Per gli altri comandi, il server web utilizzerà i valori predefiniti;
  • in [3], la riga vuota che deve concludere i comandi del client;
  • sotto la riga 3, segue la risposta del server web;
  • da [4] fino alla riga vuota [5] si trovano le intestazioni HTTP della risposta del server;
  • dopo la riga [5] segue il documento HTML richiesto [6];

Digitiamo [quit] per chiudere il client e carichiamo il file di log [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>]
  • righe 11-79: il documento HTML ricevuto. Nell’esempio precedente, Firefox aveva ricevuto lo stesso;

Ora disponiamo delle basi per programmare un client TCP che richiederebbe un URL.

16.4.3. Esempio 3

Image

Lo script [http-01.php] è un client HTTP configurato dal file jSON [config-http-01.json]. Il contenuto di quest’ultimo è il seguente:

{
    "localhost": {
        "port": 80,
        "GET": "/",
        "Host": "localhost:80",
        "User-Agent": "client PHP",
        "Accept": "text/HTML",
        "Accept-Language": "fr",
        "endOfLine":"\r\n"
    }
}
  • riga 2: il nome del computer che ospita il server web da raggiungere;
  • riga 3: la porta su cui opera tale server web;
  • riga 4: l’URL del documento desiderato;
  • riga 5: il computer di destinazione nella forma computer:porta;
  • riga 6: l’identificativo del client HTTP: è possibile inserire qualsiasi valore;
  • riga 7: il tipo di documento accettato dal client, in questo caso testo HTML;
  • riga 8: la lingua desiderata per il documento richiesto;
  • riga 9: il carattere di fine riga per i comandi inviati dal client: infatti può variare a seconda che il server sia su una macchina Unix (\n) o Windows (\r\n);

Lo script [http-01.php] è il seguente:


<?php

// rigoroso rispetto dei tipi dichiarati dei parametri delle funzioni
declare (strict_types=1);
//
// gestione degli errori
// error_reporting(E_ALL & E_STRICT);
// ini_set("display_errors", "on");
//
// costanti
const CONFIG_FILE_NAME = "config-http-01.json";
//
// si recupera la configurazione
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// ottenere il testo HTML dai URL del file di configurazione
foreach ($config as $site => $protocole) {
  // lettura della pagina indice del sito $ite
  $résultat = getURL($site, $protocole);
  // visualizzazione del risultato
  print "$résultat\n";
}//per
// fine
exit;

//-----------------------------------------------------------------------
function getURL(string $site, array $protocole, $suivi = TRUE): string {
  // legge il file URL $site["GET"] e lo salva nel file $site.HTML
  // il dialogo client/server avviene secondo il protocollo $protocole
  //
  // apertura di una connessione sulla porta di $site
  $erreurNumber = 0;
  $erreur = "";
  $connexion = fsockopen($site, $protocole["port"], $erreurNumber, $erreur);
  // ritorno in caso di errore
  if ($connexion === FALSE) {
    return "Echec de la connexion au site (" . $site . " ," . $protocole["port"] . " : $erreur";
  }
  // $connexion rappresenta un flusso di comunicazione bidirezionale
  // tra il client (questo programma) e il server web contattato
  // questo canale viene utilizzato per lo scambio di comandi e informazioni
  // il protocollo di comunicazione è HTTP
  //
  // creazione del file $site.HTML
  $HTML = fopen("output/$site.HTML", "w");
  if ($HTML === FALSE) {
    // Chiusura della connessione client/server
    fclose($connexion);
    // risposta di errore
    return "Erreur lors de la création du fichier $site.HTML";
  }
  // il client avvierà la comunicazione HTTP con il server
  if ($suivi) {
    print "Client : début de la communication avec le serveur [$site] ----------------------------\n";
  }
  // a seconda dei server, le righe del client devono terminare con \n o \r\n
  $endOfLine = $protocole["endOfLine"];
  // per semplificare, non vengono verificati i casi di errore nella comunicazione client/server
  // il client invia il comando GET per richiedere il $protocolo["GET"]
  // sintassi GET URL HTTP/1.1
  $commande = "GET " . $protocole["GET"] . " HTTP/1.1$endOfLine";
  // monitoraggio?
  if ($suivi) {
    print "--> $commande";
  }
  // si invia il comando al server
  fputs($connexion, $commande);
  // invio delle altre intestazioni HTTP
  foreach ($protocole as $verb => $value) {
    if ($verb !== "GET" && $verb != "port"" && $verb !="endOfLine") {
      // si costruisce il comando
      $commande = "$verb: $value$endOfLine";
      // seguito?
      if ($suivi) {
        print "--> $commande";
      }
      // si invia il comando al server
      fputs($connexion, $commande);
    }
  }
  // le intestazioni (header) del protocollo HTTP devono terminare con una riga vuota
  fputs($connexion, $endOfLine);
  //
  // il server risponderà ora sul canale $connexion. Invierà tutti
  // i propri dati e poi chiuderà il canale. Il client legge quindi tutto ciò che arriva da $connexion
  // fino alla chiusura del canale
  //
  // si leggono innanzitutto le intestazioni HTTP inviate dal server
  // anche queste terminano con una riga vuota
  if ($suivi) {
    print "Réponse du serveur [$site] ----------------------------\n";
  }
  $fini = FALSE;
  while (!$fini && $ligne = fgets($connexion, 1000)) {
    // c'è una riga vuota?
    $champs = [];
    preg_match("/^(.*?)\s+$/", $ligne, $champs);
    if ($champs[1] !== "") {
      if ($suivi) {
        // si visualizza l'intestazione HTTP
        print "<-- " . $champs[1] . "\n";
      }
    } else {
      // era la riga vuota - le intestazioni HTTP sono terminate
      $fini = TRUE;
    }
  }
  // si legge il documento HTML che seguirà la riga vuota
  while ($ligne = fgets($connexion, 1000)) {
    // la riga viene memorizzata nel file HTML del sito
    fputs($HTML, $ligne);
  }
  // il server ha chiuso la connessione -  il client la chiude a sua volta
  fclose($connexion);
  // chiusura del file $HTML
  fclose($HTML);
  // ritorno
  return "Fin de la communication avec le site [$site]. Vérifiez le fichier [$site.HTML]";
}

Commenti sul codice:

  • riga 14: il file di configurazione viene utilizzato per creare un dizionario:
    • le chiavi del dizionario sono i server web da interrogare;
    • i valori definiscono il protocollo HTTP da seguire;
  • righe 16-21: si esegue un ciclo sull'elenco dei server web presenti nella configurazione;
  • riga 26: la funzione getURL($site,$protocole,$suivi) richiede un documento dal sito web $site e lo salva nel file di testo $site.HTML.Par: per impostazione predefinita, le comunicazioni client/server vengono registrate nella console ($suivi=TRUE);
  • riga 33: la funzione fsockopen($site,$port,$errNumber,$erreur) consente di creare una connessione con un servizio TCP / IP in esecuzione sulla porta $port del computer $site. Se la connessione non va a buon fine, [$errNumber] è un codice di errore e [$erreur] il messaggio di errore associato. Una volta aperta la connessione client/server, numerosi servizi TCP / IP scambiano righe di testo. È il caso, in questo contesto, del protocollo HTTP (HyperText Transfer Protocol). Il flusso proveniente dal server e diretto al client può quindi essere trattato come un file di testo letto con [fgets]. Lo stesso vale per il flusso proveniente dal client e diretto al server, che può essere scritto con [fputs];
  • righe 44-50: creazione del file [$site.HTML] in cui verrà salvato il documento HTML ricevuto;
  • riga 60: il primo comando del client deve essere il comando [GET URL HTTP/1.1];
  • riga 66: la funzione fputs consente al cliente di inviare dati al server. In questo caso, la riga di testo inviata ha il seguente significato: «Desidero (GET) la pagina [URL] del sito web a cui sono connesso. Sto utilizzando il protocollo HTTP versione 1.1";
  • righe 68-79: si inviano le altre righe del protocollo HTTP [Host, User-Agent, Accept, Accept-Language]. Il loro ordine non ha importanza;
  • riga 81: si invia una riga vuota al server per indicare che il client ha terminato di inviare le proprie intestazioni HTTP e che ora è in attesa del documento richiesto;
  • righe 92-106: il server invierà innanzitutto una serie di intestazioni HTTP che forniranno varie informazioni sul documento richiesto. Queste intestazioni terminano con una riga vuota;
  • riga 93: viene letta una riga inviata dal server con la funzione PHP [fgets];
  • riga 96: si recupera il corpo della riga senza gli spazi (spazi bianchi, carattere di fine riga) presenti alla fine della riga;
  • riga 97: si verifica se è stata recuperata la riga vuota che segna la fine delle intestazioni HTTP inviate dal server;
  • righe 98-101: se ci si trova in modalità [suivi], l’intestazione HTTP ricevuta viene visualizzata sulla console;
  • righe 108-111: le righe di testo della risposta del server possono essere lette riga per riga con un ciclo while e salvate nel file di testo [output/$site.HTML]. Quando il server web ha inviato l’intera pagina richiesta, chiude la connessione con il client. Dal lato client, ciò verrà rilevato come fine del file;

Risultati:

La console visualizza i seguenti log:


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]

Nel nostro esempio, il file [output/localhost.HTML] ricevuto è il seguente:


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

Abbiamo ottenuto lo stesso documento che avremmo ottenuto con il browser Firefox.

16.4.4. Esempio 4

In questo esempio, dimostreremo che il client HTTP che abbiamo scritto non è sufficiente. Modifichiamo il file di configurazione [config-http-01.json] nel modo seguente:

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

In questo caso, richiederemo il client URL [http://tahe.developpez.com:443/]. La porta 443 del server [tahe.developpez.com] è una porta utilizzata per il protocollo HTTP sicuro denominato HTTPS. In questo protocollo, il dialogo client/server inizia con uno scambio di informazioni che rendono sicura la connessione. Il client deve quindi utilizzare il protocollo [HTTPS] e non il protocollo [HTTP], cosa che il nostro client non fa.

Con questo file di configurazione, i risultati della console sono i seguenti:


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]
  • riga 8: il server [tahe.developpez.com] ha risposto che la richiesta del client era errata;

Il contenuto del file [output/tahe.developpez.com.HTML] è quindi il seguente:


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

Il server indica chiaramente che non è stato utilizzato il protocollo corretto.

Utilizziamo ora il seguente file di configurazione:

{
    "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"
    }
}

I risultati visualizzati in console sono quindi i seguenti:


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]
  • la riga 11 indica che il server invia il documento a pezzi;

Ciò si traduce nella presenza di numeri nel flusso inviato al client: ogni numero indica al client il numero di caratteri della porzione successiva inviata dal server. Ecco come appare nel file [output/sergetahe.com.HTML]:

Image

  • in [1] e [2], la dimensione in esadecimale delle parti 1 e 2 del documento;

Un client HTTP corretto non dovrebbe lasciare questi numeri nel documento HTML finale.

Ecco un altro esempio:

{
    "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"
    }
}

È simile all’esempio precedente, ma il URL richiesto alla riga 4 non presenta il carattere / a conclusione. Non si tratta dello stesso URL. L’esecuzione del client HTTP restituisce quindi i seguenti risultati in console:


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/corsi-tutorial-di-programmazione/
<-- 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]
  • la riga 8 indica che il documento richiesto è cambiato da URL. Il nuovo URL è riportato alla riga 13. Si noti questa volta il carattere / che conclude il nuovo URL;

Il file [output/serge.tahe.com.HTML] è quindi il seguente:


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

Un client HTTP dovrebbe essere in grado di seguire i reindirizzamenti. In questo caso, dovrebbe richiedere automaticamente il nuovo URL [http://sergetahe.com/cours-tutoriels-de-programmation/].

16.4.5. Esempio 5

Gli esempi precedenti ci hanno dimostrato che il nostro client HTTP era insufficiente. Presenteremo ora uno strumento denominato [curl] che consente di recuperare documenti web gestendo le difficoltà menzionate: protocollo https, documento inviato in parti, reindirizzamenti… Lo strumento [curl] è stato installato con Laragon:

Image

Apriamo un terminale Laragon [1]:

Image

Nel terminale digitiamo il seguente comando:

Image

  • in [1], il tipo di console;
  • in [2], la cartella corrente. Questa cartella è particolare: è qui che il server Apache di Laragon va a prelevare i documenti che gli vengono richiesti. Eviteremo quindi di intasare questa cartella;
  • in [3], il comando digitato;

È possibile che il comando [curl --help] generi un errore. La causa più probabile è che non si disponga del tipo corretto di terminale. In tal caso, aprire un altro terminale con i comandi [4-6];

Il comando [curl --help] visualizza tutte le opzioni di configurazione di [curl]. Ce ne sono diverse decine. Ne useremo pochissime. Per richiedere un URL basta digitare il comando [curl URL]. Questo comando visualizzerà sulla console il documento richiesto. Se si desidera inoltre visualizzare gli scambi HTTP tra il client e il server, si digiterà [curl --verbose URL]. Infine, per salvare il documento HTML richiesto in un file, si digiterà [curl --verbose --output fichier URL].

Per evitare di ingombrare la cartella [www] di Laragon, spostiamoci in un’altra posizione del file system:

Image

  • in [1], ci si sposta nella cartella [c:\temp]. Se questa cartella non esiste, è possibile crearla o sceglierne un’altra;
  • in [2], creiamo una cartella denominata [curl];
  • in [3], ci si posiziona su di essa;
  • in [4], si visualizza il contenuto. È vuoto;

Assicurarsi che il server Apache di Laragon sia in esecuzione e, con [curl], richiedere URL e [http://localhost/] con il comando [curl –verbose –output localhost.HTML http://localhost/]. Si ottengono i seguenti risultati:


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 per mantenere intatto l'host localhost                                                   
  • righe 8-12: righe inviate da [curl] al server [localhost]. Si riconosce il protocollo HTTP;
  • righe 13-19: righe inviate in risposta dal server;
  • riga 13: indica che il documento richiesto è stato ricevuto correttamente;

Il file [localhost.HTML] contiene il documento richiesto. È possibile verificarlo aprendo il file in un editor di testo.

Ora richiediamo il file URL [https://tahe.developpez.com:443/]. Per ottenere questo URL, il client HTTP deve essere in grado di comunicare con HTTPS. È il caso del client [curl].

I risultati della console sono i seguenti:


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 all'host tahe.developpez.com lasciato intatto
  • righe 10-40: gli scambi client/server per proteggere la connessione: questa sarà crittografata;
  • righe 42-45: le intestazioni HTTP inviate dal client [curl] al server;
  • riga 48: il documento richiesto è stato trovato;
  • riga 53: il documento viene inviato in parti;

[curl] gestisce correttamente sia il protocollo sicuro HTTPS sia il fatto che il documento venga inviato in parti. Il documento inviato si troverà qui nel file [tahe.developpez.com.HTML].

Richiediamo ora il codice URL [http://sergetahe.com/cours-tutoriels-de-programmation]. Avevamo visto che per questo URL c'era un reindirizzamento verso URL [http://sergetahe.com/cours-tutoriels-de-programmation/] (con una / alla fine).

I risultati della console sono quindi i seguenti:


c:\Temp\curl
λ curl --verbose --output sergetahe.com.HTML --location http://sergetahe.com/corsi-tutorial-di-programmazione
  % 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/corsi-e-tutorial-di-programmazione/
< 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 per ospitare sergetahe.com lasciato intatto
* Issue another request to this URL: 'http://sergetahe.com/corsi-e-tutorial-di-programmazione/'
* Found bundle for host sergetahe.com: 0x1c88548 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) con host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
> GET /cours-tutoriels-de-programmation/ HTTP/1.1
> Host: sergetahe.com
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 17 May 2019 15:13:04 GMT
< Content-Type: text/HTML; charset=UTF-8
< Transfer-Encoding: chunked
< Server: Apache
< X-Powered-By: PHP/7.0
< Vary: Accept-Encoding
< Set-Cookie: SERVERID68971=2621207|XN7Pg|XN7Pg; path=/
< Cache-control: private
< X-IPLB-Instance: 17095
<
{ [14205 bytes data]
100 43101    0 43101    0     0  78795      0 --:--:-- --:--:-- --:--:--  168k
* Connection #0 verso l'host sergetahe.com lasciato intatto
  • riga 2: si utilizza l’opzione [--location] per indicare che si desidera seguire i reindirizzamenti inviati dal server;
  • riga 13: il server indica che il documento richiesto è cambiato in URL;
  • riga 18: indica il nuovo URL del documento richiesto;
  • riga 27: [curl] invia una nuova richiesta, questa volta all'indirizzo URL;
  • riga 33: viene utilizzato il nuovo URL;
  • riga 38: il server risponde di aver trovato il documento richiesto;
  • riga 41: lo invia a pezzi;

Il documento richiesto si troverà nel file [sergetahe.com.HTML].

16.4.6. Esempio 6

PHP possiede un'estensione denominata [libcurl] che consente di utilizzare le funzionalità dello strumento [curl] in un programma PHP. Occorre innanzitutto assicurarsi che questa estensione sia attivata nel file [php.ini] descritto nel paragrafo «Collegamento»:

Image

Assicurarsi che la riga 889 sopra riportata sia commentata.

Scriveremo uno script [http-02.php] che utilizzerà il seguente file di configurazione 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"
    }
}

Ogni elemento del dizionario [clé, valeur] ha la seguente struttura:

  • clé: il nome di un server web;
  • valeur è un dizionario con le seguenti chiavi:
    • timeout: tempo massimo di attesa per la risposta del server. Trascorso tale tempo, il client si disconnetterà;
    • url: URL del documento richiesto;

Il codice dello script [http-02.php] è il seguente:


<?php

// rigoroso rispetto dei tipi dichiarati dei parametri delle funzioni
declare (strict_types=1);
//
// gestione degli errori
//error_reporting(E_ALL & E_STRICT);
//ini_set("display_errors", "on");
//
// costanti
const CONFIG_FILE_NAME = "config-http-02.json";
//
// si recupera la configurazione
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);

// ottenere il testo HTML dal file di configurazione URL
foreach ($config as $site => $infos) {
  // lettura di URL dal sito $ite
  $résultat = getUrl($site, $infos["url"], $infos["timeout"]);
  // visualizzazione dei risultati
  print "$résultat\n";
}//per
// fine
exit;

//-----------------------------------------------------------------------
function getUrl(string $site, string $url, int $timeout, $suivi = TRUE): string {
  // legge il file URL $url e lo salva nel file output/$site.HTML
  //
  // seguito
  print "Client : début de la communication avec le serveur [$site] ----------------------------\n";

  // Inizializzazione di una sessione cURL
  $curl = curl_init($url);
  if ($curl === FALSE) {
    // si è verificato un errore
    return "Erreur lors de l'initialisation de la session cURL pour le site [$site]";
  }
  // opzioni di curl
  $options = [
    // modalità verbosa
    CURLOPT_VERBOSE => true,
    // nuova connessione - nessuna cache
    CURLOPT_FRESH_CONNECT => true,
    // timeout della richiesta (in secondi)
    CURLOPT_TIMEOUT => $timeout,
    CURLOPT_CONNECTTIMEOUT => $timeout,
    // non verificare la validità dei certificati SSL
    CURLOPT_SSL_VERIFYPEER => false,
    // segui i reindirizzamenti
    CURLOPT_FOLLOWLOCATION => true,
    // recupero del documento richiesto sotto forma di stringa di caratteri
    CURLOPT_RETURNTRANSFER => true
  ];

  // configurazione di curl
  curl_setopt_array($curl, $options);
  // Esecuzione della richiesta
  $page_content = curl_exec($curl);
  // Chiusura della sessione cURL
  curl_close($curl);

  // elaborazione del risultato
  if ($page_content !== FALSE) {
    // Salvataggio del risultato in $site.HTML
    $result = file_put_contents("output/$site.HTML", $page_content);
    if ($result === FALSE) {
      // Ritorno con errore
      return "Erreur lors de la création du fichier [output/$site.HTML]";
    }
    // risposta di successo
    return "Fin de la communication avec le serveur [$site]. Vérifiez le fichier [output/$site.HTML]";
  } else {
    // si è verificato un errore di comunicazione
    return "Erreur de communication avec le serveur [$site]";
  }
}

Commenti

  • riga 14: si utilizza il file di configurazione per creare il dizionario [$config];
  • righe 17-22: si esegue un ciclo sull’elenco dei siti presenti nella configurazione;
  • riga 19: per ciascuno dei siti, si chiama la funzione [getUrl] che scaricherà ilURL $infos[«url»] con un timeout $infos[«timeout»];
  • riga 34: si avvia una sessione [curl]. [curl_init] non si connette ancora al server web. Restituisce una risorsa [$curl] che fungerà da parametro per tutte le funzioni [curl] successive;
  • righe 35-38: se l’inizializzazione della sessione [curl] fallisce, la funzione [curl_init] restituisce il valore booleano FALSE;
  • righe 40-54: il dizionario [$options] configurerà la connessione [curl] al server;
  • riga 57: le opzioni della connessione vengono trasmesse alla risorsa [$curl];
  • riga 59: viene richiesta la connessione a URL con le opzioni definite. A causa dell’opzione [CURLOPT_RETURNTRANSFER => true], la funzione [curl_exec] restituisce come risultato il documento inviato dal server sotto forma di stringa di caratteri. La funzione [curl_exec] restituisce il valore booleano FALSE in caso di errore nella connessione;
  • riga 64: si analizza il risultato di [curl_exec];
  • riga 66: la pagina ricevuta viene salvata in un file locale;
  • righe 69, 72, 75: si restituisce il risultato della funzione [getUrl];

Quando si esegue lo script [http-02.php], si ottengono i seguenti risultati in console:


* 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/corsi-tutorial-di-programmazione
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 per ospitare sergetahe.com lasciato intatto
* Issue another request to this URL: 'http://sergetahe.com/corsi-e-tutorial-di-programmazione'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) con host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation HTTP/1.1
Host: sergetahe.com
Accept: */*

< HTTP/1.1 301 Moved Permanently
< Date: Sat, 18 May 2019 08:46:38 GMT
< Content-Type: text/HTML; charset=iso-8859-1
< Content-Length: 262
< Server: Apache
< Location: http://sergetahe.com/corsi-e-tutorial-di-programmazione/
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< Cache-control: private
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 all'host sergetahe.com lasciato intatto
* Issue another request to this URL: 'http://sergetahe.com/corsi-e-tutorial-di-programmazione/'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) con host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation/ HTTP/1.1
Host: sergetahe.com
Accept: */*

< HTTP/1.1 200 OK
< Date: Sat, 18 May 2019 08:46:39 GMT
< Content-Type: text/HTML; charset=UTF-8
< Transfer-Encoding: chunked
< Server: Apache
< X-Powered-By: PHP/7.0
< Link: <http://sergetahe.com/corsi-e-tutorial-di-programmazione/wp-json/>; rel="https://api.w.org/"
< Link: <http://sergetahe.com/corsi-e-tutorial-di-programmazione/>; 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 per ospitare sergetahe.com lasciato intatto
* 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 per ospitare tahe.developpez.com lasciato intatto
* 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 per ospitare www.polytech-angers.fr lasciato intatto
* 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) con host www.polytech-angers.fr
* Connected to www.polytech-angers.fr (193.49.144.41) port 80 (#0)
> GET /fr/index.HTML HTTP/1.1
Host: www.polytech-angers.fr
Accept: */*

< HTTP/1.1 200
< Date: Sat, 18 May 2019 08:46:46 GMT
< Server: Apache/2.4.29 (Ubuntu)
< X-Cocoon-Version: 2.1.13-dev
< Accept-Ranges: bytes
< Last-Modified: Sat, 18 May 2019 08:01:36 GMT
< Content-Type: text/HTML; charset=UTF-8
< Content-Length: 47372
< Vary: Accept-Encoding
< Cache-Control: max-age=1
< Expires: Sat, 18 May 2019 08:46:47 GMT
< Content-Language: fr
<
* Connection #0 verso l'host www.polytech-angers.fr lasciato intatto
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 per l'host localhost lasciato intatto

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

Commenti

  • si ottengono gli stessi scambi che con lo strumento [curl];
  • in verde, i log dello script;
  • in blu, i comandi inviati al server;
  • in giallo, i comandi ricevuti in risposta dal client;

16.4.7. Conclusione

In questo paragrafo abbiamo scoperto il protocollo HTTP e abbiamo scritto uno script [http-02.php] in grado di scaricare un URL dal web.

16.5. Il protocollo SMTP (Simple Mail Transfer Protocol)

16.5.1. Introduzione

Image

In questo capitolo:

  • [Serveur B] sarà un server SMTP locale che installeremo;
  • [Client A] sarà un client SMTP in diverse forme:
    • il client [RawTcpClient] per scoprire il protocollo SMTP;
    • uno script PHP che riproduce il protocollo SMTP del client [RawTcpClient];
    • uno script PHP che utilizza la libreria [SwiftMailServer] per l'invio di ogni tipo di e-mail;

16.5.2. Creazione di un indirizzo [gmail]

Per eseguire i nostri test SMTP, avremo bisogno di un indirizzo e-mail a cui scrivere. A tal fine creeremo un indirizzo su Gmail:

Image

  • in [5], creiamo l’utente [php7parlexemple] (scegliete un altro nome);
  • per [6], la password sarà [PHP7parlexemple] (scegliete un altro nome);
  • in [7], confermiamo queste informazioni;

Image

  • compilare i campi [9-10] e confermare (11);
  • accettare i termini di utilizzo di Google (12-13) e confermare (14);

Image

  • in [15], la posta in arrivo (Inbox) dell’utente [PHP7] (16);
  • in [17], questo utente ha una posta in arrivo vuota;
  • in [18-19], accedi all’account Google dell’utente [php7parlexemple@gmail.com]. Configureremo la sicurezza dell’account;

Image

  • in [21], autorizza applicazioni diverse da quelle di Google a utilizzare l’account [php7parlexemple]. Se non lo facciamo, il nostro server di posta locale [hMailServer] non potrà comunicare con il server SMTP di Gmail;

Image

16.5.3. Installazione di un server SMTP

Ai fini dei nostri test, installeremo il server di posta [hMailServer], che è al tempo stesso un server SMTP che consente di inviare e-mail, un server POP3 (Post Office Protocol) che consente di leggere le e-mail archiviate sul server, e un server IMAP (Internet Message Access Protocol) che a sua volta permette di leggere le e-mail archiviate sul server, ma offre funzionalità aggiuntive. In particolare, consente di gestire l’archiviazione delle e-mail sul server.

Il server di posta [hMailServer] è disponibile su URL e [https://www.hmailserver.com/] (maggio 2019).

Image

Durante l’installazione, vi verranno richieste alcune informazioni:

Image

  • in [1-2], selezionare sia il server di posta che gli strumenti per la sua amministrazione;
  • durante l’installazione vi verrà richiesta la password dell’amministratore: prendetene nota, poiché vi servirà;

[hMailServer] si installa come servizio Windows avviato automaticamente all’avvio del computer. È preferibile scegliere l’avvio manuale:

  • in [3], digitare [services] nella casella di immissione della barra di stato;

Image

  • in [4-8], si imposta il servizio in modalità [manuel] (6), quindi lo si avvia (7);

Una volta avviato, il server [hMailServer] deve essere configurato. Il server è stato installato con un programma di amministrazione [hMailServer Administrator]:

Image

  • in [2], nell’area di immissione della barra di stato, digitare [hmailserver];
  • in [3], avviare l’amministratore;
  • in [4], connettere l'amministratore al server [hMailServer];
  • in [5], digitare la password inserita durante l'installazione di [hMailServer];

Image

Creeremo un account utente:

  • fare clic con il tasto destro su [Accounts] (7), quindi (8) per aggiungere un nuovo utente;
  • nella scheda [General] (9), definiamo un utente [guest] (10) con la password [guest] (11). Avrà l'indirizzo e-mail [guest@localhost] (10);
  • in [12], l’utente [guest] è attivato;

Image

Image

  • in [15], si configura il protocollo SMTP del server di posta;
  • in [16], si configura la distribuzione delle e-mail;
  • in [17], la configurazione della distribuzione delle e-mail destinate al computer host (localhost);
  • in [18], il nome del computer locale (localhost). Lo script riportato nel paragrafo «link» consente di ottenere questo nome;
  • in [19], si configura un server di inoltro SMTP: si tratta del server che si occuperà della distribuzione delle e-mail non destinate al computer locale (localhost);
  • in [20], il server SMTP di Gmail. Utilizziamo Gmail poiché vi abbiamo creato un account nel paragrafo "link";
  • in [21], la porta SMTP di Gmail;
  • In [22], il servizio SMTP di Gmail è un servizio protetto: per accedervi è necessario un account Gmail;
  • in [23], l'utente [php7parlexemple] creato nel paragrafo "link";
  • in [24], la password di questo utente: [PHP7parlexemple], creata nel paragrafo "link";
  • in [25], si indica il tipo di protocollo di sicurezza utilizzato da Gmail;

Image

  • in [27] la porta del servizio SMTP;
  • in [28], questo servizio non richiede autenticazione;
  • in [30], inserite il messaggio di benvenuto che il server SMTP invierà ai propri clienti;

16.5.4. Il protocollo SMTP

Image

Scopriremo il protocollo SMTP con il seguente ambiente:

  • il client A sarà il client generico TCP [RawTcpClient];
  • il server B sarà il server di posta [hMailServer];
  • il client A chiederà al server B di recapitare un’e-mail all’utente [php7parlexemple@gmail.com];
  • verificheremo che tale utente abbia effettivamente ricevuto l’e-mail inviata;

Avviamo il client nel modo seguente:

Image

  • in [1], ci si connette alla porta 25 della macchina locale, dove opera il servizio SMTP di [hMailServer]. L’argomento [--quit bye] indica che l’utente uscirà dal programma digitando il comando [bye]. Senza questo argomento, il comando di chiusura del programma è [quit]. Tuttavia, [quit] è anche un comando del protocollo SMTP. Dobbiamo quindi evitare questa ambiguità;
  • in [2], il client è correttamente connesso;
  • in [3], il client attende i comandi digitati dalla tastiera;
  • in [4], il server gli invia il messaggio di benvenuto;

Image

  • in [5], il client invia il comando [EHLO nom-de-la-machine-client]. Il server risponde con una serie di messaggi della forma [250-xx] (6). Il codice [250] indica che il comando inviato dal client è andato a buon fine;
  • in [7], il client indica il mittente del messaggio, in questo caso [guest@localhost]. Questo utente deve esistere sul server di posta [hMailServer]. In questo caso è così, poiché abbiamo creato questo utente in precedenza;
  • in [8], la risposta del server;
  • in [9], si indica il destinatario del messaggio, in questo caso l’utente Gmail [php7parlexemple@gmail.com];
  • in [10], la risposta del server;
  • in [11], il comando [DATA] indica al server che il client sta per inviare il contenuto del messaggio;
  • in [12], la risposta del server;
  • nel formato [13-16], il cliente deve inviare un elenco di righe di testo che termina con una riga contenente un solo punto. Il messaggio può contenere righe [Subject :, From :, To :] (13) per definire rispettivamente l’oggetto del messaggio, il mittente e il destinatario;
  • in [14], le intestazioni precedenti devono essere seguite da una riga vuota;
  • in [15], il testo del messaggio;
  • in [16], la riga contenente un solo punto che indica la fine del messaggio;
  • in [17], una volta che il server ha ricevuto la riga contenente un solo punto, mette il messaggio in coda;
  • in [18], il client comunica al server di aver terminato;
  • in [19], la risposta del server;
  • in [20], si nota che il server ha chiuso la connessione che lo collegava al client;

Ora verifichiamo che l’utente [php7parlexemple@gmail.com] abbia effettivamente ricevuto il messaggio:

Image

  • in [2], si vede che l’utente [php7parlexemple@gmail.com] ha effettivamente ricevuto il messaggio;

Image

Image

Image

  • in [7], il mittente dell’e-mail. Si nota che non si tratta di [guest@localhost]. Ciò è dovuto al fatto che è stato il server di inoltro definito nella configurazione di [hmailServer] a consegnare il messaggio. Ora, questo server di inoltro è [smtp.gmail.com], associato alle credenziali dell’utente Gmail [php7parlexemple@gmail.com]. Qualsiasi email proveniente da [hMailServer] sembrerà provenire dall’utente [php7parlexemple@gmail.com]. Non è quello che volevamo in questo caso, ma se non si utilizza questo server di inoltro, il servizio SMTP di Gmail rifiuta le email inviate da [hMailServer] perché il SMTP di Gmail richiede un'autenticazione che [hMailServer] non invia. Probabilmente esiste un modo per aggirare questo problema, ma non l’ho trovato;
  • in [8], si vede che l’e-mail è stata ricevuta dal computer [DESKTOP-528I5CU] che ospita il server di posta [hMailServer];
  • in [9], il mittente del messaggio. Si vede che non è [guest@localhost];
  • in [10], il mittente originale del messaggio. Questa volta si tratta effettivamente di [guest@localhost];
  • in [11], l’oggetto;
  • in [12], il destinatario;
  • in [13], il messaggio;

Alla fine, il nostro client [RawTcpClient] è riuscito a inviare il messaggio anche se si è verificato un problema con il mittente. Abbiamo le basi per creare un client SMTP scritto in PHP.

16.5.5. Un client SMTP di base scritto in PHP

Ripeteremo in PHP ciò che abbiamo appreso in precedenza dal protocollo SMTP.

Image

Lo script [smtp-01.php] è configurato dal seguente file 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] è un array in cui ciascuno degli elementi è un dizionario di tipo [nom=>infos]. Il valore [infos] è a sua volta un dizionario con le seguenti chiavi e valori:

  • [smtp-server]: il nome del server SMTP da utilizzare;
  • [smtp-port]: il numero di porta del servizio SMTP;
  • [from]: il mittente del messaggio;
  • [to]: il destinatario del messaggio;
  • [subject]: l'oggetto del messaggio;
  • [message]: il messaggio da inviare;
  • Il primo elemento utilizza il server SMTP [localhost] per inviare un’e-mail a un utente di [localhost];
  • il secondo elemento utilizza i server SMTP e [localhost] per inviare un’e-mail a un utente di [Gmail];
  • il terzo elemento utilizza il server SMTP [Gmail] per inviare un'e-mail a un utente di [Gmail];

Il codice [smtp-01.php] del client SMTP è il seguente:


<?php

// client SMTP (protocollo di trasferimento SendMail) che consente di inviare un messaggio
// protocollo di comunicazione client-server SMTP
// -> il client si connette alla porta 25 del server SMTP
// <- il server gli invia un messaggio di benvenuto
// -> il client invia il comando EHLO con il nome del proprio computer
// <- il server risponde con OK oppure no
// -> il client invia il comando MAIL FROM: <mittente>
// <- il server risponde con OK oppure no
// -> il client invia il comando RCPT TO: <destinatario>
// <- il server risponde con OK oppure no
// -> il client invia il comando DATA
// <- il server risponde con OK oppure no
// -> il client invia tutte le righe del proprio messaggio e termina con una riga contenente il
// unico carattere.
// <- il server risponde con OK oppure no
// -> il client invia il comando QUIT
// <- il server risponde con OK oppure no
// le risposte del server hanno il formato xxx testo, dove xxx è un numero a 3 cifre. Qualsiasi
// numero xxx >=500 segnala un errore.
// La risposta può contenere più righe, tutte inizianti con xxx tranne l’ultima
// della forma xxx(spazio)
// le righe di testo scambiate devono terminare con i caratteri RC(#13) e LF(#10)
//
//  client SMTP (SendMail Transfer Protocol) che consente di inviare un messaggio
//
// gestione degli errori
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// rigoroso rispetto dei tipi dichiarati dei parametri delle funzioni
declare (strict_types=1);
//
// i parametri per l'invio della posta
const CONFIG_FILE_NAME = "config-smtp-01.json";

// si recupera la configurazione
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);

// invio delle e-mail
foreach ($mails as $name => $infos) {
  // tracciamento
  print "Envoi du mail [$name]\n";
  // invio della corrispondenza
  $résultat = sendmail($name, $infos, TRUE);
  // visualizzazione del risultato
  print "$résultat\n";
}//per
// fine
exit;

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

function sendmail(string $name, array $infos, bool $verbose = TRUE): string {
  // invia il messaggio [$name,$infos]. Se $verbose=TRUE    , si tiene traccia degli scambi client-server
  // si recupera il nome del client
  $client = gethostbyaddr(gethostbyname(""));
  // apertura di una connessione con il server SMTP
  $connexion = fsockopen($infos["smtp-server"], (int) $infos["smtp-port"]);
  // ritorno in caso di errore
  if ($connexion === FALSE) {
    return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
  }
  // $connexion rappresenta un flusso di comunicazione bidirezionale
  // tra il client (questo programma) e il server SMTP contattato
  // questo canale viene utilizzato per lo scambio di comandi e informazioni
  // dopo la connessione, il server invia un messaggio di benvenuto che viene letto
  $erreur = sendCommand($connexion, "", $verbose, TRUE);
  if ($erreur !== "") {
    // chiusura della connessione
    fclose($connexion);
    // ritorno
    return $erreur;
  }
  // comando EHLO
  $erreur = sendCommand($connexion, "EHLO $client", $verbose, TRUE);
  if ($erreur !== "") {
    // chiusura della connessione
    fclose($connexion);
    // ritorno
    return $erreur;
  }
  // comando MAIL FROM:
  $erreur = sendCommand($connexion, sprintf("MAIL FROM: <%s>", $infos["from"]), $verbose, TRUE);
  if ($erreur !== "") {
    // chiusura della connessione
    fclose($connexion);
    // ritorno
    return $erreur;
  }
  // comando RCPT TO:
  $erreur = sendCommand($connexion, sprintf("RCPT TO: <%s>", $infos["to"]), $verbose, TRUE);
  if ($erreur !== "") {
    // chiusura della connessione
    fclose($connexion);
    // ritorno
    return $erreur;
  }
  // comando DATA  
  $erreur = sendCommand($connexion, "DATA", $verbose, TRUE);
  if ($erreur !== "") {
    // chiusura della connessione
    fclose($connexion);
    // ritorno
    return $erreur;
  }
  // preparazione del messaggio da inviare
  // deve contenere le righe
  // Da: mittente
  // A: destinatario
  // Oggetto:
  // riga vuota
  // Messaggio
  // .
  $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 !== "") {
    // Chiusura della connessione
    fclose($connexion);
    // ritorno
    return $erreur;
  }
  // comando quit
  $erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
  if ($erreur !== "") {
    // chiusura della connessione
    fclose($connexion);
    // ritorno
    return $erreur;
  }
  // fine
  fclose($connexion);
  return "Message envoyé";
}

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

function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF): string {
  // invia $commande nel canale $connexion
  // modalità verbosa se $verbose=1
  // se $withRCLF=1, aggiunge la sequenza RCLF allo scambio
  // dati
  if ($withRCLF) {
    $RCLF = "\r\n";
  } else {
    $RCLF = "";
  }
  // invio comando se $commande non è vuoto
  if ($commande!=="") {
    fputs($connexion, "$commande$RCLF");
    // eventuale eco
    if ($verbose) {
      affiche($commande, 1);
    }
  }//se
  // lettura della risposta
  $réponse = fgets($connexion, 1000);
  // eventuale eco
  if ($verbose) {
    affiche($réponse, 2);
  }
  // recupero codice di errore
  $codeErreur = (int) substr($réponse, 0, 3);
  // ultima riga della risposta?
  while (substr($réponse, 3, 1) === "-") {
    // lettura risposta
    $réponse = fgets($connexion, 1000);
    // eventuale eco
    if ($verbose) {
      affiche($réponse, 2);
    }
  }//while
  // risposta terminata
  // errore restituito dal server?
  if ($codeErreur >= 500) {
    return substr($réponse, 4);
  }
// ritorno senza errori
  return "";
}

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

function affiche($échange, $sens) {
  // visualizza $échange sullo schermo
  // se $sens=1 visualizza -->$echange
  // se $sens=2 visualizza <-- $échange senza gli ultimi 2 caratteri RCLF
  switch ($sens) {
    case 1:
      print "--> [$échange]\n";
      break;
    case 2:
      $L = strlen($échange);
      print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
      break;
  }//switch
}

Commenti

  • riga 39: si utilizza il file di configurazione;
  • riga 42: si esegue un ciclo sugli elementi dell'array [mails]. Ogni elemento è un dizionario [name=>infos], dove [name] è un nome arbitrario e [infos] è un dizionario contenente le informazioni necessarie per l’invio di un’e-mail;
  • riga 46: l’invio dell’e-mail è gestito dalla funzione [sendmail] che accetta tre parametri:
    • $name: il nome assegnato a questo invio;
    • $infos: il dizionario contenente le informazioni necessarie per l’invio;
    • verbose: un valore booleano che indica se le comunicazioni client/server devono essere registrate o meno nella console;
  • riga 46: la funzione [sendmail] restituisce un messaggio di errore vuoto se non si sono verificati errori;
  • riga 56: la funzione [sendmail] invia i vari comandi che un client deve inviare SMTP:
    • righe 77-84: il comando EHLO;
    • righe 85-92: il comando MAIL FROM: ;
    • righe 93-100: l'ordine RCPT TO: ;
    • righe 101-108: il comando DATA;
    • righe 117-124: invio del messaggio (Da, A, Oggetto, testo);
    • righe 125-132: il comando QUIT;
  • riga 140: la funzione [sendCommand] ha il compito di inviare i comandi del client al server SMTP. Accetta quattro parametri:
    • [$connexion]: la connessione che collega il client al server;
    • [$commande]: il comando da inviare;
    • [$verbose]: se TRUE, allora gli scambi client/server vengono registrati nella console;
    • [$withRCLF]: se TRUE, invia il comando terminato dalla sequenza \r\n. Ciò è necessario per tutti i comandi del protocollo SMTP, ma [sendCommand] serve anche a inviare il messaggio. In questo caso non si aggiunge la sequenza \r\n;
  • righe 150-157: il comando viene inviato al server;
  • righe 158-163: lettura della prima riga della risposta. Questa può comprendere più righe. Ogni riga ha la forma XXX-YYY, dove XXX è un codice numerico, tranne l’ultima riga della risposta che ha la forma XXX YYY (assenza del carattere -);
  • righe 167-174: lettura di tutte le righe della risposta;
  • riga 177: se il codice numerico XXX è superiore a 500, allora il server ha restituito un errore;

Risultati

L'esecuzione dello script produce i seguenti risultati in console:


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.
  • righe 1-26: l’utilizzo del server SMTP [hMailServer] per inviare un’e-mail a [guest@localhost] procede correttamente;
  • righe 27-52: l’utilizzo dei server SMTP e [hMailServer] per inviare un’e-mail a [php7parlexemple@gmail.com] avviene correttamente;
  • righe 53-65: l'utilizzo del server SMTP [Gmail] per inviare un'e-mail a [php7parlexemple@gmail.com] non funziona correttamente: alla riga 65, il server SMTP invia un codice di errore 530 con il messaggio di errore. Questo indica che il client SMTP deve prima autenticarsi tramite una connessione sicura. Il nostro client non lo ha fatto e viene quindi rifiutato;

16.5.6. Un secondo client SMTP scrive utilizzando la libreria [SwiftMailer]

Il client precedente presenta almeno due carenze:

  • non è in grado di utilizzare una connessione sicura se il server la richiede;
  • non è in grado di allegare file al messaggio;

Nel nostro nuovo script utilizzeremo la libreria [SwiftMailer] [https://swiftmailer.symfony.com/] (maggio 2019). La procedura di installazione di [SwiftMailer] è descritta in URL [https://swiftmailer.symfony.com/docs/introduction.HTML] (maggio 2019).

Per prima cosa, avviare Laragon:

Image

  • in [1], aprite un terminale;

Image

  • in [3], verificate di trovarvi nella cartella [<laragon>/www] dove <laragon> è la cartella di installazione di Laragon;
  • in [3], digitate il comando indicato (maggio 2019). Verificate in URL e [https://swiftmailer.symfony.com/docs/introduction.HTML] il comando esatto;
  • in [4], viene indicato che non è stata effettuata alcuna installazione né alcun aggiornamento. Ciò è dovuto al fatto che la libreria era già stata installata su questo computer;
  • in [5], la cartella di installazione di [swiftmailer] [6];
  • in [7], un file di cui avremo bisogno nel nostro script;

Fatto ciò, verificate che la cartella [<laragon>/www/vendor] [5] si trovi effettivamente nel ramo [Include Path] di NetBeans (cfr. paragrafo «link»).

Infine, la libreria [SwiftMailer] richiede che l’estensione PHP [mbstring] sia attiva. A tal fine, si verifica il file [php.ini] (cfr. paragrafo «link»):

Image

Lo script [smtp-02.php] utilizzerà il seguente file di configurazione 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"
        ]
    }
}

Sono presenti le stesse voci del file [config-smtp-01.json] con due voci aggiuntive:

  • [tls]: in TRUE indica che è necessario utilizzare una connessione sicura con il server SMTP. Nel caso in cui [tls] sia uguale a TRUE, è necessario aggiungere due voci:
    • [user]: il nome utente che autentica la connessione;
    • [password]: la sua password;

Nel nostro esempio, abbiamo utilizzato le credenziali dell’utente [php7parlexemple@gmail.com] per collegarci al server di Gmail. Utilizzate le vostre;

  • [attachments]: indica i nomi dei file da allegare all’e-mail;

Il codice dello script [smtp-02.php] è il seguente:


<?php

// client SMTP (SendMail Transfer Protocol) che consente di inviare un messaggio
//
// gestione degli errori
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dipendenze
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
//
// parametri di invio della posta
const CONFIG_FILE_NAME = "config-smtp-02.json";

// si recupera la configurazione
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);

// invio delle e-mail
foreach ($mails as $name => $infos) {
  // monitoraggio
  print "Envoi du mail [$name]\n";
  // invio della posta
  $résultat = sendmail($name, $infos);
  // visualizzazione del risultato
  print "$résultat\n";
}//for
// fine
exit;

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

function sendmail($name, $infos) {

  // invia $infos[message] al server SMTP $infos[smtp-server] sulla porta $infos[smt-port]
  // se $infos[tls] è vero, verrà utilizzato il supporto TLS
  // l'e-mail viene inviata a nome di $infos[from]
  // al destinatario $infos['to']
  // Il documento $info[attachment] è allegato al messaggio
  // il messaggio ha come oggetto $infos[subject]
  //
  // messaggio in formato HTML
  $messageHTML = str_replace("\n", "<br/>", $infos["message"]);
  try {
    // creazione del messaggio
    $message = (new \Swift_Message())
      // oggetto del messaggio
      ->setSubject($infos["subject"])
      // mittente
      ->setFrom($infos["from"])
      // destinatari con un dizionario (setTo/setCc/setBcc)
      ->setTo($infos["to"])
      // testo del messaggio
      ->setBody($infos["message"])
      // versione HTML
      ->addPart("<b>$messageHTML</b>", 'text/html')
    ;
    // allegati
    foreach ($infos["attachments"] as $attachment) {
      // percorso dell'allegato
      $fileName = __DIR__ . $attachment;
      // si verifica che il file esista
      if (file_exists($fileName)) {
        // si allega il documento al messaggio
        $message->attach(\Swift_Attachment::fromPath($fileName));
      } else {
        // errore
        print "L'attachement [$fileName] n'existe pas\n";
      }
    }
    // protocollo TLS?
    if ($infos["tls"] === "TRUE") {
      // TLS
      $transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"], 'tls'))
        ->setUsername($infos["user"])
        ->setPassword($infos["password"]);
    } else {
      // nessun TLS
      $transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"]));
    }
    // il gestore dell'invio
    $mailer = new \Swift_Mailer($transport);
    // invio del messaggio
    $result = $mailer->send($message);
    // fine
    return "Message [$name] envoyé";
  } catch (\Throwable $ex) {
    // errore
    return "Erreur lors de l'envoi du message [$name] : " . $ex->getMessage();
  }
}

Commenti

  • riga 10: carichiamo il file [autoload.php] presente nella cartella [<lagagon>/www/vendor], dove <laragon> è la cartella di installazione di Laragon. Questo file consentirà di caricare i file di definizione delle classi di [SwiftMailer] sin dal primo utilizzo di tali classi. Ci evita di inserire tanti file [require] quanti sono le classi e le interfacce di SwiftMailer che utilizzeremo;
  • riga 32: la nuova funzione [sendmail] che ha due parametri:
    • [$name], che serve a distinguere i messaggi l’uno dall’altro;
    • [$infos]: le informazioni necessarie per inviare il messaggio al destinatario;
  • riga 42: avremo due versioni del messaggio: una in testo semplice e l’altra in HTML. Qui modifichiamo i caratteri di fine riga nel codice HTML <br/>;
  • righe 45-69: definiamo il messaggio utilizzando la classe [\SwiftMessage];
  • riga 47: il metodo [SwiftMessage→setSubject] serve a impostare l’oggetto del messaggio;
  • riga 49: il metodo [SwiftMessage→setFrom] serve a impostare il mittente del messaggio;
  • riga 51: il metodo [SwiftMessage→setTo] serve a impostare il destinatario del messaggio;
  • riga 53: il metodo [SwiftMessage→setBody] serve a impostare il corpo del messaggio;
  • riga 55: il metodo [SwiftMessage→addPart] serve a specificare diverse versioni del messaggio, in questo caso il messaggio nel formato HTML. Quando il messaggio presenta delle varianti, i client di posta visualizzano la variante preferita dall’utente;
  • righe 58-69: il metodo [SwiftMessage→addAttachment] (64) consente di allegare un file al messaggio;
  • righe 70-79: una volta definito il messaggio da inviare, è necessario specificare come inviarlo. La modalità di trasporto del messaggio è definita dalla classe [\Swift_SmtpTransport]. Occorre fornire almeno due informazioni: il nom e il port del server SMTP. Ce n’è anche una terza: il server SMTP richiede un’autenticazione sicura?
  • righe 73-75: l’istanza [\Swift_SmtpTransport] per una connessione sicura al server SMTP;
  • riga 78: l’istanza [\Swift_SmtpTransport] per una connessione non sicura al server SMTP;
  • riga 81: è la classe [\SwiftMailer] che invia i messaggi. È necessario passarle la modalità di trasporto scelta;
  • riga 83: il messaggio [\SwiftMessage] viene inviato tramite il trasporto [\Swift_SmtpTransport] selezionato. Il metodo [SwiftMailer→send] restituisce il valore booleano FALSE se il messaggio non è stato inviato;
  • righe 86-89: la libreria [SwiftMailer] genera un'eccezione non appena si verifica un errore;

Nota: si noti che lo spazio dei nomi delle classi della libreria [SwiftMailer] è la radice \. Abbiamo indicato esplicitamente le classi [\SwiftMessage, \Swift_SmtpTransport, \SwiftMailer] per ricordarlo;

Risultati

Quando si esegue lo script [smtp-02.php], si ottengono i seguenti risultati in console:

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é

Se si consulta l'account Gmail dell'utente [php7parlexemple], si ottiene quanto segue:

Image

  • in [1], l’oggetto;
  • in [2], l’oggetto;
  • in [3], il destinatario;
  • in [4], il messaggio;
  • in [5-10], gli allegati;

Se si richiede di visualizzare il messaggio originale, si ottiene il seguente documento:


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

  • riga 9: l’oggetto;
  • riga 10: il mittente;
  • riga 11: il destinatario;
  • riga 13: il messaggio contiene diverse parti delimitate dai tag [--_=_swift_xx];
  • righe 19-24: il messaggio in testo semplice;
  • righe 27-30: il messaggio in formato HTML;
  • righe 34-36: il file allegato [Hello from SwiftMailer.docx];
  • righe 40-42: il file allegato [Hello from SwiftMailer.pdf];
  • righe 46-48: il file allegato [Hello from SwiftMailer.odt];
  • righe 58-60: il file allegato [Cours-Tutoriels-Serge-Tahé-1568x268.png];
  • righe 58-60: il file allegato [test-localhost.eml];
  • righe 62-114: il file allegato [test-localhost.eml] è a sua volta un messaggio il cui contenuto è visualizzato alle righe 62-114. Si può notare che questo messaggio contiene a sua volta degli allegati;

16.6. I protocolli POP3 (Post Office Protocol) e IMAP (Internet Message Access Protocol)

16.6.1. Introduzione

Per leggere le e-mail archiviate in un server di posta, esistono due protocolli:

  • il protocollo POP3 (Post Office Protocol), storicamente il primo protocollo ma ormai poco utilizzato;
  • il protocollo IMAP (Internet Message Access Protocol), più recente rispetto al POP3 e attualmente il più utilizzato;

Per scoprire il protocollo POP3, utilizzeremo la seguente architettura:

Image

  • [Serveur B] sarà un server locale POP3 / IMAP, implementato dal server di posta [hMailServer];
  • [Client A] sarà un client POP3 / IMAP di varie forme:
    • il client [RawTcpClient] per scoprire il protocollo POP3;
    • uno script PHP che riproduce il protocollo POP3 del client [RawTcpClient];
    • uno script PHP che utilizza la libreria IMAP di PHP, che consente di implementare sia i client IMAP che POP3;

16.6.2. Introduzione al protocollo POP3

Innanzitutto, utilizziamo lo script [smtp-01.php] per inviare un’e-mail all’utente [guest@localhost]. Se avete eseguito i test relativi allo script, questo utente dovrebbe aver ricevuto le e-mail, ma non siamo riusciti a verificarlo. Per inviargli una nuova e-mail, utilizzate ad esempio il seguente file di configurazione [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"
    }
}

Ora vediamo con il client [RawTcpClient] come è possibile leggere la casella di posta dell’utente [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…
  • riga 1: il server POP3 opera generalmente sulla porta 110. È il caso anche qui;
  • riga 5: il comando [USER] serve a definire l'utente di cui si desidera leggere la casella di posta;
  • riga 7: il comando [PASS] serve a definire la sua password;
  • riga 9: il comando [LIST] richiede l'elenco dei messaggi presenti nella casella di posta dell'utente;
  • riga 14: il comando [RETR] richiede la visualizzazione del messaggio di cui si specifica il numero;
  • riga 29: il comando [DELE] richiede l'eliminazione del messaggio di cui si specifica il numero;
  • riga 40: il comando [QUIT] indica al server che l'operazione è terminata;

La risposta del server può assumere diverse forme:

  • una singola riga che inizia con [+OK] per indicare che il comando precedente del client è andato a buon fine;
  • una singola riga che inizia con [-ERR] per indicare che il comando precedente del client non è andato a buon fine;
  • diverse righe in cui:
    • la prima riga inizia con [+OK];
    • l'ultima riga è costituita da un unico punto;

16.6.3. Uno script di base che implementa il protocollo POP3

Image

Poiché il protocollo POP3 ha la stessa struttura del protocollo SMTP, lo script [pop3-01.php] è un adattamento dello script [smtp-01.php]. Avrà il seguente file di configurazione [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
    }
}
  • righe 3-4: il server POP3 interpellato è il server locale [hMailServer];
  • righe 5-6: si desidera leggere la casella di posta dell’utente [guest@localhost];
  • riga 7: verranno letti al massimo 5 messaggi;

Lo script [pop3-01.php] è il seguente:


<?php

// client POP3 (Post Office Protocol) che consente di leggere i messaggi da una casella di posta
// protocollo di co    POP3 client-server
// -> il client si connette alla porta 110 del server SMTP
// <- il server gli invia un messaggio di benvenuto
// -> il client invia il comando USER utente
// <- il server risponde con OK oppure no
// -> il client invia il comando PASS mot_de_passe
// <- il server risponde con OK oppure no
// -> il client invia il comando LIST
// <- il server risponde con OK oppure no
// -> il client invia il comando RETR con un numero specifico per ciascuna e-mail
// <- il server risponde con OK oppure no. Se risponde con OK, invia il contenuto dell'e-mail richiesta
// -> il server invia tutte le righe dell'e-mail e termina con una riga contenente il
// unico carattere.
// -> il client invia il comando DELE n° per eliminare un’e-mail
// <- il server risponde OK oppure no
// // -> il client invia il comando QUIT per terminare la comunicazione con il server
// <- il server risponde con OK oppure no
// le risposte del server hanno il formato +OK testo oppure -ERR testo
// La risposta può comprendere più righe. In tal caso, l'ultima riga è costituita da un unico punto
// le righe di testo scambiate devono terminare con i caratteri RC(#13) e LF(#10)
//
//  client POP3 (SendMail Transfer Protocol) che consente di leggere le e-mail
//
// gestione degli errori
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// rigoroso rispetto dei tipi dichiarati dei parametri delle funzioni
declare (strict_types=1);
//
// i parametri per l'invio della posta
const CONFIG_FILE_NAME = "config-pop3-01.json";

// si recupera la configurazione
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);

// lettura delle caselle di posta
foreach ($mailboxes as $name => $infos) {
  // monitoraggio
  print "Lecture de la boîte à lettres [$name]\n";
  // lettura della casella di posta
  $résultat = readmail($name, $infos, TRUE);
  // visualizzazione del risultato
  print "$résultat\n";
}//for
// fine
exit;

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

function readmail(string $name, array $infos, bool $verbose = TRUE): string {
  // legge il contenuto della casella di posta [$name]
  // importa tutti i messaggi
  // ogni messaggio viene eliminato dopo la lettura
  // Se $verbose=1, tiene traccia degli scambi client-server
  //
  // apertura di una connessione con il server SMTP
  $connexion = fsockopen($infos["server"], (int) $infos["port"]);
  // Ritorno in caso di errore
  if ($connexion === FALSE) {
    return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
  }
  // $connexion rappresenta un flusso di comunicazione bidirezionale
  // tra il client (questo programma) e il server POP3 contattato
  // questo canale viene utilizzato per lo scambio di comandi e informazioni
  // dopo la connessione, il server invia un messaggio di benvenuto che viene letto
  $erreur = sendCommand($connexion, "", $verbose, TRUE);
  if ($erreur !== "") {
    // chiusura della connessione
    fclose($connexion);
    // ritorno
    return $erreur;
  }
  // comando USER
  $erreur = sendCommand($connexion, "USER {$infos["user"]}", $verbose, TRUE);
  if ($erreur !== "") {
    // chiusura della connessione
    fclose($connexion);
    // ritorno
    return $erreur;
  }
  // comando PASS
  $erreur = sendCommand($connexion, "PASS {$infos["password"]}", $verbose, TRUE);
  if ($erreur !== "") {
    // chiusura della connessione
    fclose($connexion);
    // ritorno
    return $erreur;
  }
  // comando LIST
  $premièreLigne = "";
  $erreur = sendCommand($connexion, "LIST", $verbose, TRUE, $premièreLigne);
  if ($erreur !== "") {
    // chiusura della connessione
    fclose($connexion);
    // ritorno
    return $erreur;
  }
  // analisi della prima riga per determinare il numero di messaggi
  $champs = [];
  preg_match("/^\+OK (\d+)/", $premièreLigne, $champs);
  $nbMessages = (int) $champs[1];
  // si esegue un ciclo sui messaggi
  $iMessage = 0;
  while ($iMessage < $nbMessages && $iMessage < $infos["maxmails"]) {
    // comando RETR  
    $erreur = sendCommand($connexion, "RETR " . ($iMessage + 1), $verbose, TRUE);
    if ($erreur !== "") {
      // chiusura della connessione
      fclose($connexion);
      // ritorno
      return $erreur;
    }
    // comando DELE
    $erreur = sendCommand($connexion, "DELE " . ($iMessage + 1), $verbose, TRUE);
    if ($erreur !== "") {
      // chiusura della connessione
      fclose($connexion);
      // ritorno
      return $erreur;
    }
    // messaggio successivo
    $iMessage++;
  }
  // comando QUIT
  $erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
  if ($erreur !== "") {
    // chiusura della connessione
    fclose($connexion);
    // indietro
    return $erreur;
  }
  // fine
  fclose($connexion);
  return "Terminé";
}

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

function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF, string &$premièreLigne = ""): string {
  // invia $commande nel canale $connexion
  // modalità verbosa se $verbose=1
  // se $withRCLF=1, aggiunge la sequenza RCLF allo scambio
  // inserisce la prima riga della risposta in [$premièreLigne
  // ]
  // dati
  if ($withRCLF) {
    $RCLF = "\r\n";
  } else {
    $RCLF = "";
  }
  // invia comando se $commande non è vuoto
  if ($commande !== "") {
    fputs($connexion, "$commande$RCLF");
    // eventuale eco
    if ($verbose) {
      affiche($commande, 1);
    }
  }//if
  // lettura della risposta
  $réponse = fgets($connexion, 1000);
  // si memorizza la prima riga
  $premièreLigne = $réponse;
  // eventuale eco
  if ($verbose) {
    affiche($réponse, 2);
  }
  // recupero codice di errore
  $codeErreur = substr($réponse, 0, 1);
  if ($codeErreur === "-") {
    // si è verificato un errore
    return substr($réponse, 5);
  }
  // Casi particolari dei comandi RETR e LIST che hanno risposte su più righe
  $commande = substr(strtolower($commande), 0, 4);
  if ($commande === "list" || $commande === "retr") {
    // ultima riga della risposta?
    $champs = [];
    $match = preg_match("/^\.\s+$/", $réponse, $champs);
    while (!$match) {
      // lettura della risposta
      $réponse = fgets($connexion, 1000);
      // eventuale eco
      if ($verbose) {
        affiche($réponse, 2);
      }
      // analisi della risposta
      $champs = [];
      $match = preg_match("/^\.\s+$/", $réponse, $champs);
    }//while
  }
  // ritorno senza errori
  return "";
}

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

function affiche($échange, $sens) {
  // visualizza $échange sullo schermo
  // se $sens=1 visualizza -->$echange
  // se $sens=2 visualizza <-- $échange senza gli ultimi 2 caratteri RCLF
  switch ($sens) {
    case 1:
      print "--> [$échange]\n";
      break;
    case 2:
      $L = strlen($échange);
      print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
      break;
  }//switch
}

Commenti

Come già detto, [pop3-01.php] è un adattamento dello script [smtp-01.php] che abbiamo già commentato. Ci limiteremo a commentare le principali differenze:

  • riga 55: la funzione [readmail] ha il compito di leggere le e-mail dalla casella di posta. Le informazioni per connettersi a questa casella di posta si trovano nel dizionario [$infos];
  • righe 61-66: apertura di una connessione con il server POP3;
  • righe 71-77: lettura del messaggio di benvenuto inviato dal server;
  • righe 78-85: invio del comando [USER] per identificare l’utente di cui si desiderano i messaggi;
  • righe 86-93: si invia il comando [PASS] per fornire la password di tale utente;
  • righe 94-102: si invia il comando [LIST] per verificare il numero di email presenti nella casella di posta di tale utente.
  • riga 96: si aggiunge il parametro [$premièreLigne] ai parametri della funzione [readmail]. Nella prima riga della risposta al comando LIST, il server indica quanti messaggi sono presenti nella casella di posta;
  • righe 104-106: si recupera il numero di messaggi dalla prima riga della risposta;
  • righe 109-128: si esegue un ciclo su ciascuno dei messaggi. Per ciascuno di essi si inviano due comandi:
    • RETR i: per recuperare il messaggio n. i (righe 111-117);
    • DELE i: per eliminarlo una volta letto (righe 118-125);
  • righe 129-136: si invia il comando [QUIT] per comunicare al server che l’operazione è terminata;
  • righe 178-194: per i comandi [LIST] e [RETR], la risposta del server è composta da più righe, l'ultima delle quali è costituita da un unico punto;

Risultati

All’esecuzione, si ottengono i seguenti risultati:


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.

Abbiamo qui un client POP3 di base a cui mancano alcune funzionalità:

  1. la possibilità di comunicare con un server POP3 protetto;
  2. la possibilità di leggere gli allegati di un messaggio;

Implementeremo la prima funzionalità utilizzando le funzioni da [imap] a PHP.

16.6.4. Client POP3 / IMAP implementato con le funzioni [imap] di PHP

Per prima cosa dobbiamo verificare che le funzioni [imap] siano disponibili nella versione di PHP che stiamo utilizzando. Apriamo il file [php.ini] descritto nel paragrafo "link" e cerchiamo le righe che fanno riferimento a [imap]:

Image

Riga 895, verificare che l'estensione [imap] sia effettivamente attivata.

Lo script [imap-01.php] utilizzerà il seguente file 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-"
    }
}

Il file [config-imap-01.json] definisce un array di server IMAP / POP3 da contattare. Ogni elemento è una struttura [clé:valeur], dove:

  • [clé]: è il server da contattare. Ne abbiamo due qui:
    • [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]: indica il server [imap.gmail.com] che è in ascolto sulla porta 993. Il protocollo client/server è IMAP. Il parametro /ssl indica che la comunicazione client/server è protetta. Il parametro /novalidate-cert richiede al client di non verificare il certificato di sicurezza che il server gli invierà. Infine, un server IMAP gestisce un insieme di caselle di posta per lo stesso utente. Specificando INBOX nel URL del server IMAP, indichiamo che ci interessa la casella di posta denominata INBOX, che è normalmente quella in cui arrivano i nuovi messaggi;
    • [{localhost:110/pop3}INBOX]: indica il server [localhost] che ascolta sulla porta 110. Il protocollo client/server in questo caso è POP3;
  • [valeur]: è un dizionario che specifica i seguenti punti:
    • [imap-server]: il nome del server IMAP o POP3;
    • [imap-port]: la porta del server IMAP o POP3;
    • [user]: il proprietario della cui casella di posta si desidera leggere il contenuto;
    • [password]: la sua password;
    • [output-dir]: la cartella in cui devono essere salvati i messaggi;
    • [prefix]: i nomi dei file in cui verranno salvati i messaggi avranno il formato prefixN, dove N è il numero del messaggio;
    • [pop3]: un valore booleano impostato su TRUE per indicare che il protocollo utilizzato è POP3. In questo caso, dopo aver letto un messaggio, lo si eliminerà. Questo è il funzionamento standard dei server POP3: un messaggio letto non viene conservato sul server;

Lo script [imap-01.php] è il seguente:


<?php

// client IMAP (Internet Message Access Protocol) che consente di leggere le e-mail
//
// rigoroso rispetto dei tipi dichiarati dei parametri delle funzioni
declare (strict_types=1);
// gestione degli errori
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
//
// parametri di lettura della posta
const CONFIG_FILE_NAME = "config-imap-01.json";

// recupera la configurazione
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);

// lettura delle caselle di posta
foreach ($mailboxes as $name => $infos) {
  // monitoraggio
  print "------------Lecture de la boîte à lettres [$name]\n";
  // lettura della casella di posta
  readmailbox($name, $infos);
}
// fine
exit;

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

function readmailbox(string $name, array $infos): void {
  // Tentativo di connessione
  $imapResource = imap_open($name, $infos["user"], $infos["password"]);
  // Test sul ritorno della funzione imap_open()
  if (!$imapResource) {
    // Errore
    print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
  } else {
    // Connessione stabilita
    print "Connexion établie avec le serveur [$name].\n";
    // numero totale di messaggi nella casella di posta
    $nbmsg = imap_num_msg($imapResource);
    print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
    // Messaggi non letti nella casella di posta corrente
    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) {
          // si recuperano le informazioni sul messaggio n. $msgNumber
          $infosMail = imap_headerinfo($imapResource, $msgNumber);
          if ($infosMail === FALSE) {
            print "Statut du message n° [$msgNumber] de la boîte à lettres [$name] non récupéré : " . imap_last_error() . "\n";
          } else {
            print "Statut du message n° [$msgNumber] de la boîte à lettres [$name]\n";
            print_r($infosMail);
          }
          // si recupera il corpo del messaggio n. $msgNumber
          getMailBody($imapResource, $msgNumber, $infos);

          // se il protocollo è POP3, si elimina il messaggio
          $pop3 = $infos["pop3"];
          if ($pop3 !== NULL) {
            // si elimina il messaggio in due fasi
            imap_delete($imapResource, $msgNumber);
            imap_expunge($imapResource);
          }
        }
      }
    }
  }
  // chiusura della connessione
  $imapClose = imap_close($imapResource);
  if (!$imapClose) {
    // Errore
    print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
  } else {
    // Operazione riuscita
    print "Fermeture de la connexion réussie.\n";
  }
}

function getMailBody($imapResource, int $msgNumber, array $infos): void {
  // si recupera il corpo del messaggio n. $msgNumber
  $corpsMail = imap_body($imapResource, $msgNumber);

  print "Enregistrement du message dans le fichier {$infos["output-dir"]}/{$infos["prefix"]}$msgNumber\n";
  // si crea la cartella se necessario
  if (!file_exists($infos["output-dir"])) {
    mkdir($infos["output-dir"]);
  }
  // si salva il messaggio
  if (!file_put_contents($infos["output-dir"] . "/" . $infos["prefix"] . $msgNumber, $corpsMail)) {
    print "Echec de l'enregistrement\n";
  }
}

Commenti

  • righe 19-24: si esegue un ciclo su tutti i server presenti nel file di configurazione;
  • riga 32: la funzione [raedmailbox] legge la casella di posta indicata in [$name];
  • riga 32: apertura di una connessione IMAP;
    • il primo parametro è l'ID della casella di posta da leggere (URL IMAP);
    • il secondo parametro è il nome dell’utente proprietario di questa casella di posta;
    • il terzo parametro è la sua password;

La funzione [imap_open] gestisce la sicurezza della connessione se l'ID della casella di posta (URL IMAP) include il parametro /ssl;

  • riga 41: la funzione [imap_num_msg] consente di ottenere il numero totale di messaggi presenti nella casella di posta;
  • riga 46: la funzione [imap_search] consente di cercare determinati messaggi. In questo caso, cerchiamo i messaggi che non sono ancora stati letti (UNSEEN). Il secondo parametro è un criterio di selezione. Ne esistono una ventina. La funzione [imap_search] restituisce un array di numeri di messaggio. Questi possono assumere due forme: numero di sequenza o identificativo del messaggio UID. Per impostazione predefinita, la funzione [imap_search] restituisce un array di numeri di sequenza. Se si aggiunge un terzo parametro [SE_UID], si otterranno gli identificatori UID dei messaggi;
  • riga 47: la funzione [imap_search] restituisce il valore booleano FALSE se non ha trovato alcun messaggio;
  • riga 50: si esegue un ciclo su tutti i messaggi non letti;
  • riga 52: un messaggio ha delle intestazioni che si possono ottenere con la funzione [imap_headerinfo]. Il suo secondo parametro è normalmente un numero di sequenza del messaggio. Se si desidera impostare un identificativo del messaggio UID, è necessario impostare il terzo parametro su [FT_UID];
  • riga 53: la funzione [imap_headerinfo] restituisce il valore booleano FALSE se non è riuscita a eseguire l’operazione. In caso contrario, restituisce un oggetto complesso che viene visualizzato con la funzione [print_r], riga 57;
  • riga 60: dopo le intestazioni, viene ora richiesto il corpo del messaggio tramite la funzione [imap_body]. Questa funzione restituisce NULL se non è riuscita a completare l'operazione;
  • righe 84-87: si salva il corpo del messaggio in un file locale;
  • righe 63-68: se il protocollo utilizzato era POP3, si elimina il messaggio appena letto:
    • la funzione [imap_delete] contrassegna il messaggio come «da eliminare» ma non lo elimina;
    • la funzione [imap_expunge] elimina fisicamente tutti i messaggi contrassegnati come «da eliminare»;
  • riga 74: si chiude la connessione con il server IMAP. A tal fine si utilizza la funzione [imap_close];
  • riga 86: la funzione [imap_body] consente di recuperare il corpo di un messaggio identificato dal suo numero;

Eseguiamo lo script [smtp-02.json] affinché l’utente [php7parlexemple] di Gmail e l’utente [guest] di [localhost] ricevano nuovi messaggi. Fatto ciò, eseguiamo lo script [imap-01.php] per leggere le loro caselle di posta.

I risultati visualizzati nella console sono i seguenti:


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

Se subito dopo questi risultati rieseguiamo lo script [imap-01.php], i risultati sono i seguenti:


------------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.
  • riga 3: nella casella di posta Gmail è presente sempre lo stesso numero di messaggi, ma non ci sono più messaggi nuovi non letti (riga 5). Ciò dimostra che l’esecuzione precedente ha modificato lo stato dei messaggi letti da «non letto» a «letto»;
  • riga 9: non ci sono più messaggi nella casella di posta dell’utente [guest@localhost]. Ciò è dovuto al fatto che nell’esecuzione precedente i messaggi letti su [localhost] sono stati successivamente eliminati;

I messaggi sono stati salvati localmente:

Image

Se si osserva, ad esempio, il contenuto del messaggio n. 26 di Gmail, si nota quanto segue:



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

  • righe 11-13: il messaggio in testo semplice;
  • riga 19: il messaggio HTML;
  • riga 25: l'allegato;

Proviamo a migliorare questo script per ottenere, in file separati, i diversi tipi di messaggi e gli allegati.

16.6.5. Client POP3 / IMAP migliorato

Nello script [imap-01.php], viene visualizzato il corpo del messaggio n. i come un file di testo contenente sia i diversi tipi di messaggi sia il contenuto codificato dei vari allegati. È possibile ricavare la struttura del messaggio per identificarne le diverse parti. Nello script [imap-02.php], modifichiamo la funzione [getMailBody] nel modo seguente:


function getMailBody($imapResource, int $msgNumber, array $infos): void {
  // si recupera la struttura del messaggio
  $structure=imap_fetchstructure($imapResource, $msgNumber);
  // la si visualizza
  print_r($structure);
}
  • riga 3: richiediamo la struttura del messaggio;
  • riga 5: la visualizziamo;

L’obiettivo è conoscere le informazioni contenute nella struttura di un messaggio per capire come è possibile ricavarne le diverse parti. Nel nostro esempio, il messaggio viene inviato dallo script [smtp-02.php] con la seguente configurazione [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"
        ]
    }
}

Si tratta quindi di un messaggio con cinque allegati che viene inviato a [guest@localhost] (righe 11-15). Lo script [imap-02.php] viene eseguito con la seguente configurazione [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"
    }
}

Viene quindi compromessa la casella di posta di [guest@localhost] (riga 5). Lo script [imap-02.php] visualizza quindi la struttura del messaggio inviato da [smtp-02.php]. Tale struttura, visualizzata sulla console, è la seguente:


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
                        (

                        )

                )

        )

)

Commenti

  • La documentazione PHP relativa alla funzione [imap_fetchstructure] illustra il significato dei diversi campi dell’oggetto restituito dalla funzione:

Image

I valori numerici del campo [type] hanno il seguente significato:

Image

I valori numerici del campo [encoding] hanno il seguente significato:

Image

Il messaggio registrato da [imap-01.php] iniziava con il seguente testo:


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_=_
  • le righe 15) e 33) delimitano il messaggio di tipo [multipart/mixed] (riga m);
  • le righe 18) e 16) delimitano la prima parte del messaggio: il messaggio in testo in chiaro;
  • le righe 26) e 32) delimitano la seconda parte del messaggio: il messaggio HTML;

Troviamo le diverse informazioni del messaggio sopra riportato nell’oggetto restituito da [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
                                                )

                                        )

                                )

                        )

                )

  • riga 3: il messaggio è di tipo MIME (Multipurpose Internet Mail Extensions) [multipart];
  • riga 4: il messaggio è codificato a 7 bit;
  • riga 5: [ifsubtype]=1 indica che nella struttura è presente un campo [subtype];
  • riga 6: il campo [subtype] indica un sottotipo MIME, in questo caso il tipo [mixed]. In totale, il tipo MIME del documento è [multipart/mixed];
  • riga 7: [ifdescription]=0 indica che nella struttura non è presente il campo [description];
  • riga 8: [ifid]=0 indica che nella struttura non è presente il campo [id];
  • riga 10: [ifdisposition]=0 indica che nella struttura non è presente il campo [disposition];
  • riga 11: [ifdparameters]=0 indica che nella struttura non è presente il campo [dparameters];
  • riga 12: [ifparameters]=1 indica che nella struttura è presente un campo [parameters];
  • riga 13: il campo [parameters] descrive i parametri del messaggio. In questo caso ce n’è solo uno;
  • righe 15-19: questo oggetto descrive la riga successiva del messaggio di testo:
boundary="_=_swift_1558872295_5bc8ee2ca8b3723c0b39ca8bbfbebdeb_=_"

Queste righe servono a delimitare il messaggio. Nel messaggio recuperato da [imap-01.php], la parte del messaggio appena descritta corrisponde alla riga m). L’attributo [boundary] non è lo stesso poiché gli screenshot corrispondono allo stesso messaggio ma inviato in momenti diversi;

  • riga 23: qui ha inizio la struttura delle diverse parti del messaggio;
  • righe 25-45: questa prima parte è di tipo [multipart/alternative]. Corrisponde alla riga p) del testo del messaggio;
  • riga 47: questa prima parte presenta a sua volta delle sottoparti;
  • righe 47-70: questa prima sottoparte è di tipo [text/plain] (righe 51, 54), è codificata nel tipo [ENCQUOTEDPRINTABLE] (riga 52) e presenta un parametro [charset=utf-8] (righe 66-67);
  • le righe 49-72 descrivono le righe s-x del messaggio di testo;
  • righe 74-99: descrivono la seconda sottoparte della parte [multipart/alternative];
  • righe 74-99: questa seconda sottosezione è di tipo [text/HTML] (righe 76, 79), è codificata come tipo [ENCQUOTEDPRINTABLE] (riga 77) e presenta un parametro [charset=utf-8] (righe 89-93);
  • le righe 74-99 descrivono le righe aa-ad del messaggio di testo;

La parte [multipart/alternative] è ora terminata. Inizia la parte [application/vnd.openxmlformats-officedocument.wordprocessingml.document] descritta dal seguente testo:

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"

Anche in questo caso, queste informazioni si trovano nell’oggetto restituito dalla funzione [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
                                )

                        )

                )

            
  • riga 1: è la seconda parte del messaggio complessivo. Si ricorda che la prima parte era di tipo [multipart/alternative];
  • righe 3-6: questa seconda parte è di tipo [application/vnd.openxmlformats-officedocument.wordprocessingml.document] (righe 3 e 6) ed è codificata in Base 64 (riga 4);
  • riga 11: questa seconda parte è un allegato (riga 11) e presenta due parametri: [filename=Hello from SwiftMailer.docx] (righe 15-21) e [name=Hello from SwiftMailer.docx] (righe 26-32). Si noti che quest’ultimo parametro non è presente nel messaggio di testo. È stato quindi aggiunto nella funzione [imap_fetchstructure];

Le righe da 1 a 36 vengono riprodotte per ciascuno dei cinque allegati del messaggio.

La funzione [imap_fetch_structure] ci permette quindi di ottenere la struttura di un messaggio. Questa definisce delle parti che a loro volta possono avere delle sottoparti. Per ottenere il testo di una parte o di una sottoparte si utilizza la funzione [imap_fetchbody].

Modifichiamo la funzione [getMailBody], che ci permette di ottenere il corpo di un messaggio, nel modo seguente:


function getMailBody($imapResource, int $msgNumber, array $infos, object $infosMail): void {
  // si recupera la struttura del messaggio
  $structure = imap_fetchstructure($imapResource, $msgNumber);
  if ($structure !== FALSE) {
    // si recuperano queste diverse parti
    getParts($imapResource, $msgNumber, $infos, $infosMail, $structure);
  }
}

function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
  // calcolo del numero di sezione
  if (substr($sectionNumber, 0, 2) === "0.") {
    $sectionNumber = substr($sectionNumber, 2);
  }
  print "-----contenu de la partie n° [$sectionNumber]\n";
  // tipo di contenuto
  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;
  }
  // tipo di codifica
  $encodings=["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
  print "Transfer-Encoding : ".$encodings[$part->encoding]."\n";
   
  // si passa alle eventuali sottoparti
  if (isset($part->parts)) {
    for ($i = 1; $i <= count($part->parts); $i++) {
      // una nuova parte del messaggio
      $subpart = $part->parts[$i - 1];
      // chiamata ricorsiva - si richiede il corpo della parte [$subpart]
      getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
    }
  }
}

Commenti

  • riga 3: recuperiamo la struttura del messaggio;
  • riga 6: richiediamo le diverse parti presenti nell’array [parts] della struttura;
  • riga 10: la funzione [getParts] riceve i seguenti parametri:
    • [$imapResource]: la connessione al server IMAP;
    • [$msgNumber]: il numero di sequenza del messaggio di cui si desiderano le parti;
    • [$infos]: informazioni su dove memorizzare le parti che verranno trovate nel file system locale;
    • [$infosMail]: informazioni generali sull'e-mail (mittente, destinatario/i, oggetto...;
    • [$part]: un oggetto che rappresenta una parte del messaggio;
    • [$sectionNumber]: un numero di sezione (o di parte) del messaggio;
  • righe 17-34: viene visualizzato il tipo di contenuto della parte n. [$section] del messaggio. A tal fine si utilizzano i campi [$part→type] e [$part→subtype] della parte [$part];
  • righe 36-37: viene visualizzato il tipo di codifica della parte [$sectionNumber];
  • righe 40-47: è possibile che la sezione di cui sono state appena visualizzate le informazioni contenga a sua volta delle sottosezioni;
  • righe 41-46: in tal caso, si richiede di visualizzare il tipo di contenuto delle diverse sottoparti della parte appena visualizzata. Qui si effettua una chiamata ricorsiva alla funzione [getParts];

Ancora una volta inviamo un’e-mail all’utente Gmail [php7parlexemple@gmail.com] con lo script [smtp-02.php] e la leggiamo con lo script precedente [imap-02.php]. Ciò produce i seguenti risultati in console:


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

Riusciamo effettivamente a recuperare i diversi tipi di contenuto del messaggio e il loro tipo di codifica. La numerazione delle parti segue la seguente regola:

  • righe 6-7: la parte [multipart/mixed], che rappresenta l’intero messaggio, porta il n. 0. Le diverse parti di questo oggetto porteranno quindi i numeri 1, 2…

Il messaggio è composto in totale da cinque parti:

  • righe 9-10: la parte [multipart/alternative], contrassegnata dal n. 1;
  • righe 17-18: la parte [APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT], contrassegnata dal n. 2. Si tratta dell’allegato di un file Word;
  • righe 20-21: la parte [APPLICATION/PDF] contrassegnata dal n. 3. Si tratta dell’allegato di un file PDF;
  • righe 23-24: la parte [APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT] contrassegnata dal n. 4. Si tratta dell’allegato di un file OpenOffice;
  • righe 26-27: la parte [UNKNOWN/PNG] contrassegnata dal n. 5. Si tratta dell'allegato di un file immagine;
  • righe 30-31: la parte [MESSAGE/RFC822] contrassegnata dal n. 6. Si tratta dell’allegato di un’e-mail;

Quando una parte contiene delle sottoparti, queste sono numerate x.1, x.2… dove x è il numero della parte che le racchiude. Quindi:

  • righe 11-12: la prima parte della parte [multipart/alternative] porta il n. 1.1. Si tratta di un contenuto di tipo [text/plain]: il messaggio dell’e-mail;
  • righe 14-15: la seconda parte della parte [multipart/alternative] porta il n. 1.2. Si tratta di un contenuto di tipo [text/HTML]: il messaggio e-mail in HTML;
  • righe 32-33: la prima parte dell’allegato [MESSAGE/RFC822] porta il n. 6.1. Si tratta di un contenuto di tipo [text/plain]. Infatti, secondo lo standard MIME, la numerazione delle parti di un allegato e-mail [MESSAGE/RFC822] differisce dalla regola descritta in precedenza. Pertanto, la prima parte dell’allegato [MESSAGE/RFC822] non porta il n. 6.1 ma un altro numero;

Ora che sappiamo come individuare le diverse parti e sottoparti di un’e-mail, non ci resta che recuperarne il contenuto.

Il codice dello script si evolve come segue:


function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
  // calcolo del numero di sezione
  if (substr($sectionNumber, 0, 2) === "0.") {
    $sectionNumber = substr($sectionNumber, 2);
  }
  print "-----contenu de la partie n° [$sectionNumber]\n";
  // tipo di contenuto
  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;
  }
  // tipo di codifica
  $encodings = ["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
  print "Transfer-Encoding : " . $encodings[$part->encoding] . "\n";

  // si tratta di un messaggio?
  if ($part->type === TYPEMESSAGE) {
    // non verranno gestite le sottoparti di questo messaggio (e-mail allegata)
    // si visualizza il corpo dell'e-mail allegata
    print imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
  } else {
    // si passa alle eventuali sottoparti
    if (isset($part->parts)) {
      for ($i = 1; $i <= count($part->parts); $i++) {
        // una nuova parte del messaggio
        $subpart = $part->parts[$i - 1];
        // chiamata ricorsiva - viene richiesto il corpo della parte [$subpart]
        getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
      }
    } else {
      // non ci sono sottoparti - si visualizza quindi il corpo del messaggio
      print imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
    }
  }
}

Commenti

  • riga 46: la funzione [imap_fetchbody] recupera il corpo della parte n. [$sectionNumber] del messaggio. La numerazione delle parti di un messaggio segue la regola spiegata in precedenza;
  • riga 1: si inizia con la sezione “0”;
  • riga 41: le sottoparti di questa sezione vengono quindi numerate “0.1”, “0.2”, mentre dovrebbero essere numerate “1”, “2”…
  • righe 3-5: si corregge questa anomalia;
  • righe 37-43: se la parte corrente ha delle sottoparti, si esegue un ciclo su ciascuna di esse (righe 38-43). Il loro numero di sezione è [$sectionNumber.$i];
  • righe 44-47: quando non ci sono più sottoparti, si visualizza il corpo della parte corrente con la funzione [imap_fetchbody]. Nel nostro esempio, si tratta delle parti [text/plain], [text/HTML] e degli allegati;

L'esecuzione di questo script produce i seguenti risultati:


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

Commenti

  • righe 14-16: il contenuto del messaggio di testo codificato in [quoted-printable] (riga 13);
  • riga 20: il contenuto del messaggio HTML codificato in [quoted-printable] (riga 19);
  • righe 24-28: il contenuto del file Word codificato in [base64] (riga 23);
  • righe 32-37: il contenuto del file PDF codificato in [base64] (riga 31);
  • righe 41-45: il contenuto del file OpenOffice codificato in [base64] (riga 40);
  • righe 50-55: il contenuto del file immagine codificato in [base64] (riga 49);
  • righe 59-63: il contenuto dell'allegato e-mail codificato in [base64] (riga 58);

Ora che:

  • sappiamo come recuperare i testi delle diverse parti di un’e-mail;
  • conosciamo la codifica di questi testi;

possiamo salvare questi testi in file.

Il codice si evolve come segue:


function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
  // calcolo del numero di sezione
  if (substr($sectionNumber, 0, 2) === "0.") {
    $sectionNumber = substr($sectionNumber, 2);
  }
  print "-----contenu de la partie n° [$sectionNumber]\n";
  // tipo di contenuto
  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;
  }
  // tipo di codifica
  $encodings = ["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
  print "Transfer-Encoding : " . $encodings[$part->encoding] . "\n";

  // si tratta di un messaggio?
  if ($part->type === TYPEMESSAGE) {
    // non verranno gestite le sottoparti di questo messaggio
    savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
  } else {
    // si passa alle eventuali sottoparti
    if (isset($part->parts)) {
      for ($i = 1; $i <= count($part->parts); $i++) {
        // una nuova parte del messaggio
        $subpart = $part->parts[$i - 1];
        // chiamata ricorsiva - si richiede il corpo della parte [$subpart]
        getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
      }
    } else {
      // non ci sono sottoparti - si salva quindi il corpo del messaggio
      savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
    }
  }
}
  • righe 33 e 45: la visualizzazione del testo di una parte [$imapResource, $msgNumber, $sectionNumber] dell’e-mail viene ora sostituita dal suo salvataggio in un file;

La funzione [savePart] è la seguente:


// salvataggio di una parte del messaggio
function savePart($imapResource, int $msgNumber, string $sectionNumber, array $infos, object $infosMail): void {
  // cartella di salvataggio
  $outputDir = $infos["output-dir"] . "/message-$msgNumber";
  // se la cartella non esiste, la si crea
  if (!file_exists($outputDir)) {
    mkdir($outputDir);
  }
  // struttura della parte da salvare
  $struct = imap_bodystruct($imapResource, $msgNumber, $sectionNumber);
  // tipo di documento
  $type = $struct->type;
  // sottotipo di documento
  $subtype = "";
  if (isset($struct->subtype)) {
    $subtype = strtolower($struct->subtype);
  }
  // si analizza il tipo della parte
  switch ($type) {
    case TYPETEXT:
      // caso del messaggio di testo: 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:
      // altri casi - ci si concentra solo sugli allegati
      if (isset($struct->disposition)) {
        $disposition = strtolower($struct->disposition);
        if ($disposition === "attachment") {
          // si tratta di un allegato - lo si salva
          saveAttachment($imapResource, $msgNumber, $sectionNumber, $outputDir, $struct);
        }
      } else {
        // questa parte non verrà elaborata
        print "Partie [$sectionNumber] ignorée\n";
      }
      break;
  }
}
  • righe 3-8: creazione della cartella di salvataggio. Questa riporta il numero del messaggio di cui si analizzano le parti;
  • riga 10: la parte del messaggio da salvare è definita in modo univoco dai tre parametri [$imapResource, $msgNumber, $sectionNumber]. Si richiede la struttura di questa parte con la funzione [imap_bodystruct];
  • riga 12: si recupera il tipo principale della parte del messaggio;
  • righe 13-17: si recupera il suo sottotipo;
  • righe 20-30: si elaborano i due tipi di contenuto: [text/plain] (righe 23-25) e [text/HTML] (righe 26-28). Gli altri tipi [text/xx] vengono ignorati;
  • riga 24: il testo della parte [text/plain] verrà salvato in un file [message.txt];
  • riga 27: il testo della parte [text/HTML] verrà salvato in un file [message.HTML];
  • righe 31-43: si tratta il caso delle parti il cui tipo principale non è [text];
  • riga 35: si considerano solo gli allegati del messaggio;
  • riga 37: questi vengono salvati in un file utilizzando la funzione [saveAttachment];

Riassumendo il codice precedente:

  • salva le parti [text/plain] e [text/HTML] utilizzando la funzione [saveText]. Queste parti rappresentano il contenuto dell’e-mail;
  • salva i vari allegati utilizzando la funzione [saveAttachment];

La funzione [saveText] è la seguente:


// salvataggio del testo [$text] del messaggio
function saveText(string $fileName, int $type, string $text, object $infosMail, object $struct) {
  // preparazione del testo da salvare
  // $text è codificato - lo si decodifica
  switch ($struct->encoding) {
    case ENCBASE64:
      $text = base64_decode($text);
      break;
    case ENCQUOTEDPRINTABLE:
      $text = quoted_printable_decode($text);
      break;
  }
  // intestazioni del messaggio
  // da
  $from = "From: ";
  foreach ($infosMail->from as $expéditeur) {
    $from .= $expéditeur->mailbox . "@" . $expéditeur->host . ";";
  }
  // a
  $to = "To: ";
  foreach ($infosMail->to as $destinataire) {
    $to .= $destinataire->mailbox . "@" . $destinataire->host . ";";
  }
  // oggetto
  $subject = "Subject: " . $infosMail->subject;
  // creazione del testo da salvare
  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;
  }
  // creazione del file
  print "sauvegarde d'un message dans [$fileName]\n";
  // creazione del file
  if (! file_put_contents($fileName, $contents)) {
    // creazione del file non riuscita
    print "Impossible de créer le fichier [$fileName]\n";
  }
}

Commenti

  • riga 1:
    • [$fileName] è il nome del file in cui verrà salvato il testo [$text];
    • [$type]: vale 0 per un file di testo, 1 per un file HTML;
    • [$text]: è il testo da salvare. Ma bisogna prima decodificarlo perché è codificato;
    • [$infosMail]: contiene informazioni generali sull’e-mail. Useremo i campi [from, to, subject];
    • [$struct]: è la struttura che descrive la parte dell'e-mail che stiamo salvando. Questo ci permetterà di conoscere il tipo di codifica del testo da salvare;
  • righe 4-12: si decodifica il testo da salvare;
  • righe 13-25: si recuperano le informazioni [from, to, subject] dall’e-mail;
  • righe 27-36: a seconda del tipo (0 o 1) del testo da salvare, si costruisce un testo in formato semplice (riga 30) o un testo HTML (riga 34);
  • riga 40: il testo completo viene salvato nel file [$fileName];

Gli allegati vengono invece salvati con la seguente funzione [saveAttachment]:


// salvataggio di un allegato
function saveAttachment($imapResource, int $msgNumber, string $sectionNumber, string $outputDir, object $struct) {
  // analisi della struttura dell'allegato
  // si sta cercando di recuperare il nome del file in cui salvare l'allegato
  // questo nome si trova nei [dparameters] della struttura
  if (isset($struct->dparameters)) {
    // si recuperano i [dparameters]
    $dparameters = $struct->dparameters;
    $fileName = "";
    // si percorre l'array dei [dparameters]
    foreach ($dparameters as $dparameter) {
      // ogni [dparameter] è un oggetto con due attributi [attribute, value]
      $attribute = strtolower($dparameter->attribute);
      // l'attributo [filename] corrisponde al nome del file da creare
      // in questo caso il nome del file si trova in [$dparameter->value]
      if ($attribute === "filename") {
        $fileName = $dparameter->value;
        break;
      }
    }
    // se non è stato trovato alcun nome di file, si controlla l'attributo [parameters] della struttura
    if ($fileName === "" && isset($struct->parameters)) {
      // si recuperano i valori [parameters]
      $parameters = $struct->parameters;
      foreach ($parameters as $parameter) {
        // ogni parametro è un dizionario con due chiavi [attribute, value]
        $attribute = strtolower($parameter->attribute);
        // se l'attributo è [name], allora [value] è il nome del file
        if ($attribute === "name") {
          $fileName = $parameter->value;
          // il nome del file può essere codificato
          // ad esempio =?utf-8?Q?Corsi-Tutorial-Serge-Tah=C3=A9-1568x268=2Ep
          // si recupera la codifica con un'espressione regolare
          $champs = [];
          $match = preg_match("/=\?(.+?)\?/", $fileName, $champs);
          // se c'è corrispondenza, allora si decodifica il nome del file
          if ($match) {
            $fileName = iconv_mime_decode($fileName, 0, $champs[1]);
          }
          break;
        }
      }
    }
  }
  // se è stato trovato un nome di file, si salva l'allegato
  if ($fileName !== "") {
    // salvataggio dell'allegato
    $fileName = "$outputDir/$fileName";
    print "sauvegarde de l'attachement dans [$fileName]\n";
    // creazione del file
    if ($file = fopen($fileName, "w")) {
      // si recupera il testo codificato dall'allegato
      $text = imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
      // l'allegato è codificato - lo si decodifica
      switch ($struct->encoding) {
        // base 64
        case ENCBASE64:
          $text = base64_decode($text);
          break;
        // quoted printable
        case ENCQUOTEDPRINTABLE:
          $text = quoted_printable_decode($text);
          break;
        default:
          // si ignorano gli altri casi
          break;
      }
      // scrittura del testo nel file
      fputs($file, $text);
      // chiusura del file
      fclose($file);
    } else {
      // errore nella creazione del file
      print "L'attachement n'a pu être sauvegardé dans [$fileName]\n";
    }
  }
}

Commenti

  • riga 2: la funzione [saveAttachment] accetta i seguenti parametri:
    • [$imapResource, int $msgNumber, string $sectionNumber] definisce in modo univoco la parte IMAP da salvare;
    • [string $outputDir] è la cartella di salvataggio;
    • [object $struct] descrive la struttura della parte del messaggio da salvare;
  • righe 6-44: si cerca il nome del file associato all’allegato. Si utilizzerà lo stesso nome di file per salvarlo. Il nome del file dell’allegato si trova nella tabella [$struct→dparameters] o nella tabella [$struct→parameters], o in entrambe;
  • righe 30-40: se il nome del file contiene caratteri non codificati su 7 bit, allora è stato codificato in [quoted-printable]. In questo caso, in [$struct→dparameters], l’attributo si chiama [fileName*] invece di [fileName]. Ciò significa che non ha soddisfatto la condizione della riga 16. Il nome del file viene quindi cercato nella tabella [$struct→parameters];
  • riga 32: un esempio di nome di file codificato. Ha la seguente forma: =?codage_original?codage_actuel?nom_encodé. Pertanto, il nome [=?utf-8?Q?Cours-Tutoriels-Serge-Tah=C3=A9-1568x268=2Ep] indica che il nome del file era UTF-8 e che attualmente è [quoted-printable] (Q);
  • riga 38: il nome del file viene decodificato con la funzione [iconv_mime_decode], che in questo caso accetta tre parametri:
    • la stringa da decodificare;
    • impostato su 0 per impostazione predefinita;
    • il set di caratteri da utilizzare per rappresentare la stringa decodificata. Questo parametro è presente nella stringa da decodificare. Si ottiene con un'espressione regolare alle righe 34-35;
  • righe 45-75: si salva l’allegato in un file con il nome che è stato trovato;

Per testare lo script [imap-02.php], si invia innanzitutto un’e-mail a [guest@localhost] con la seguente configurazione:

{
    "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"
        ]
    }
}

Ci sono quindi cinque allegati.

Si legge l’e-mail inviata con [imap-02.php] e la seguente configurazione:

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

I risultati della console sono i seguenti:


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

I file salvati si trovano nella cartella [output/localhost-pop3/message-N]:

Image

16.6.6. Client POP3 / IMAP con la libreria [php-mime-mail-parser]

Nello script precedente [imap-02.php], siamo riusciti a salvare:

  • i contenuti [text/plain] e [text/HTML] dell'e-mail;
  • gli allegati dell’e-mail;

Per un allegato di tipo [message/rfc822] abbiamo salvato anche il contenuto dell’allegato. Tuttavia, questo tipo di allegato è a sua volta un’e-mail che, a sua volta, contiene i contenuti [text/plain] e [text/HTML], oltre ad altri allegati. Ci si può quindi trovare nella seguente situazione:

  • un [mail 1] la cui struttura è analoga a quella di un allegato di tipo [message/rfc822];
  • un [mail 2] allegato all’e-mail 1;
  • un [mail 3] allegato all’e-mail 2;
  • ecc…

Lo script [imap-02.php] salva il contenuto di [mail 1] (testi e allegati). Salva [mail 2] come documento allegato, ma si ferma qui. Non tenta di analizzare [mail 2] per estrarne i testi e gli allegati. Si potrebbe pensare che basti applicare a [mail 2] ciò che è stato fatto per [mail 1]. Una chiamata ricorsiva al metodo che ha elaborato [mail 1] potrebbe quindi essere sufficiente per ottenere il contenuto di tutte le e-mail annidate l’una nell’altra. Purtroppo le parti di [mail 2] sono numerate con una logica diversa da quella utilizzata per [mail 1], il che impedisce di utilizzare lo stesso algoritmo in entrambi i casi, a meno di ricorrere a una logica piuttosto complessa per calcolare i numeri delle parti di un’e-mail, indipendentemente dalla sua posizione nell’insieme delle email annidate.

Lo script [imap-02.php] era già complesso. Per evitare di renderlo ancora più complesso al fine di gestire i contenuti delle email annidate, utilizzeremo la libreria [php-mime-mail-parser] disponibile su GitHub (maggio 2019) all’indirizzo URL [https://github.com/php-mime-mail-parser/php-mime-mail-parser] e scritta da Vincent Dauce.

16.6.6.1. Installazione della libreria [php-mime-mail-parser]

La pagina di presentazione della libreria indica come installarla su Windows:

Image

Ci sono due passaggi per l’installazione su Windows:


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

LA DLL della libreria [mailparse] è disponibile su URL [http://pecl.php.net/package/mailparse] (maggio 2019);

Image

  • in [2], scegliere la versione più recente e stabile della libreria;

Image

  • in [3], scegliere la versione di PHP che si sta utilizzando (in questo documento è PHP 7.2);
  • in [4], scegliete la versione del vostro OS per Windows (in questo caso si tratta di un Windows a 64 bit). Si sceglie la versione [Thread Safe];

Per conoscere la versione di PHP scaricata con Laragon, aprite [Terminal] dalla finestra di Laragon e digitate il seguente comando:


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                      

La versione di PHP 7.2.11 è indicata alla riga 3. La stessa riga riporta la versione di Windows utilizzata per la compilazione (32 o 74 bit).

Una volta ottenuto il file DLL, è necessario copiarlo nella cartella [<laragon>/bin/php/<version-php>/ext] [5]:

Image

Fatto ciò, è necessario attivare questa estensione nel file [php.ini] che configura PHP (vedi paragrafo "link"):

Image

È probabile che la riga [7] non esista e che sia necessario aggiungerla manualmente.

Una volta attivata l’estensione, è possibile verificarne la validità digitando il seguente comando in un terminale di 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)                                                              

Il comando [php –-ini] carica il file di configurazione della riga 4. A quel punto caricherà i DLL di tutte le estensioni attivate in [php.ini]. Se una di queste è errata, verrà segnalato. Verrà quindi verificata la validità del file DLL aggiunto a [php_mailparse.dll]. Può essere dichiarato non corretto per vari motivi, tra cui i più frequenti sono i seguenti:

  • avete scaricato un file DLL non corrispondente alla versione di PHP utilizzata;
  • avete scaricato un file DLL a 32 bit mentre disponete di un file PHP a 64 bit o viceversa;

Una volta attivata e verificata l’estensione, è possibile procedere all’installazione della libreria [php-mime-mail-parser]:

Image

Il comando [8] va digitato in un terminale Laragon (vedi paragrafo link):

Image

  • in [1], verificare di trovarsi nella cartella [<laragon>/www];
  • in [2], il comando di installazione della libreria [php-mime-mail-parser];
  • in [3], qui non è stato installato nulla poiché la libreria [php-mime-mail-parser] era già installata;

L’installazione della libreria [php-mime-mail-parser] avviene nella cartella [<laragon>/www/vendor]:

Image

Image

  • in [2-3], i sorgenti della libreria [php-mime-mail-parser];

Ora che l’ambiente di lavoro è stato installato, si può procedere alla scrittura dello script [imap-03.php].

16.6.6.2. Lo script [imap-03.php]

Lo script [imap-03.php] utilizza lo stesso file di configurazione [config-imap-01.json] degli script precedenti:

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

Lo script [imap-03.php] è il seguente:


<?php

// client IMAP (Internet Message Access Protocol) che consente di leggere le e-mail
// scritto con la libreria [php-mime-mail-parser]
// disponibile all’indirizzoURL [https://github.com/php-mime-mail-parser/php-mime-mail-parser] (maggio 2019)
//
// rigoroso rispetto dei tipi dichiarati dei parametri delle funzioni
declare (strict_types=1);
// gestione degli errori
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dipendenze
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
// parametri di lettura della posta
const CONFIG_FILE_NAME = "config-imap-01.json";

// si recupera la configurazione
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);

// lettura delle caselle di posta
foreach ($mailboxes as $name => $infos) {
  // monitoraggio
  print "------------Lecture de la boîte à lettres [$name]\n";
  // lettura della casella di posta
  readmailbox($name, $infos);
}
// fine
exit;

Commenti

  • righe 18-23: il contenuto del file di configurazione viene inserito nel dizionario [$mailboxes];
  • righe 26-31: ogni casella di posta viene letta dalla funzione [readmailbox] (riga 30). Questa funzione legge infatti i messaggi non letti presenti nella casella di posta. Una casella di posta corrisponde all’indirizzo e-mail di un determinato utente;

La funzione [readmailbox] è la seguente:


function readmailbox(string $name, array $infos): void {
  // si effettua la connessione
  $imapResource = imap_open($name, $infos["user"], $infos["password"]);
  if (!$imapResource) {
    // errore
    print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
    exit;
  }
  // Connessione stabilita
  print "Connexion établie avec le serveur [$name].\n";
  // numero totale di messaggi nella casella di posta
  $nbmsg = imap_num_msg($imapResource);
  print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
  // messaggi non letti nella casella di posta corrente
  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 {
      // si scorre l'elenco dei messaggi non letti
      foreach ($msgNumbers as $msgNumber) {
        print "---message n° [$msgNumber]\n";
        // si recupera il corpo del messaggio n. $msgNumber
        getMailBody($imapResource, $msgNumber, $infos);
        // se il protocollo è POP3, si elimina il messaggio dopo averlo recuperato
        $pop3 = $infos["pop3"];
        if ($pop3 !== NULL) {
          // si contrassegna il messaggio come "da eliminare"
          imap_delete($imapResource, $msgNumber);
        }
      }
      // fine della lettura dei messaggi non letti
      if ($pop3 !== NULL) {
        // si eliminano i messaggi contrassegnati come "da eliminare"
        imap_expunge($imapResource);
      }
    }
  }
  // chiusura della connessione
  $imapClose = imap_close($imapResource);
  if (!$imapClose) {
    // errore
    print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
  } else {
    // operazione riuscita
    print "Fermeture de la connexion réussie.\n";
  }
}

Commenti

Il codice della funzione [readmailbox] è lo stesso degli script precedenti.

La funzione [getMailBody] (riga 25), che analizza il corpo di un messaggio (contenuto + allegati), è la seguente:


// analisi del corpo del messaggio
function getMailBody($imapResource, int $msgNumber, array $infos): void {
  // si recupera l'intero testo del messaggio
  $text = imap_fetchbody($imapResource, $msgNumber, "");
  if ($text === FALSE) {
    print "Le corps du message [$msgNumber] n'a pu être récupéré";
    return;
  }
  // si crea un parser che analizzerà il testo del messaggio
  $parser = (new PhpMimeMailParser\Parser())->setText($text);
  // si recuperano le diverse parti del messaggio
  $outputDir = $infos["output-dir"] . "/message-$msgNumber";
  getParts($parser, $msgNumber, $outputDir);
}

Commenti

  • riga 2: la funzione [getMailBody] accetta tre parametri:
    • [$imapResource]: la risorsa IMAP a cui si è connessi;
    • [$msgNumber]: il numero del messaggio (nella casella di posta) da elaborare;
    • [$infos]: informazioni varie sulla casella di posta su cui si sta operando;
  • riga 4: si recupera l'intero messaggio n. [$msgNumber];
  • righe 5-8: caso in cui non sia stato possibile recuperare il contenuto del messaggio;
  • riga 10: si inizia a utilizzare la libreria [php-mime-mail-parser]. L’oggetto [$parser] avrà il compito di analizzare il testo del messaggio;
  • riga 12: [$outputDir] sarà la cartella in cui verranno salvati i contenuti testuali e gli allegati del messaggio n. [$msgNumber];
  • riga 13: si richiede alla funzione [getParts] di individuare le diverse parti (contenuti testuali e allegati) del messaggio n. [$msgNumber] e di salvarle nella cartella [$outputDir];

La funzione [getParts] è la seguente:


// recupero delle diverse parti di un messaggio
function getParts(PhpMimeMailParser\Parser $parser, int $msgNumber, string $outputDir): void {
  // si crea la cartella di salvataggio del messaggio, se necessario
  if (!file_exists($outputDir)) {
    if (!mkdir($outputDir)) {
      print "Le dossier [$outputDir] n'a pu être créé\n";
      return;
    }
  }
  // si recuperano le intestazioni del messaggio
  $arrayHeaders = $parser->getHeaders();
  // si salvano i messaggi di testo
  $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");
  }
  // si salvano i messaggi 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");
  }
  // si recuperano gli allegati del messaggio
  $attachments = $parser->getAttachments();
  // numero dell'allegato
  $iAttachment = 0;
  // si scorre l'elenco degli allegati
  foreach ($attachments as $attachment) {
    // tipo di allegato
    $fileType = $attachment->getContentType();
    print "-- Sauvegarde d'un attachement de type [$fileType] dans le fichier [$outputDir/{$attachment->getFilename()}]\n";
    // si salva l'allegato
    try {
      $attachment->save($outputDir, PhpMimeMailParser\Parser::ATTACHMENT_DUPLICATE_SUFFIX);
    } catch (Exception $e) {
      print "L'attachement n'a pu être sauvegardé : " . $e->getMessage() . "\n";
    }
    // caso particolare del tipo message/rfc822
    if ($fileType === "message/rfc822") {
      // l'allegato è a sua volta un messaggio - lo si analizzerà anch'esso
      // si cambia la directory di salvataggio
      $iAttachment++;
      $outputDir = $outputDir . "/rfc822-$iAttachment";
      // si cambia il contenuto da analizzare
      $parser->setText($attachment->getContent());
      // si analizza il messaggio in modo ricorsivo
      getParts($parser, $msgNumber, $outputDir);
    }
  }
}

Commenti

  • riga 2: la funzione [getParts] accetta tre parametri:
    • un parser [$parser] a cui è stato trasmesso il testo completo del messaggio da analizzare;
    • [$msgNumber] è il numero del messaggio attualmente in fase di analisi;
    • [$outputDir] è la cartella in cui devono essere salvati i contenuti e gli allegati del messaggio;
  • righe 4-9: creazione della cartella [$outputDir];
  • riga 11: si recuperano le intestazioni del messaggio in fase di analisi (da, a, oggetto…);
  • riga 13: si recuperano le parti dell’e-mail di tipo [text/plain]. Si ottiene un array;
  • righe 14-17: si salvano tutti gli elementi dell’array recuperato, assegnando a ciascuno un nome di file diverso;
  • riga 19: si recuperano le parti dell'e-mail con il tipo [text/html]. Si ottiene un array;
  • righe 20-23: si salvano tutti gli elementi dell'array recuperato, assegnando a ciascuno un nome di file diverso;
  • riga 25: si recupera l'elenco degli allegati del messaggio analizzato;
  • riga 29: si scorre questo elenco;
  • riga 24: si recupera il tipo dell’allegato (attributo Content-Type);
  • righe 34-38: si salva l’allegato nella cartella [$outputDir]. Il secondo parametro [PhpMimeMailParser\Parser::ATTACHMENT_DUPLICATE_SUFFIX] è una strategia di denominazione degli allegati. Se [$attachment→getFilename()] è pari a X e il file X esiste già, allora la libreria [php-mime-mail-parser] prova i nomi [X_1], [X_2], ecc. fino a trovare un nome di file che non esista;
  • riga 40: si verifica se l’allegato è un’e-mail;
  • righe 41-48: in tal caso, l’e-mail viene a sua volta analizzata per estrarne i contenuti e gli allegati;
  • riga 44: se [$outputDir] è uguale a X e tra gli allegati del messaggio analizzato ci sono due e-mail, allora la prima verrà salvata nella cartella [$outputDir/rfc822-1] e la seconda nella cartella [$outputDir/rfc822-2];
  • riga 46: il contenuto dell'e-mail allegata diventa il nuovo testo da analizzare;
  • riga 48: si chiama la funzione [getParts] in modo ricorsivo per analizzare il nuovo testo;

La funzione [saveMessage] salva i contenuti testuali del messaggio da analizzare:


// salvataggio di un messaggio di testo
function saveMessage(string $text, int $type, array $arrayHeaders, string $filename): void {
  // contenuto da salvare
  $contents = "";
  // aggiunta delle intestazioni
  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";
  }
  // aggiunta del testo del messaggio
  $contents .= $text;
  // salvataggio di tutto
  if (!file_put_contents($filename, $contents)) {
    // errore
    print "Le message n'a pu être sauvegardé dans le fichier [$filename]\n";
  } else {
    // operazione riuscita
    print "Le message a été sauvegardé dans le fichier [$filename]\n";
  }
}

Commenti

  • la funzione [saveMessage] accetta i seguenti parametri:
    • [$text]: il testo da salvare;
    • [$type]: il tipo di testo (0: text/plain, 1: text/HTML);
    • [$arrayHeaders]: le intestazioni del messaggio analizzato;
    • [$filename]: il nome del file in cui deve essere salvato [$text];
  • riga 4: [$contents] rappresenterà l’intero testo da salvare;
  • righe 6-20: verranno prima salvate tutte le intestazioni del messaggio (from, to, subject…);
  • righe 16-19: nel caso di un testo HTML, ogni riga termina con il tag <br/> affinché ogni intestazione appaia su una riga a sé stante in un browser;
  • riga 22: alle intestazioni si aggiunge il testo del messaggio da salvare;
  • righe 24-30: il tutto viene salvato nel file [$filename];

L'utilizzo della libreria [php-mime-mail-parser] semplifica notevolmente la scrittura dello script di lettura delle e-mail.

Lo script [smtp-02.php] viene utilizzato per inviare un’e-mail all’utente [guest@localhost] con la seguente configurazione:

{
    "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"
        ]
    }
}
  • righe 11-15: sono presenti cinque allegati;
  • riga 15: [test-localhost-2.eml] è un'e-mail strutturata come segue:
    • [test-localhost-2.eml] contiene 4 allegati (gli stessi delle righe 11-14) e un’e-mail allegata;
    • l’e-mail allegata a [test-localhost-2.eml] contiene 4 allegati (gli stessi delle righe 11-14);

Lo script [imap-03.php] viene utilizzato per leggere la casella di posta dell’utente [guest@localhost] con la seguente configurazione:

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

Dopo l’esecuzione, la struttura delle cartelle di [output/localhost-pop3] è diventata la seguente:

Image

  • in [1], i 5 allegati dell’e-mail ricevuta da [guest@localhost];
  • in [2], i 5 allegati dell’e-mail [test-localhost-2.eml] provenienti da [1];
  • in [3], i 4 allegati dell'e-mail [test-localhost.eml] inviata da [2];

I messaggi visualizzati in console sono i seguenti:


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

Se si visualizza [message_1.HTML] da [3] in un browser, si ottiene quanto segue:

Image