Skip to content

16. Netzwerkfunktionen

Wir befassen uns nun mit den Netzwerkfunktionen von PHP, die uns die Programmierung von TCP / IP (Transfer Control Protocol / Internet Protocol) ermöglichen.

Image

16.1. Grundlagen der Internet-Programmierung

16.1.1. Allgemeines

Betrachten wir die Kommunikation zwischen zwei entfernten Rechnern A und B:

Image

Wenn eine Anwendung AppA auf Rechner A mit einer Anwendung AppB auf Rechner B im Internet kommunizieren möchte, muss sie mehrere Informationen kennen:

  • die IP-Adresse (Internet Protocol) oder den Namen des Rechners B;
  • die Portnummer, über die die Anwendung AppB arbeitet. Denn der Rechner B kann zahlreiche Anwendungen unterstützen, die über das Internet arbeiten. Wenn er Informationen aus dem Netzwerk empfängt, muss er wissen, für welche Anwendung diese Informationen bestimmt sind. Die Anwendungen auf Rechner B haben über Schnittstellen, die auch als Kommunikationsports bezeichnet werden, Zugriff auf das Netzwerk. Diese Information ist in dem von Rechner B empfangenen Paket enthalten, damit es an die richtige Anwendung weitergeleitet werden kann;
  • die von Rechner B verstandenen Kommunikationsprotokolle. In unserer Untersuchung werden wir ausschließlich die Protokolle TCP-IP verwenden;
  • das von der Anwendung AppB akzeptierte Dialogprotokoll. Denn die Maschinen A und B werden miteinander „kommunizieren“. Was sie sich mitteilen, wird in die Protokolle TCP-IP eingekapselt. Wenn jedoch am Ende der Kette die Anwendung AppB die von der Anwendung AppA gesendete Information empfängt, muss sie in der Lage sein, diese zu interpretieren. Dies ist vergleichbar mit der Situation, in der zwei Personen, A und B, telefonisch miteinander kommunizieren: Ihr Gespräch wird über das Telefon übertragen. Die Sprache wird vom Telefon A in Form von Signalen codiert, über Telefonleitungen übertragen, erreicht das Telefon B und wird dort decodiert. Person B hört dann Sprache. Hier kommt der Begriff des Dialogprotokolls ins Spiel: Wenn A Französisch spricht und B diese Sprache nicht versteht, können A und B keinen sinnvollen Dialog führen;

Daher müssen sich die beiden kommunizierenden Anwendungen auf die Art des Dialogs einigen, den sie verwenden werden. Beispielsweise unterscheidet sich der Dialog mit einem Dienst ftp von dem mit einem Dienst pop: Diese beiden Dienste akzeptieren nicht dieselben Befehle. Sie verfügen über ein unterschiedliches Dialogprotokoll;

16.1.2. Die Merkmale des Protokolls TCP

Wir werden hier nur Netzwerkkommunikationen betrachten, die das Transportprotokoll TCP verwenden, dessen Hauptmerkmale wie folgt lauten:

  • Der Prozess, der Daten senden möchte, baut zunächst eine Verbindung zu dem Prozess auf, der die zu sendenden Informationen empfangen soll. Diese Verbindung wird zwischen einem Port des sendenden Rechners und einem Port des empfangenden Rechners hergestellt. Zwischen den beiden Ports entsteht so ein virtueller Pfad, der ausschließlich den beiden Prozessen vorbehalten ist, die die Verbindung hergestellt haben;
  • Alle vom Quellprozess gesendeten Pakete folgen diesem virtuellen Pfad und kommen in der Reihenfolge an, in der sie gesendet wurden;
  • die gesendeten Informationen erscheinen als kontinuierlicher Strom. Der sendende Prozess sendet Informationen in seinem eigenen Tempo. Diese werden nicht unbedingt sofort gesendet: Das Protokoll TCP wartet, bis genügend Daten vorhanden sind, um sie zu versenden. Sie werden in einer Struktur namens Segment TCP gespeichert. Sobald dieses Segment gefüllt ist, wird es an die Schicht IP weitergeleitet, wo es in ein Paket IP gekapselt wird;
  • Jedes vom Protokoll TCP gesendete Segment wird nummeriert. Das empfangende Protokoll TCP überprüft, ob es die Segmente in der richtigen Reihenfolge erhält. Für jedes korrekt empfangene Segment sendet es eine Empfangsbestätigung an den Absender;
  • sobald dieser die Bestätigung erhält, teilt er dies dem sendenden Prozess mit. Dieser kann somit feststellen, dass ein Segment erfolgreich angekommen ist;
  • erhält das Protokoll TCP, das ein Segment gesendet hat, nach einer bestimmten Zeit keine Empfangsbestätigung, sendet es das betreffende Segment erneut und gewährleistet so die Qualität des Informationsübertragungsdienstes;
  • Die zwischen den beiden kommunizierenden Prozessen hergestellte virtuelle Verbindung lautet full-duplex: Das bedeutet, dass die Informationen in beide Richtungen übertragen werden können. So kann der Zielprozess Empfangsbestätigungen senden, während der Quellprozess weiterhin Informationen sendet. Dies ermöglicht es beispielsweise dem Quellprotokoll TCP, mehrere Segmente zu senden, ohne auf eine Empfangsbestätigung zu warten. Stellt er nach einer gewissen Zeit fest, dass er die Empfangsbestätigung für ein bestimmtes Segment mit der Nummer n nicht erhalten hat, setzt er die Übertragung der Segmente an dieser Stelle fort;

16.1.3. Die Client-Server-Beziehung

Häufig verläuft die Kommunikation im Internet asymmetrisch: Rechner A initiiert eine Verbindung, um einen Dienst von Rechner B anzufordern: Er gibt an, dass er eine Verbindung zum Dienst SB1 von Rechner B herstellen möchte. Dieser akzeptiert oder lehnt die Anfrage ab. Wenn sie akzeptiert, kann Rechner A seine Anfragen an den Dienst SB1 senden. Diese müssen dem Dialogprotokoll entsprechen, das vom Dienst SB1 verstanden wird. So entsteht ein Frage-Antwort-Dialog zwischen Rechner A, dem sogenannten Client, und Rechner B, dem sogenannten Server. Einer der beiden Partner wird die Verbindung beenden.

16.1.4. Architektur eines Clients

Die Architektur eines Netzwerkprogramms, das die Dienste einer Serveranwendung in Anspruch nimmt, sieht wie folgt aus:

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. Architektur eines Servers

Die Architektur eines Programms, das Dienste bereitstellt, sieht wie folgt aus:

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

Das Serverprogramm behandelt die erste Verbindungsanfrage eines Clients anders als dessen nachfolgende Anfragen zur Inanspruchnahme eines Dienstes. Das Programm erbringt den Dienst nicht selbst. Würde es dies tun, könnte es während der Dauer des Dienstes keine Verbindungsanfragen mehr entgegennehmen, und die Clients würden nicht bedient werden. Es geht daher anders vor: Sobald eine Verbindungsanfrage am Listening-Port empfangen und angenommen wird, erstellt der Server eine Aufgabe, die dafür zuständig ist, den vom Client angeforderten Dienst bereitzustellen. Dieser Dienst wird auf einem anderen Port des Serverrechners bereitgestellt, dem sogenannten Dienstport. Auf diese Weise können mehrere Clients gleichzeitig bedient werden.

Eine Dienst-Task hat folgende Struktur:

tant que le service n'a pas été rendu totalement
        attendre une demande sur le port de service
        lorsqu'il y en a une, élaborer la réponse
        transmettre la réponse via le port de service
fin tant que
libérer le port de service

16.2. Entdecken Sie die Kommunikationsprotokolle des Internets

16.2.1. Einführung

Wenn sich ein Client mit einem Server verbunden hat, kommt es zu einem Dialog zwischen beiden. Die Art dieses Dialogs wird als Kommunikationsprotokoll des Servers bezeichnet. Zu den gängigsten Protokollen im Internet gehören die folgenden:

  • HTTP: HyperText Transfer Protocol – das Protokoll für die Kommunikation mit einem Webserver (HTTP-Server);
  • SMTP: Simple Mail Transfer Protocol – das Protokoll für die Kommunikation mit einem E-Mail-Versandserver (Server SMTP);
  • POP: Post Office Protocol – das Kommunikationsprotokoll mit einem E-Mail-Speicherserver (Server POP). Hier geht es darum, empfangene E-Mails abzurufen und nicht darum, welche zu versenden;
  • IMAP: Internet Message Access Protocol – das Kommunikationsprotokoll für den Austausch mit einem E-Mail-Speicherserver (Server IMAP). Dieses Protokoll hat das ältere Protokoll POP schrittweise abgelöst;
  • FTP: File Transfer Protocol – das Protokoll für die Kommunikation mit einem Dateispeicherserver (Server FTP);

Alle diese Protokolle zeichnen sich dadurch aus, dass es sich um textbasierte Protokolle handelt: Client und Server tauschen Textzeilen aus. Wenn ein Client in der Lage ist,

  • eine Verbindung zu einem Server TCP herzustellen;
  • die vom Server gesendeten Textzeilen in der Konsole anzuzeigen;
  • die Textzeilen, die ein Benutzer über die Tastatur eingibt, an den Server senden;

dann ist es möglich, mit einem Server TCP zu kommunizieren, der ein Textzeilenprotokoll verwendet, sofern man die Regeln dieses Protokolls kennt.

16.2.2. Dienstprogramme TCP

Image

In den zu diesem Dokument gehörenden Codes finden sich zwei Kommunikations-Dienstprogramme TCP:

  • [RawTcpClient] ermöglicht die Verbindung zum Port P eines Servers S;
  • [RawTcpServer] ermöglicht es, einen Server zu erstellen, der an einem Port P auf Clients wartet;

Der Server TCP [RawTcpServer] wirdwird mit der Syntax [RawTcpServeur port] aufgerufen, um einen Dienst TCP auf dem Port [port] des lokalen Rechners (des Computers, an dem Sie arbeiten) zu erstellen:

  • Der Server kann mehrere Clients gleichzeitig bedienen;
  • der Server führt die vom Benutzer über die Tastatur eingegebenen Befehle aus. Diese lauten wie folgt:
    • list: Listet die derzeit mit dem Server verbundenen Clients auf. Diese werden im Format [id=x-nom=y] angezeigt. Das Feld [id] dient zur Identifizierung der Clients;
    • send x [texte]: Sendet Text an den Client Nr. x (ID=x). Die eckigen Klammern [] werden nicht mitgesendet. Sie sind im Befehl erforderlich und dienen dazu, den an den Client gesendeten Text optisch abzugrenzen;
    • close x: schließt die Verbindung zum Kunden Nr. x;
    • quit: schließt alle Verbindungen und beendet den Dienst;
  • Die vom Client an den Server gesendeten Zeilen werden auf der Konsole angezeigt;
  • Der gesamte Datenaustausch wird in einer Textdatei mit dem Namen [machine-portService.txt] protokolliert, wobei
    • [machine] der Name des Rechners ist, auf dem der Code ausgeführt wird;
    • [port] der Dienstport ist, der die Anfragen des Clients beantwortet;

Der Client TCP [RawTcpClient] wird mit der Syntax [RawTcpClient serveur port] aufgerufen, um eine Verbindung zum Port [port] des Servers [serveur] herzustellen:

  • Die vom Benutzer über die Tastatur eingegebenen Zeilen werden an den Server gesendet;
  • die vom Server gesendeten Zeilen werden auf der Konsole angezeigt;
  • der gesamte Datenaustausch wird in einer Textdatei mit dem Namen [serveur-port.txt] protokolliert;

Sehen wir uns ein Beispiel an. Wir öffnen zwei Windows-Befehlsfenster und wechseln in jedem davon in den Ordner „Utilities“. In einem der Fenster starten wir den Server [RawTcpServer] auf Port 100:

Image

  • Bei [1] befinden wir uns im Ordner „Utilities“;
  • Bei [2] starten wir den Server TCP auf Port 100;
  • Bei [3] wartet der Server auf einen Client TCP;
  • In [4] wartet der Server auf einen Befehl, den der Benutzer über die Tastatur eingibt;

Im anderen Befehlsfenster wird der Client TCP gestartet:

Image

  • In [5] befinden wir uns im Ordner „Utilities“;
  • Bei [6] starten wir den Client TCP: Wir weisen ihn an, eine Verbindung zum Port 100 des lokalen Rechners (dem, mit dem Sie arbeiten) herzustellen;
  • In [7] hat der Client erfolgreich eine Verbindung zum Server hergestellt. Die Daten des Clients werden angegeben: Er befindet sich auf dem Rechner [DESKTOP-528I5CU] (in diesem Beispiel der lokale Rechner) und nutzt den Port [50405] für die Kommunikation mit dem Server:
  • In [8] wartet der Client auf einen Befehl, den der Benutzer über die Tastatur eingibt;

Kehren wir zum Serverfenster zurück. Sein Inhalt hat sich geändert:

Image

  • In [9] wurde ein Client erkannt. Der Server hat ihm die Nummer 1 zugewiesen. Der Server hat den Remote-Client (Rechner und Port) korrekt identifiziert;
  • in [10] wartet der Server erneut auf einen neuen Client;

Kehren wir zum Client-Fenster zurück und senden wir einen Befehl an den Server:

Image

  • In [11] wurde der Befehl an den Server gesendet;

Kehren wir zum Serverfenster zurück. Sein Inhalt hat sich geändert:

Image

  • in [12], in Klammern die vom Server empfangene Nachricht;

Senden wir eine Antwort an den Client:

Image

  • zu [13], die an den Client gesendete Antwort 1. Es wird nur der Text zwischen den eckigen Klammern gesendet, nicht die Klammern selbst;

Kehren wir zum Client-Fenster zurück:

Image

  • in [14], die vom Kunden empfangene Antwort. Der empfangene Text ist der zwischen den eckigen Klammern;

Kehren wir zum Serverfenster zurück, um weitere Befehle zu sehen:

Image

  • in [15] fordern wir die Liste der Clients an;
  • in [16] die Antwort;
  • mit [17] beenden wir die Verbindung mit Client Nr. 1;
  • in [18] die Bestätigung vom Server;
  • in [19] fahren wir den Server herunter;
  • in [20] die Bestätigung des Servers;

Kehren wir zum Client-Fenster zurück:

Image

  • in [21] hat der Client das Ende des Dienstes erkannt;

Es wurden zwei Protokolldateien erstellt, eine für den Server und eine für den Client:

Image

  • in [25], die Server-Protokolle: Der Dateiname entspricht dem Namen des Clients [machine-port];
  • [26], die Client-Protokolle: Der Dateiname entspricht dem Namen des Servers [machine-port];

Die Server-Protokolle lauten wie folgt:

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

Die Client-Protokolle lauten wie folgt:

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

16.3. Den Namen oder die Adresse IP eines Rechners im Internet ermitteln

Image

Rechner im Internet werden durch eine Adresse (IP, IPv4 oder IPv6) und meist auch durch einen Namen identifiziert. Letztendlich wird jedoch nur die Adresse verwendet. Daher muss man manchmal die Adresse IP eines Rechners kennen, der durch seinen Namen identifiziert wird.

Das Skript [ip-01.php] lautet wie folgt:


<?php

// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);
//
// Fehlerbehandlung
error_reporting(E_ALL & E_STRICT);
ini_set("display_errors", "on");
//
// Konstanten
$HOTES = array("istia.univ-angers.fr", "www.univ-angers.fr", "www.ibm.com", "localhost", "", "xx");
// Adressen IP und Namen der Maschinen von $HOTES
for ($i = 0; $i < count($HOTES); $i++) {
  getIPandName($HOTES[$i]);
}
// Ende
print "Terminé\n";
exit;

//------------------------------------------------
function getIPandName(string $nomMachine): void {
  //$nomMachine: Name des Rechners, dessen Adresse von IP benötigt wird
  //
  // nomMachine --> Adresse von IP
  $ip = gethostbyname($nomMachine);
  print "---------------\n";
  if ($ip !== $nomMachine) {
    print "ip[$nomMachine]=$ip\n";
    // Adresse 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";
  }
}

Kommentare

  • Zeilen 7–8: Hier wird festgelegt, dass PHP alle Fehler (E_ALL & E_STRICT) meldet und diese angezeigt werden. Dieser Modus wird nur im Entwicklungsmodus empfohlen, um den Code mithilfe der Warnungen von PHP zu verbessern. Im Produktionsmodus würde man in Zeile 8 „off“ eingeben. Seit PHP 5.4 ist die Stufe E_STRICT in E_ALL enthalten;
  • Zeile 11: die Liste der Maschinen, deren Name und Adresse abgerufen werden sollen (IP);

Die Netzwerkfunktionen von PHP werden in der Funktion getIpandName in Zeile 21 verwendet.

  • Zeile 25: Mit der Funktion gethostbyname($nom) wird die Adresse IP „ip3.ip2.ip1.ip0“ des Rechners mit dem Namen $nom ermittelt. Wenn der Rechner $nom nicht existiert, gibt die Funktion $nom als Ergebnis zurück;
  • Zeile 30: Mit der Funktion gethostbyaddr($ip) wird der Name des Rechners mit der Adresse $ip in der Form „ip3.ip2.ip1.ip0“ ermittelt. Wenn der Rechner $ip nicht existiert, gibt die Funktion $ip als Ergebnis zurück;

Ergebnisse:


---------------
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. Das Protokoll HTTP (HyperText-Übertragungsprotokoll)

16.4.1. Beispiel 1

Image

Wenn ein Browser eine URL anzeigt, fungiert er als Client eines Webservers oder, anders ausgedrückt, eines HTTP-Servers. Er ergreift die Initiative und sendet zunächst eine Reihe von Befehlen an den Server. In diesem ersten Beispiel:

  • ist der Server das Dienstprogramm [RawTcpServer];
  • der Client ist ein Browser;

Zunächst starten wir den Server auf Port 100:

Image

Anschließend fordern wir mit einem Browser die URL und [localhost:100] an, d. h. wir geben an, dass der abgefragte Server HTTP auf Port 100 des lokalen Rechners läuft:

Image

Kehren wir zum Serverfenster zurück:

Image

  • in [3], dem Client, der eine Verbindung hergestellt hat;
  • in [4-7] die Reihe von Textzeilen, die er gesendet hat:
    • in [4]: Diese Zeile hat das Format [GET URL HTTP/1.1]. Sie fordert das URL an und weist den Server an, das Protokoll HTTP 1.1 zu verwenden;
    • in [5]: Diese Zeile hat das Format [Host: serveur:port]. Die Groß-/Kleinschreibung des Befehls [Host] spielt keine Rolle. An dieser Stelle sei daran erinnert, dass der Client einen lokalen Server abfragt, der auf Port 100 läuft;
    • der Befehl [User-Agent] gibt die Identität des Clients an;
    • der Befehl [Accept] gibt an, welche Dokumenttypen vom Client akzeptiert werden;
    • der Befehl [Accept-Language] gibt an, in welcher Sprache die angeforderten Dokumente gewünscht werden, falls sie in mehreren Sprachen vorliegen;
    • Der Befehl [Connection] gibt die gewünschte Verbindungsart an: [keep-alive] gibt an, dass die Verbindung aufrechterhalten werden soll, bis der Datenaustausch abgeschlossen ist;
    • Bei [7]: Der Client beendet seine Befehle mit einer Leerzeile;

Wir beenden die Verbindung, indem wir den Server beenden:

Image

16.4.2. Beispiel 2

Da wir nun die Befehle kennen, die ein Browser sendet, um ein URL anzufordern, werden wir dieses URL mit unserem Client TCP [RawTcpClient] anfordern. Der Apache-Server von Laragon wird unser Webserver sein.

Starten wir Laragon und anschließend den Apache-Webserver:

Image

Image

Rufen wir nun mit einem Browser die Seiten URL und [http://localhost:80] auf. Hier geben wir nur den Server [localhost:80] an und kein Dokument URL. In diesem Fall wird die Seite URL / angefordert, d. h. das Stammverzeichnis des Webservers:

Image

  • in [1], das angeforderte URL. Ursprünglich wurde [http://localhost:80] eingegeben, und der Browser (hier Firefox) hat sie einfach in [localhost] umgewandelt, da das Protokoll [http] impliziert wird, wenn kein Protokoll angegeben ist, und der Port [80] impliziert wird, wenn der Port nicht angegeben ist;
  • in [2], die Startseite / des abgefragten Webservers;

Sehen wir uns nun den vom Browser empfangenen Text an:

Image

  • Man klickt mit der rechten Maustaste auf die empfangene Seite und wählt die Option [2]. Man erhält den folgenden Quellcode:

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

Nun rufen wir die URL und [http://localhost:80] mit unserem Client TCP auf:

Image

  • in [1] stellen wir eine Verbindung zum Port 80 des Servers localhost her. Dort läuft der Webserver von Laragon;

Nun geben wir die Befehle ein, die wir im vorigen Absatz entdeckt haben:

Image

  • auf [1] den Befehl [GET]. Wir fragen das Stammverzeichnis / des Webservers ab;
  • mit [2] den Befehl [Host];
  • das sind die beiden einzigen unverzichtbaren Befehle. Für die übrigen Befehle verwendet der Webserver Standardwerte;
  • in [3] die leere Zeile, die die Befehle des Clients abschließen muss;
  • unterhalb von Zeile 3 folgt die Antwort des Webservers;
  • von [4] bis zur leeren Zeile [5] folgen die Header HTTP der Serverantwort;
  • Nach der Zeile [5] folgt das angeforderte Dokument HTML [6];

Wir geben [quit] ein, um den Client zu beenden, und laden die Protokolldatei [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>]
  • Zeilen 11–79: Das empfangene Dokument HTML. Im vorherigen Beispiel hatte Firefox dasselbe empfangen;

Wir verfügen nun über die Grundlagen, um einen Client TCP zu programmieren, der eine URL anfordern würde.

16.4.3. Beispiel 3

Image

Das Skript [http-01.php] ist ein HTTP-Client, der durch die Datei jSON [config-http-01.json] konfiguriert wird. Der Inhalt dieser Datei lautet wie folgt:

{
    "localhost": {
        "port": 80,
        "GET": "/",
        "Host": "localhost:80",
        "User-Agent": "client PHP",
        "Accept": "text/HTML",
        "Accept-Language": "fr",
        "endOfLine":"\r\n"
    }
}
  • Zeile 2: Der Name des Rechners, auf dem der anzusteuernde Webserver gehostet wird;
  • Zeile 3: der Port, auf dem dieser Webserver läuft;
  • Zeile 4: die URL-Nummer des gewünschten Dokuments;
  • Zeile 5: der Zielrechner in der Form „Rechner:Port“;
  • Zeile 6: die Identifikation des Clients HTTP: Hier kann man beliebige Angaben machen;
  • Zeile 7: der vom Client akzeptierte Dokumenttyp, hier Text HTML;
  • Zeile 8: die gewünschte Sprache für das angeforderte Dokument;
  • Zeile 9: Das Zeilenendezeichen für die vom Client gesendeten Befehle: Dieses kann nämlich unterschiedlich sein, je nachdem, ob der Server auf einem Unix-Rechner (\n) oder einem Windows-Rechner (\r\n) läuft;

Das Skript [http-01.php] lautet wie folgt:


<?php

// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);
//
// Fehlerbehandlung
// error_reporting(E_ALL & E_STRICT);
// ini_set("display_errors", "on");
//
// Konstanten
const CONFIG_FILE_NAME = "config-http-01.json";
//
// Die Konfiguration wird abgerufen
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// den Text HTML aus der Konfigurationsdatei abrufen
foreach ($config as $site => $protocole) {
  // Indexseite der Website $ite lesen
  $résultat = getURL($site, $protocole);
  // Anzeige des Ergebnisses
  print "$résultat\n";
}//für
// Ende
exit;

//-----------------------------------------------------------------------
function getURL(string $site, array $protocole, $suivi = TRUE): string {
  // liest die Datei URL $site["GET"] und speichert sie in der Datei $site.HTML
  // Der Client-Server-Dialog erfolgt gemäß dem Protokoll $protocole
  //
  // Aufbau einer Verbindung über den Port von $site
  $erreurNumber = 0;
  $erreur = "";
  $connexion = fsockopen($site, $protocole["port"], $erreurNumber, $erreur);
  // Rückkehr im Fehlerfall
  if ($connexion === FALSE) {
    return "Echec de la connexion au site (" . $site . " ," . $protocole["port"] . " : $erreur";
  }
  // $connexion stellt einen bidirektionalen Kommunikationsfluss dar
  // zwischen dem Client (diesem Programm) und dem angerufenen Webserver
  // Dieser Kanal wird für den Austausch von Befehlen und Informationen genutzt
  // Das Dialogprotokoll lautet HTTP
  //
  // Erstellung der Datei $site.HTML
  $HTML = fopen("output/$site.HTML", "w");
  if ($HTML === FALSE) {
    // Schließen der Client-/Server-Verbindung
    fclose($connexion);
    // Fehlermeldung
    return "Erreur lors de la création du fichier $site.HTML";
  }
  // Der Client wird den Dialog HTTP mit dem Server starten
  if ($suivi) {
    print "Client : début de la communication avec le serveur [$site] ----------------------------\n";
  }
  // Je nach Server müssen die Zeilen des Clients mit \n oder \r\n enden
  $endOfLine = $protocole["endOfLine"];
  // Der Einfachheit halber werden Fehlerfälle in der Client-Server-Kommunikation nicht geprüft
  // Der Client sendet den Befehl GET, um das $protocole["GET"] abzufragen
  // Syntax GET URL HTTP/1.1
  $commande = "GET " . $protocole["GET"] . " HTTP/1.1$endOfLine";
  // Verfolgung?
  if ($suivi) {
    print "--> $commande";
  }
  // Der Befehl wird an den Server gesendet
  fputs($connexion, $commande);
  // Ausgabe der übrigen Header HTTP
  foreach ($protocole as $verb => $value) {
    if ($verb !== "GET" && $verb != "port"" && $verb !="endOfLine") {
      // Der Befehl wird zusammengestellt
      $commande = "$verb: $value$endOfLine";
      // Weiterverfolgung?
      if ($suivi) {
        print "--> $commande";
      }
      // Der Befehl wird an den Server gesendet
      fputs($connexion, $commande);
    }
  }
  // Die Header des Protokolls HTTP müssen mit einer Leerzeile enden
  fputs($connexion, $endOfLine);
  //
  // Der Server antwortet nun auf dem Kanal $connexion. Er sendet alle
  // seine Daten senden und anschließend den Kanal schließen. Der Client liest also alles, was von $connexion
  // bis zum Schließen des Kanals
  //
  // Zunächst werden die vom Server gesendeten Header HTTP gelesen
  // auch diese enden mit einer Leerzeile
  if ($suivi) {
    print "Réponse du serveur [$site] ----------------------------\n";
  }
  $fini = FALSE;
  while (!$fini && $ligne = fgets($connexion, 1000)) {
    // Gibt es eine Leerzeile?
    $champs = [];
    preg_match("/^(.*?)\s+$/", $ligne, $champs);
    if ($champs[1] !== "") {
      if ($suivi) {
        // Der Header HTTP wird angezeigt
        print "<-- " . $champs[1] . "\n";
      }
    } else {
      // Das war die Leerzeile – die Kopfzeilen HTTP sind beendet
      $fini = TRUE;
    }
  }
  // Das Dokument HTML, das auf die leere Zeile folgt, wird gelesen
  while ($ligne = fgets($connexion, 1000)) {
    // Die Zeile wird in der Datei „HTML“ der Website gespeichert
    fputs($HTML, $ligne);
  }
  // Der Server hat die Verbindung geschlossen – der Client schließt sie ebenfalls
  fclose($connexion);
  // Schließen der Datei $HTML
  fclose($HTML);
  // Zurück
  return "Fin de la communication avec le site [$site]. Vérifiez le fichier [$site.HTML]";
}

Kommentare zum Code:

  • Zeile 14: Die Konfigurationsdatei wird verwendet, um ein Wörterbuch zu erstellen:
    • Die Schlüssel des Wörterbuchs sind die abzufragenden Webserver;
    • die Werte legen das einzuhaltende Protokoll HTTP fest;
  • Zeilen 16–21: Es wird eine Schleife über die Liste der Webserver aus der Konfiguration durchlaufen;
  • Zeile 26: Die Funktion getURL($site,$protocole,$suivi) fordert ein Dokument von der Website $site an und speichert es in der Textdatei $site.HTML.Par: Standardmäßig werden die Client-Server-Kommunikationen auf der Konsole protokolliert ($suivi=TRUE);
  • Zeile 33: Die Funktion fsockopen($site,$port,$errNumber,$erreur) ermöglicht es, eine Verbindung zu einem Dienst TCP / IP herzustellen, der auf dem Port $port des Rechners $site läuft. Wenn die Verbindung fehlschlägt, ist [$errNumber] eine Fehlernummer und [$erreur] die zugehörige Fehlermeldung. Sobald die Client-Server-Verbindung hergestellt ist, tauschen zahlreiche Dienste wie TCP und IP Textzeilen aus. Dies ist hier beim Protokoll HTTP (HyperText Transfer Protocol) der Fall. Der vom Server an den Client gesendete Datenstrom kann dann wie eine mit [fgets] gelesene Textdatei behandelt werden. Gleiches gilt für den vom Client an den Server gesendeten Datenstrom, der mit [fputs] geschrieben werden kann;
  • Zeilen 44–50: Erstellung der Datei [$site.HTML], in der das empfangene Dokument HTML gespeichert wird;
  • Zeile 60: Der erste Befehl des Clients muss der Befehl [GET URL HTTP/1.1] sein;
  • Zeile 66: Die Funktion fputs ermöglicht es dem Kunden, Daten an den Server zu senden. Hier hat die gesendete Textzeile folgende Bedeutung: „Ich möchte (GET) die Seite [URL] der Website, mit der ich verbunden bin. Ich arbeite mit dem Protokoll HTTP, Version 1.1“;
  • Zeilen 68–79: Es werden die übrigen Zeilen des Protokolls HTTP [Host, User-Agent, Accept, Accept-Language] gesendet. Ihre Reihenfolge spielt keine Rolle;
  • Zeile 81: Es wird eine leere Zeile an den Server gesendet, um anzuzeigen, dass der Client das Senden seiner HTTP-Header abgeschlossen hat und nun auf das angeforderte Dokument wartet;
  • Zeilen 92–106: Der Server sendet zunächst eine Reihe von Headern HTTP, die verschiedene Informationen über das angeforderte Dokument liefern. Diese Header enden mit einer leeren Zeile;
  • Zeile 93: Mit der Funktion PHP [fgets] wird eine vom Server gesendete Zeile gelesen;
  • Zeile 96: Der Hauptteil der Zeile wird ohne Leerzeichen (Leerzeichen, Zeilenendezeichen) am Zeilenende extrahiert;
  • Zeile 97: Es wird geprüft, ob die Leerzeile abgerufen wurde, die das Ende der vom Server gesendeten HTTP-Kopfzeilen markiert;
  • Zeilen 98–101: Befindet man sich im Modus [suivi], wird die empfangene Kopfzeile HTTP auf der Konsole angezeigt;
  • Zeilen 108–111: Die Textzeilen der Serverantwort können mit einer Schleife zeilenweise gelesen und in der Textdatei gespeichert werden. Wenn der Webserver die gesamte angeforderte Seite gesendet hat, schließt er die Verbindung zum Client. Auf der Client-Seite wird dies als Dateiende erkannt;

Ergebnisse:

Die Konsole zeigt folgende Protokolleinträge an:


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]

In unserem Beispiel lautet die empfangene Datei „[output/localhost.HTML]“ wie folgt:


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

Wir haben tatsächlich dasselbe Dokument erhalten wie mit dem Firefox-Browser.

16.4.4. Beispiel 4

In diesem Beispiel werden wir zeigen, dass der von uns geschriebene Client HTTP unzureichend ist. Passen Sie die Konfigurationsdatei [config-http-01.json] wie folgt an:

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

Hier werden wir den Client URL [http://tahe.developpez.com:443/] aufrufen. Port 443 des Rechners [tahe.developpez.com] ist ein Port, der für das sichere HTTP-Protokoll namens HTTPS verwendet wird. Bei diesem Protokoll beginnt der Client-Server-Dialog mit einem Informationsaustausch, der die Verbindung sichert. Der Client muss daher das Protokoll [HTTPS] und nicht das Protokoll [HTTP] verwenden, was unser Client jedoch nicht tut.

Mit dieser Konfigurationsdatei lauten die Ergebnisse in der Konsole wie folgt:


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]
  • Zeile 8: Der Server [tahe.developpez.com] hat geantwortet, dass die Anfrage des Clients fehlerhaft war;

Der Inhalt der Datei [output/tahe.developpez.com.HTML] lautet dann wie folgt:


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

Der Server weist eindeutig darauf hin, dass wir nicht das richtige Protokoll verwendet haben.

Verwenden wir nun die folgende Konfigurationsdatei:

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

Die Konsolenausgaben lauten dann wie folgt:


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]
  • Zeile 11 zeigt an, dass der Server das Dokument in Teilen versendet;

Dies äußert sich darin, dass der an den Client gesendete Datenstrom Zahlen enthält: Jede Zahl teilt dem Client die Anzahl der Zeichen des nächsten vom Server gesendeten Teils mit. So sieht das in der Datei [output/sergetahe.com.HTML] aus:

Image

  • in [1] und [2] die hexadezimale Größe der Teile 1 und 2 des Dokuments;

Ein korrekter HTTP-Client sollte diese Zahlen nicht im endgültigen HTML-Dokument belassen.

Hier ist ein weiteres Beispiel:

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

Es ähnelt dem vorherigen Beispiel, aber das in Zeile 4 angeforderte URL endet nicht mit dem Zeichen /. Es handelt sich nicht um dasselbe URL. Die Ausführung des Clients HTTP liefert dann folgende Konsolenergebnisse:


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/Programmierkurse-Tutorials/
<-- 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]
  • Zeile 8 zeigt an, dass sich das angeforderte Dokument von URL geändert hat. Die neue URL wird in Zeile 13 angegeben. Beachten Sie diesmal das Zeichen /, das die neue URL abschließt;

Die Datei [output/serge.tahe.com.HTML] sieht dann wie folgt aus:


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

Ein Client mit der ID HTTP sollte den Weiterleitungen folgen können. Hier sollte er automatisch die neue URL [http://sergetahe.com/cours-tutoriels-de-programmation/] erneut anfordern.

16.4.5. Beispiel 5

Die vorangegangenen Beispiele haben gezeigt, dass unser Client HTTP unzureichend war. Wir stellen nun ein Tool namens [curl] vor, mit dem sich Webdokumente abrufen lassen und das die genannten Schwierigkeiten bewältigt: HTTPS-Protokoll, in Teilen gesendete Dokumente, Weiterleitungen … Das Tool [curl] wurde mit Laragon installiert:

Image

Öffnen wir ein Laragon-Terminal [1]:

Image

Im Terminal geben wir den folgenden Befehl ein:

Image

  • in [1], dem Konsolentyp;
  • in [2] den aktuellen Ordner. Dieser Ordner ist besonders wichtig: Hier holt der Apache-Server von Laragon die angeforderten Dokumente ab. Wir sollten diesen Ordner daher nicht mit unnötigen Dateien überladen;
  • in [3] den eingegebenen Befehl;

Es ist möglich, dass der Befehl [curl --help] einen Fehler verursacht. Die wahrscheinlichste Ursache ist, dass Sie nicht den richtigen Terminaltyp verwenden. Öffnen Sie in diesem Fall ein weiteres Terminal mit den Befehlen [4-6];

Der Befehl [curl --help] zeigt alle Konfigurationsoptionen von [curl] an. Es gibt mehrere Dutzend davon. Wir werden nur sehr wenige davon verwenden. Um ein URL anzufordern, reicht es aus, den Befehl [curl URL] einzugeben. Dieser Befehl zeigt das angeforderte Dokument auf der Konsole an. Wenn man zusätzlich den Datenaustausch HTTP zwischen Client und Server anzeigen möchte, gibt man [curl --verbose URL] ein. Um schließlich das angeforderte Dokument HTML in einer Datei zu speichern, gibt man [curl --verbose --output fichier URL] ein.

Um zu vermeiden, dass der Ordner „[www]“ von Laragon überfüllt wird, wechseln wir an einen anderen Ort im Dateisystem:

Image

  • Bei [1] wechseln wir in den Ordner [c:\temp]. Falls dieser Ordner nicht existiert, können Sie ihn erstellen oder einen anderen auswählen;
  • in [2] erstellen wir einen Ordner namens [curl];
  • In [3] markieren wir diesen Ordner;
  • In [4] listet man den Inhalt auf. Er ist leer;

Stellen Sie sicher, dass der Apache-Server von Laragon gestartet ist, und rufen Sie mit [curl] die Ordner URL und [http://localhost/] mit dem Befehl [curl –verbose –output localhost.HTML http://localhost/] ab. Man erhält folgende Ergebnisse:


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 zum Host „localhost“ bleibt unverändert                                                   
  • Zeilen 8–12: Zeilen, die von [curl] an den Server [localhost] gesendet wurden. Man erkennt das Protokoll HTTP;
  • Zeilen 13–19: Vom Server als Antwort gesendete Zeilen;
  • Zeile 13: zeigt an, dass das angeforderte Dokument erfolgreich empfangen wurde;

Die Datei [localhost.HTML] enthält das angeforderte Dokument. Sie können dies überprüfen, indem Sie die Datei in einem Texteditor öffnen.

Nun fordern wir die Datei URL [https://tahe.developpez.com:443/] an. Um diese URL zu erhalten, muss der Client HTTP die Sprache HTTPS beherrschen. Dies ist beim Client [curl] der Fall.

Die Konsolenausgaben lauten wie folgt:


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 zum Host tahe.developpez.com, unverändert
  • Zeilen 10–40: Der Austausch zwischen Client und Server zur Sicherung der Verbindung: Diese wird verschlüsselt;
  • Zeilen 42–45: Die vom Client [curl] an den Server gesendeten Header HTTP;
  • Zeile 48: Das angeforderte Dokument wurde gefunden;
  • Zeile 53: Das Dokument wird in Teilen gesendet;

[curl] verarbeitet sowohl das sichere Protokoll HTTPS als auch die Tatsache, dass das Dokument in Teilen gesendet wird, korrekt. Das gesendete Dokument ist hier in der Datei [tahe.developpez.com.HTML] zu finden.

Fordern wir nun die Dateien URL und [http://sergetahe.com/cours-tutoriels-de-programmation] an. Wir hatten gesehen, dass es bei dieser URL eine Weiterleitung zu den Dateien URL und [http://sergetahe.com/cours-tutoriels-de-programmation/] gab (mit einem / am Ende).

Die Konsolenausgabe sieht dann wie folgt aus:


c:\Temp\curl
λ curl --verbose --output sergetahe.com.HTML --location http://sergetahe.com/Programmierkurse-Tutorials
  % 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/Programmierkurse-Tutorials/
< 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 auf dem Host sergetahe.com unverändert belassen
* Issue another request to this URL: 'http://sergetahe.com/Programmierkurse-und-Tutorials/'
* Found bundle for host sergetahe.com: 0x1c88548 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) mit 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 zum Host sergetahe.com, unverändert belassen
  • Zeile 2: Mit der Option [--location] wird angegeben, dass die vom Server gesendeten Weiterleitungen verfolgt werden sollen;
  • Zeile 13: Der Server teilt mit, dass sich das angeforderte Dokument geändert hat (URL);
  • Zeile 18: Er gibt die neue URL des angeforderten Dokuments an;
  • Zeile 27: [curl] sendet eine neue Anfrage, diesmal an die neue URL;
  • Zeile 33: Die neue URL wird verwendet;
  • Zeile 38: Der Server antwortet, dass er das angeforderte Dokument gefunden hat;
  • Zeile 41: Er sendet es in Teilen;

Das angeforderte Dokument befindet sich in der Datei [sergetahe.com.HTML].

16.4.6. Beispiel 6

PHP verfügt über eine Erweiterung namens [libcurl], die es ermöglicht, die Funktionen des Tools [curl] in einem Programm PHP zu nutzen. Zunächst muss sichergestellt werden, dass diese Erweiterung in der im Abschnitt „Link“ beschriebenen Datei [php.ini] aktiviert ist:

Image

Stellen Sie sicher, dass die oben genannte Zeile 889 nicht auskommentiert ist.

Wir werden ein Skript [http-02.php] erstellen, das die folgende Konfigurationsdatei jSON nutzt:

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

Jedes Element des Wörterbuchs [clé, valeur] hat folgende Struktur:

  • clé: der Name eines Webservers;
  • valeur ist ein Wörterbuch mit den folgenden Schlüsseln:
    • timeout: maximale Wartezeit auf die Antwort des Servers. Nach Ablauf dieser Zeit trennt der Client die Verbindung;
    • url: URL des angeforderten Dokuments;

Der Code des Skripts [http-02.php] lautet wie folgt:


<?php

// strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);
//
// Fehlerbehandlung
//error_reporting(E_ALL & E_STRICT);
//ini_set("display_errors", "on");
//
// Konstanten
const CONFIG_FILE_NAME = "config-http-02.json";
//
// Konfiguration abrufen
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);

// den Text HTML aus der Konfigurationsdatei abrufen
foreach ($config as $site => $infos) {
  // URL von der Website $ite auslesen
  $résultat = getUrl($site, $infos["url"], $infos["timeout"]);
  // Anzeige des Ergebnisses
  print "$résultat\n";
}//für
// Ende
exit;

//-----------------------------------------------------------------------
function getUrl(string $site, string $url, int $timeout, $suivi = TRUE): string {
  // liest die Datei URL $url und speichert sie in der Datei output/$site.HTML
  //
  // Fortsetzung
  print "Client : début de la communication avec le serveur [$site] ----------------------------\n";

  // Initialisierung einer Sitzung cURL
  $curl = curl_init($url);
  if ($curl === FALSE) {
    // Es ist ein Fehler aufgetreten
    return "Erreur lors de l'initialisation de la session cURL pour le site [$site]";
  }
  // curl-Optionen
  $options = [
    // Ausführlicher Modus
    CURLOPT_VERBOSE => true,
    // Neue Verbindung – kein Cache
    CURLOPT_FRESH_CONNECT => true,
    // Zeitlimit für die Anfrage (in Sekunden)
    CURLOPT_TIMEOUT => $timeout,
    CURLOPT_CONNECTTIMEOUT => $timeout,
    // Gültigkeit der Zertifikate nicht prüfen SSL
    CURLOPT_SSL_VERIFYPEER => false,
    // Weiterleitungen verfolgen
    CURLOPT_FOLLOWLOCATION => true,
    // Abfrage des angeforderten Dokuments als Zeichenkette
    CURLOPT_RETURNTRANSFER => true
  ];

  // Konfiguration von curl
  curl_setopt_array($curl, $options);
  // Ausführung der Anfrage
  $page_content = curl_exec($curl);
  // Schließen der Sitzung cURL
  curl_close($curl);

  // Auswertung des Ergebnisses
  if ($page_content !== FALSE) {
    // Speichern des Ergebnisses in $site.HTML
    $result = file_put_contents("output/$site.HTML", $page_content);
    if ($result === FALSE) {
      // Fehlermeldung
      return "Erreur lors de la création du fichier [output/$site.HTML]";
    }
    // Erfolgreiche Rückgabe
    return "Fin de la communication avec le serveur [$site]. Vérifiez le fichier [output/$site.HTML]";
  } else {
    // Es ist ein Kommunikationsfehler aufgetreten
    return "Erreur de communication avec le serveur [$site]";
  }
}

Kommentare

  • Zeile 14: Die Konfigurationsdatei wird verwendet, um das Wörterbuch [$config] zu erstellen;
  • Zeilen 17–22: Es wird eine Schleife über die Liste der in der Konfiguration gefundenen Websites durchlaufen;
  • Zeile 19: Für jede Website wird die Funktion [getUrl] aufgerufen, die dieURL, $infos, [«url»] mit einem Timeout $infos, [«timeout»] herunterlädt;
  • Zeile 34: Es wird eine Sitzung mit der ID [curl] gestartet. [curl_init] stellt noch keine Verbindung zum Webserver her. Sie gibt eine Ressource [$curl] zurück, die als Parameter für alle folgenden Funktionen [curl] dient;
  • Zeilen 35–38: Wenn die Initialisierung der Sitzung [curl] fehlschlägt, gibt die Funktion [curl_init] den booleschen Wert FALSE zurück;
  • Zeilen 40–54: Das Dictionary [$options] konfiguriert die Verbindung [curl] zum Server;
  • Zeile 57: Die Verbindungsoptionen werden an die Ressource [$curl] übermittelt;
  • Zeile 59: Verbindung zu URL mit den definierten Optionen angefordert. Aufgrund der Option [CURLOPT_RETURNTRANSFER => true] gibt die Funktion [curl_exec] das vom Server gesendete Dokument als Zeichenkette zurück. Die Funktion [curl_exec] gibt den Booleschen Wert FALSE zurück, falls die Verbindung fehlschlägt;
  • Zeile 64: Das Ergebnis von [curl_exec] wird analysiert;
  • Zeile 66: Die empfangene Seite wird in einer lokalen Datei gespeichert;
  • Zeilen 69, 72, 75: Das Ergebnis der Funktion [getUrl] wird ausgegeben;

Bei Ausführung des Skripts [http-02.php] erhält man folgende Konsolenausgaben:


* 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/Programmierkurse-Tutorials
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 zum Host sergetahe.com unverändert belassen
* Issue another request to this URL: 'http://sergetahe.com/Programmierkurse-Tutorials'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) mit 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/Programmierkurse-Tutorials/
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< Cache-control: private
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 zum Host sergetahe.com, unverändert belassen
* Issue another request to this URL: 'http://sergetahe.com/Programmierkurse-und-Tutorials/'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) mit 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/Programmierkurse-Tutorials/wp-json/>; rel="https://api.w.org/"
< Link: <http://sergetahe.com/Programmierkurse-und-Tutorials/>; 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, um sergetahe.com unverändert zu belassen
* 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 zum Host tahe.developpez.com, unverändert belassen
* 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, um www.polytech-angers.fr unverändert zu belassen
* 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) mit dem 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 zum Host www.polytech-angers.fr, der unverändert bleibt
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 zum Host „localhost“ bleibt unverändert

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

Kommentare

  • Man erhält dieselben Datenaustausche wie mit dem Tool [curl];
  • in Grün die Protokolle des Skripts;
  • in Blau die an den Server gesendeten Befehle;
  • gelb: die Befehle, die der Client als Antwort erhält;

16.4.7. Fazit

In diesem Abschnitt haben wir das Protokoll HTTP kennengelernt und ein Skript [http-02.php] geschrieben, mit dem sich eine URL aus dem Internet herunterladen lässt.

16.5. Das Protokoll SMTP (Simple Mail Transfer Protocol)

16.5.1. Einleitung

Image

In diesem Kapitel:

  • [Serveur B] wird ein lokaler SMTP-Server sein, den wir installieren werden;
  • [Client A] wird ein SMTP-Client in verschiedenen Formen sein:
    • der Client [RawTcpClient] zur Erkundung des Protokolls SMTP;
    • ein Skript PHP, das das Protokoll SMTP des Clients [RawTcpClient] nachbildet;
    • ein Skript PHP, das die Bibliothek [SwiftMailServer] nutzt, um E-Mails aller Art zu versenden;

16.5.2. Erstellung einer E-Mail-Adresse [gmail]

Für unsere Tests mit SMTP benötigen wir eine E-Mail-Adresse, an die wir schreiben können. Dazu erstellen wir eine Adresse bei Gmail:

Image

  • Unter [5] erstellen wir den Benutzer [php7parlexemple] (wählen Sie einen anderen Namen);
  • bei [6] lautet das Passwort [PHP7parlexemple] (wählen Sie einen anderen Namen);
  • Bei [7] bestätigen wir diese Angaben;

Image

  • Füllen Sie die Felder [9-10] aus und bestätigen Sie (11);
  • die Nutzungsbedingungen von Google akzeptieren (12–13) und anschließend bestätigen (14);

Image

  • in [15]: der Posteingang (Inbox) des Nutzers [PHP7] (16);
  • in [17]: Dieser Nutzer hat einen leeren Posteingang;
  • in [18-19]: Melden Sie sich beim Google-Konto des Nutzers [php7parlexemple@gmail.com] an. Wir werden die Kontosicherheit konfigurieren;

Image

  • In [21]: Erlauben Sie anderen Anwendungen als denen von Google, das Konto [php7parlexemple] zu nutzen. Wenn wir dies nicht tun, kann unser lokaler E-Mail-Server [hMailServer] nicht mit dem Gmail-Server SMTP kommunizieren;

Image

16.5.3. Einrichtung eines Servers SMTP

Für unsere Tests werden wir den Mailserver [hMailServer] installieren, der sowohl als SMTP-Server zum Versenden von E-Mails dient, einen POP3-Server (Post Office Protocol), mit dem auf dem Server gespeicherte E-Mails abgerufen werden können, sowie einen IMAP-Server (Internet Message Access Protocol), der ebenfalls das Abrufen der auf dem Server gespeicherten E-Mails ermöglicht, aber darüber hinausgeht. Er ermöglicht insbesondere die Verwaltung der Speicherung von E-Mails auf dem Server.

Der Mailserver [hMailServer] ist unter URL und [https://www.hmailserver.com/] verfügbar (Mai 2019).

Image

Während der Installation werden Sie um die Angabe bestimmter Informationen gebeten:

Image

  • Wählen Sie unter [1-2] sowohl den Mailserver als auch die Tools zu dessen Verwaltung aus;
  • Während der Installation werden Sie nach dem Administratorpasswort gefragt: Notieren Sie es sich, da Sie es benötigen werden;

[hMailServer] wird als Windows-Dienst installiert, der beim Systemstart automatisch gestartet wird. Es ist ratsam, einen manuellen Start zu wählen:

  • Geben Sie in [3] „[services]“ in das Eingabefeld der Statusleiste ein;

Image

  • Bei [4-8] versetzt man den Dienst in den Modus [manuel] (6) und startet ihn (7);

Nach dem Start muss der Server [hMailServer] konfiguriert werden. Der Server wurde mit einem Verwaltungsprogramm [hMailServer Administrator] installiert:

Image

  • in [2] im Eingabefeld der Statusleiste „[hmailserver]“ eingeben;
  • bei [3] den Administrator starten;
  • Bei [4]: Verbinden Sie den Administrator mit dem Server [hMailServer];
  • Geben Sie bei [5] das Passwort ein, das Sie bei der Installation von [hMailServer] festgelegt haben;

Image

Wir werden nun ein Benutzerkonto anlegen:

  • Rechtsklick auf [Accounts] (7) und dann (8), um einen neuen Benutzer hinzuzufügen;
  • Auf der Registerkarte [General] (9) legen wir einen Benutzer [guest] (10) mit dem Passwort [guest] (11) an. Er erhält die E-Mail-Adresse [guest@localhost] (10);
  • in [12] ist der Benutzer [guest] aktiviert;

Image

Image

  • In [15] wird das Protokoll SMTP des Mail-Servers konfiguriert;
  • In [16] wird die E-Mail-Verteilung konfiguriert;
  • in [17] die Konfiguration der E-Mail-Verteilung an den Host-Rechner (localhost);
  • in [18] den Namen des lokalen Rechners (localhost). Mit dem Skript aus dem Abschnitt „Link“ können Sie diesen Namen ermitteln;
  • In [19] wird ein Relay-Server SMTP konfiguriert: Dabei handelt es sich um den Server, der für die Weiterleitung von E-Mails zuständig ist, die nicht für den lokalen Rechner (localhost) bestimmt sind;
  • in [20] der Gmail-Server SMTP. Wir verwenden Gmail, da wir dort im Abschnitt „Link“ ein Konto erstellt haben;
  • bei [21] ist der Port SMTP von Gmail gemeint;
  • in [22] ist der Dienst SMTP von Gmail ein gesicherter Dienst: Für den Zugriff ist ein Gmail-Konto erforderlich;
  • in [23], der im Absatz „Link“ erstellte Benutzer [php7parlexemple];
  • in [24] das Passwort dieses Benutzers: [PHP7parlexemple], das im Abschnitt „Link“ erstellt wurde;
  • in [25] wird der von Gmail verwendete Sicherheitsprotokolltyp angegeben;

Image

  • in [27] der Port des Dienstes SMTP;
  • In [28] wird angegeben, dass dieser Dienst keine Authentifizierung erfordert;
  • Geben Sie unter „[30]“ die Begrüßungsnachricht ein, die der Server „SMTP“ an seine Clients sendet;

16.5.4. Das Protokoll SMTP

Image

Wir werden das Protokoll SMTP anhand der folgenden Umgebung näher betrachten:

  • Client A ist der generische Client TCP ([RawTcpClient]);
  • Server B ist der Mailserver [hMailServer];
  • Client A wird Server B auffordern, eine E-Mail an den Benutzer [php7parlexemple@gmail.com] zuzustellen;
  • Wir überprüfen, ob dieser Benutzer die gesendete E-Mail tatsächlich erhalten hat;

Wir starten den Client wie folgt:

Image

  • Unter [1] stellen wir eine Verbindung zum Port 25 des lokalen Rechners her, auf dem der Dienst SMTP von [hMailServer] läuft. Das Argument [--quit bye] gibt an, dass der Benutzer das Programm durch Eingabe des Befehls [bye] beendet. Ohne dieses Argument lautet der Befehl zum Beenden des Programms [quit]. Nun ist [quit] aber auch ein Befehl des Protokolls SMTP. Wir müssen diese Mehrdeutigkeit also vermeiden;
  • Bei [2] ist der Client ordnungsgemäß verbunden;
  • bei [3] wartet der Client auf über die Tastatur eingegebene Befehle;
  • bei [4] sendet der Server ihm seine Begrüßungsnachricht;

Image

  • in [5] sendet der Client den Befehl [EHLO nom-de-la-machine-client]. Der Server antwortet ihm mit einer Reihe von Nachrichten der Form [250-xx] (6). Der Code [250] zeigt an, dass der vom Client gesendete Befehl erfolgreich war;
  • in [7] gibt der Client den Absender der Nachricht an, hier [guest@localhost]. Dieser Benutzer muss auf dem Mailserver [hMailServer] vorhanden sein. Dies ist hier der Fall, da wir diesen Benutzer zuvor angelegt haben;
  • In [8] steht die Antwort des Servers;
  • in [9] wird der Empfänger der Nachricht angegeben, hier der Gmail-Benutzer [php7parlexemple@gmail.com];
  • in [10] die Antwort des Servers;
  • in [11] teilt der Befehl [DATA] dem Server mit, dass der Client den Inhalt der Nachricht senden wird;
  • in [12] die Antwort des Servers;
  • in [13-16] muss der Client eine Liste von Textzeilen senden, die mit einer Zeile endet, die nur einen einzigen Punkt enthält. Die Nachricht kann Zeilen [Subject :, From :, To :] (13) enthalten, um jeweils den Betreff der Nachricht, den Absender und den Empfänger anzugeben;
  • In [14] muss auf die vorangegangenen Kopfzeilen eine Leerzeile folgen;
  • in [15] der Text der Nachricht;
  • in [16] die Zeile, die nur einen einzigen Punkt enthält und das Ende der Nachricht kennzeichnet;
  • in [17]: Sobald der Server die Zeile mit dem einzelnen Punkt empfangen hat, stellt er die Nachricht in die Warteschlange;
  • in [18] teilt der Client dem Server mit, dass er fertig ist;
  • in [19] die Antwort des Servers;
  • In [20] ist zu erkennen, dass der Server die Verbindung zum Client geschlossen hat;

Nun überprüfen wir, ob der Benutzer [php7parlexemple@gmail.com] die Nachricht tatsächlich erhalten hat:

Image

  • In [2] ist zu sehen, dass der Benutzer [php7parlexemple@gmail.com] die Nachricht tatsächlich erhalten hat;

Image

Image

Image

  • in [7], dem Absender der E-Mail. Man sieht, dass es sich nicht um [guest@localhost] handelt. Dies liegt daran, dass der in der Konfiguration von [hmailServer] festgelegte Relay-Server die Nachricht zugestellt hat. Dieser Relay-Server ist jedoch [smtp.gmail.com], der mit den Anmeldedaten des Gmail-Nutzers [php7parlexemple@gmail.com] verknüpft ist. Jede E-Mail, die von [hMailServer] stammt, wird so aussehen, als käme sie vom Nutzer [php7parlexemple@gmail.com]. Das war hier nicht beabsichtigt, aber wenn man diesen Relay-Server nicht verwendet, lehnt der Gmail-Dienst SMTP die von [hMailServer] gesendeten E-Mails ab, da der Gmail-Dienst SMTP eine Authentifizierung verlangt, die [hMailServer] nicht übermittelt. Es gibt sicherlich eine Möglichkeit, dieses Problem zu umgehen, aber ich habe sie nicht gefunden;
  • In [8] sieht man, dass die E-Mail von dem Rechner [DESKTOP-528I5CU] empfangen wurde, auf dem der Mailserver [hMailServer] gehostet wird;
  • in [9] ist der Absender der Nachricht zu sehen. Man sieht, dass es sich nicht um [guest@localhost] handelt;
  • in [10], der ursprüngliche Absender der Nachricht. Diesmal ist es tatsächlich [guest@localhost];
  • in [11], der Betreff;
  • in [12] der Empfänger;
  • in [13] die Nachricht;

Schließlich ist es unserem Client [RawTcpClient] gelungen, die Nachricht zu versenden, obwohl beim Absender ein Problem aufgetreten ist. Wir verfügen nun über die Grundlagen, um einen Client SMTP zu erstellen, der in PHP geschrieben ist.

16.5.5. Ein einfacher Client SMTP, geschrieben in PHP

Wir werden das, was wir zuvor über das Protokoll SMTP gelernt haben, in PHP umsetzen.

Image

Das Skript [smtp-01.php] wird durch die folgende Datei jSON [config-smtp-01.json] konfiguriert:


{
    "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] ist ein Array, bei dem jedes Element ein Dictionary vom Typ [nom=>infos] ist. Der Wert [infos] ist selbst ein Dictionary mit den folgenden Schlüsseln und Werten:

  • [smtp-server]: der Name des zu verwendenden Servers SMTP;
  • [smtp-port]: die Portnummer des Dienstes SMTP;
  • [from]: der Absender der Nachricht;
  • [to]: der Empfänger der Nachricht;
  • [subject]: Betreff der Nachricht;
  • [message]: die zu versendende Nachricht;
  • Das erste Element nutzt den Server SMTP [localhost], um eine E-Mail an einen Benutzer von [localhost] zu senden;
  • Das zweite Element nutzt die Server SMTP und [localhost], um eine E-Mail an einen Benutzer von [Gmail] zu senden;
  • Das dritte Element verwendet die Server SMTP und [Gmail], um eine E-Mail an einen Benutzer von [Gmail] zu senden;

Der Code [smtp-01.php] des Clients SMTP lautet wie folgt:


<?php

// Client SMTP (SendMail-Übertragungsprotokoll) zum Senden einer Nachricht
// Kommunikationsprotokoll SMTP Client-Server
// -> Client verbindet sich über Port 25 mit dem SMTP-Server
// <- Der Server sendet ihm eine Willkommensnachricht
// -> Der Client sendet den Befehl EHLO mit dem Namen seines Rechners
// <- Der Server antwortet mit „OK“ oder nicht
// -> Der Client sendet den Befehl MAIL FROM: <Absender>
// <- Server antwortet mit OK oder nicht
// -> Der Client sendet den Befehl RCPT TO: <Empfänger>
// <- Server antwortet mit OK oder nicht
// -> Der Client sendet den Befehl DATA
// <- Server antwortet mit OK oder nicht
// -> Der Client sendet alle Zeilen seiner Nachricht und schließt mit einer Zeile ab, die das
// einziges Zeichen.
// <- Server antwortet mit OK oder nicht
// -> Der Client sendet den Befehl QUIT
// <- Server antwortet mit OK oder nicht
// Die Antworten des Servers haben die Form „xxx Text“, wobei „xxx“ eine dreistellige Zahl ist. Alles
// Zahl xxx >= 500 weist auf einen Fehler hin.
// Die Antwort kann mehrere Zeilen umfassen, die alle mit „xxx“ beginnen, außer der letzten
// in der Form xxx (Leerzeichen)
// Die ausgetauschten Textzeilen müssen mit den Zeichen RC(#13) und LF(#10) enden
//
//  SMTP-Client (SendMail-Übertragungsprotokoll) zum Senden einer Nachricht
//
// Fehlerbehandlung
//ini_set („error_reporting“, E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);
//
// Parameter für den E-Mail-Versand
const CONFIG_FILE_NAME = "config-smtp-01.json";

// die Konfiguration wird abgerufen
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);

// Versenden von E-Mails
foreach ($mails as $name => $infos) {
  // Nachverfolgung
  print "Envoi du mail [$name]\n";
  // Versenden der E-Mail
  $résultat = sendmail($name, $infos, TRUE);
  // Anzeige des Ergebnisses
  print "$résultat\n";
}//für
// Ende
exit;

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

function sendmail(string $name, array $infos, bool $verbose = TRUE): string {
  // sendet Nachricht [$name,$infos]. Wenn $verbose=TRUE    , wird der Austausch zwischen Client und Server nachverfolgt
  // wird der Name des Clients abgerufen
  $client = gethostbyaddr(gethostbyname(""));
  // Aufbau einer Verbindung zum Server SMTP
  $connexion = fsockopen($infos["smtp-server"], (int) $infos["smtp-port"]);
  // Rückgabe bei Fehler
  if ($connexion === FALSE) {
    return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
  }
  // $connexion stellt einen bidirektionalen Kommunikationsfluss dar
  // zwischen dem Client (diesem Programm) und dem kontaktierten SMTP-Server
  // Dieser Kanal wird für den Austausch von Befehlen und Informationen genutzt
  // Nach dem Verbindungsaufbau sendet der Server eine Begrüßungsnachricht, die gelesen wird
  $erreur = sendCommand($connexion, "", $verbose, TRUE);
  if ($erreur !== "") {
    // Beenden der Verbindung
    fclose($connexion);
    // Zurück
    return $erreur;
  }
  // Befehl EHLO
  $erreur = sendCommand($connexion, "EHLO $client", $verbose, TRUE);
  if ($erreur !== "") {
    // Verbindung wird geschlossen
    fclose($connexion);
    // Zurück
    return $erreur;
  }
  // Befehl MAIL FROM:
  $erreur = sendCommand($connexion, sprintf("MAIL FROM: <%s>", $infos["from"]), $verbose, TRUE);
  if ($erreur !== "") {
    // Verbindung beenden
    fclose($connexion);
    // Zurück
    return $erreur;
  }
  // Befehl RCPT TO:
  $erreur = sendCommand($connexion, sprintf("RCPT TO: <%s>", $infos["to"]), $verbose, TRUE);
  if ($erreur !== "") {
    // Verbindung wird geschlossen
    fclose($connexion);
    // Zurück
    return $erreur;
  }
  // Befehl DATA  
  $erreur = sendCommand($connexion, "DATA", $verbose, TRUE);
  if ($erreur !== "") {
    // Verbindung wird geschlossen
    fclose($connexion);
    // Zurück
    return $erreur;
  }
  // Vorbereitung der zu sendenden Nachricht
  // sie muss folgende Zeilen enthalten
  // From: Absender
  // An: Empfänger
  // Betreff:
  // Leere Zeile
  // Nachricht
  // .
  $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 !== "") {
    // Verbindung geschlossen
    fclose($connexion);
    // Zurück
    return $erreur;
  }
  // Befehl „quit“
  $erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
  if ($erreur !== "") {
    // Verbindung wird geschlossen
    fclose($connexion);
    // Zurück
    return $erreur;
  }
  // Ende
  fclose($connexion);
  return "Message envoyé";
}

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

function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF): string {
  // sendet $commande an den Kanal $connexion
  // Ausführlicher Modus, wenn $verbose=1
  // Wenn $withRCLF=1, fügt die Sequenz RCLF zum Austausch hinzu
  // Daten
  if ($withRCLF) {
    $RCLF = "\r\n";
  } else {
    $RCLF = "";
  }
  // Befehl senden, wenn $commande nicht leer ist
  if ($commande!=="") {
    fputs($connexion, "$commande$RCLF");
    // eventuelles Echo
    if ($verbose) {
      affiche($commande, 1);
    }
  }//if
  // Antwort lesen
  $réponse = fgets($connexion, 1000);
  // eventuelles Echo
  if ($verbose) {
    affiche($réponse, 2);
  }
  // Fehlercode abrufen
  $codeErreur = (int) substr($réponse, 0, 3);
  // Letzte Zeile der Antwort?
  while (substr($réponse, 3, 1) === "-") {
    // Antwort lesen
    $réponse = fgets($connexion, 1000);
    // eventuelles Echo
    if ($verbose) {
      affiche($réponse, 2);
    }
  }//while
  // Antwort abgeschlossen
  // Vom Server zurückgegebenen Fehler?
  if ($codeErreur >= 500) {
    return substr($réponse, 4);
  }
// Rückgabe ohne Fehler
  return "";
}

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

function affiche($échange, $sens) {
  // zeigt $échange auf dem Bildschirm an
  // wenn $sens=1, wird -->$echange angezeigt
  // wenn $sens=2, zeigt es <-- $échange ohne die letzten beiden Zeichen an: RCLF
  switch ($sens) {
    case 1:
      print "--> [$échange]\n";
      break;
    case 2:
      $L = strlen($échange);
      print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
      break;
  }//-Switch
}

Anmerkungen

  • Zeile 39: Die Konfigurationsdatei wird ausgewertet;
  • Zeile 42: Es wird eine Schleife über die Elemente des Arrays [mails] durchlaufen. Jedes Element ist ein Dictionary [name=>infos], wobei [name] ein beliebiger Name ist und [infos] ein Dictionary, das die für den Versand einer E-Mail erforderlichen Informationen enthält;
  • Zeile 46: Der Versand der E-Mail erfolgt über die Funktion [sendmail], die drei Parameter akzeptiert:
    • $name: der Name, der diesem Versand zugewiesen wurde;
    • $infos: das Wörterbuch mit den für den Versand erforderlichen Informationen;
    • verbose: Ein boolescher Wert, der angibt, ob der Client-Server-Datenaustausch in der Konsole protokolliert werden soll oder nicht;
  • Zeile 46: Die Funktion [sendmail] gibt eine Fehlermeldung zurück, die leer ist, wenn kein Fehler aufgetreten ist;
  • Zeile 56: Die Funktion [sendmail] sendet die verschiedenen Befehle, die ein SMTP-Client senden muss:
    • Zeilen 77–84: der Befehl EHLO;
    • Zeilen 85–92: der Befehl MAIL FROM: ;
    • Zeilen 93–100: den Befehl RCPT TO: ;
    • Zeilen 101–108: der Befehl DATA;
    • Zeilen 117–124: Versand der Nachricht (Absender, Empfänger, Betreff, Text);
    • Zeilen 125–132: der Befehl QUIT;
  • Zeile 140: Die Funktion [sendCommand] ist dafür zuständig, die Befehle des Clients an den Server SMTP zu senden. Sie akzeptiert vier Parameter:
    • [$connexion]: die Verbindung zwischen Client und Server;
    • [$commande]: der zu sendende Befehl;
    • [$verbose]: Wenn TRUE, wird der Datenaustausch zwischen Client und Server in der Konsole protokolliert;
    • [$withRCLF]: Wenn TRUE, wird der Befehl mit der Sequenz \r\n abgeschlossen gesendet. Dies ist für alle Befehle des Protokolls SMTP erforderlich, aber [sendCommand] dient ebenfalls zum Senden der Nachricht. Hier wird die Sequenz \r\n nicht hinzugefügt;
  • Zeilen 150–157: Der Befehl wird an den Server gesendet;
  • Zeilen 158–163: Auslesen der ersten Zeile der Antwort. Diese kann mehrere Zeilen umfassen. Jede Zeile hat die Form XXX-YYY, wobei XXX ein numerischer Code ist, mit Ausnahme der letzten Zeile der Antwort, die die Form XXX YYY hat (ohne das Zeichen -);
  • Zeilen 167–174: Einlesen aller Zeilen der Antwort;
  • Zeile 177: Wenn der numerische Code XXX größer als 500 ist, hat der Server einen Fehler zurückgegeben;

Ergebnisse

Die Ausführung des Skripts liefert folgende Konsolenergebnisse:


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.
  • Zeilen 1–26: Die Nutzung der Server SMTP und [hMailServer] zum Versenden einer E-Mail an [guest@localhost] verläuft erfolgreich;
  • Zeilen 27–52: Die Nutzung der Server SMTP und [hMailServer] zum Versenden einer E-Mail an [php7parlexemple@gmail.com] verläuft reibungslos;
  • Zeilen 53–65: Die Nutzung der Server SMTP und [Gmail] zum Versenden einer E-Mail an [php7parlexemple@gmail.com] verläuft nicht erfolgreich: In Zeile 65 sendet der Server SMTP einen Fehlercode 530 mit der Fehlermeldung. Diese besagt, dass sich der Client SMTP zuvor über eine sichere Verbindung authentifizieren muss. Unser Client hat dies nicht getan und wird daher abgelehnt;

16.5.6. Ein zweiter Client mit der Kennung SMTP schreibt mit der Bibliothek [SwiftMailer]

Der vorherige Client weist mindestens zwei Mängel auf:

  • Er kann keine sichere Verbindung herstellen, wenn der Server dies verlangt;
  • er kann der Nachricht keine Anhänge hinzufügen;

In unserem neuen Skript werden wir die Bibliothek [SwiftMailer] [https://swiftmailer.symfony.com/] (Mai 2019) verwenden. Die Installation von [SwiftMailer] wird in URL [https://swiftmailer.symfony.com/docs/introduction.HTML] (Mai 2019) beschrieben.

Starten Sie zunächst Laragon:

Image

  • in [1] ein Terminal öffnen;

Image

  • in [3], vergewissern Sie sich, dass Sie sich im Ordner [<laragon>/www] befinden, wobei <laragon> der Installationsordner von Laragon ist;
  • Geben Sie unter [3] den angegebenen Befehl ein (Mai 2019). Überprüfen Sie unter URL und [https://swiftmailer.symfony.com/docs/introduction.HTML] den genauen Befehl;
  • In [4] wird angezeigt, dass keine Installation oder Aktualisierung durchgeführt wurde. Das liegt daran, dass die Bibliothek bereits auf diesem Rechner installiert war;
  • in [5] der Installationsordner von [swiftmailer] und [6];
  • in [7] eine Datei, die wir in unserem Skript benötigen;

Überprüfen Sie anschließend, ob sich der Ordner „[<laragon>/www/vendor] [5]“ tatsächlich im Zweig „[Include Path]“ von NetBeans befindet (siehe Abschnitt „Link“).

Schließlich erfordert die Bibliothek „[SwiftMailer]“, dass die Erweiterung „PHP [mbstring]“ aktiv ist. Dazu überprüfen wir die Datei „[php.ini]“ (siehe Abschnitt „Link“):

Image

Das Skript [smtp-02.php] verwendet die folgende Konfigurationsdatei 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"
        ]
    }
}

Es sind dieselben Abschnitte wie in der Datei [config-smtp-01.json] enthalten, ergänzt um zwei weitere Abschnitte:

  • [tls]: Bei TRUE gibt an, dass eine sichere Verbindung zum Server SMTP verwendet werden muss. Falls [tls] den Wert TRUE hat, müssen zwei Felder hinzugefügt werden:
    • [user]: den Benutzernamen, der die Verbindung authentifiziert;
    • [password]: sein Passwort;

In unserem Beispiel haben wir die Anmeldedaten des Benutzers [php7parlexemple@gmail.com] verwendet, um uns beim Gmail-Server anzumelden. Verwenden Sie Ihre eigenen;

  • [attachments]: gibt die Namen der Dateien an, die an die E-Mail angehängt werden sollen;

Der Code des Skripts [smtp-02.php] lautet wie folgt:


<?php

// Client SMTP (SendMail Transfer Protocol) zum Senden einer Nachricht
//
// Fehlerbehandlung
//ini_set („error_reporting“, E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// Abhängigkeiten
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
//
// Einstellungen für den E-Mail-Versand
const CONFIG_FILE_NAME = "config-smtp-02.json";

// Konfiguration abrufen
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);

// Versenden von E-Mails
foreach ($mails as $name => $infos) {
  // Nachverfolgung
  print "Envoi du mail [$name]\n";
  // Versand der E-Mail
  $résultat = sendmail($name, $infos);
  // Anzeige des Ergebnisses
  print "$résultat\n";
}//für
// Ende
exit;

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

function sendmail($name, $infos) {

  // sendet $infos[message] an den SMTP-Server $infos[smtp-server] auf dem Port $infos[smt-port]
  // Wenn $infos[tls] wahr ist, wird das Format TLS verwendet
  // Die E-Mail wird im Namen von $infos[from] versendet
  // an den Empfänger $infos['to']
  // Das Dokument $info[attachment] ist der Nachricht angehängt
  // Die Nachricht hat den Betreff $infos[subject]
  //
  // Nachricht im Format HTML
  $messageHTML = str_replace("\n", "<br/>", $infos["message"]);
  try {
    // Erstellung der Nachricht
    $message = (new \Swift_Message())
      // Betreff der Nachricht
      ->setSubject($infos["subject"])
      // Absender
      ->setFrom($infos["from"])
      // Empfänger mit einem Adressbuch (setTo/setCc/setBcc)
      ->setTo($infos["to"])
      // Nachrichtentext
      ->setBody($infos["message"])
      // HTML-Variante
      ->addPart("<b>$messageHTML</b>", 'text/html')
    ;
    // Anhänge
    foreach ($infos["attachments"] as $attachment) {
      // Pfad zum Anhang
      $fileName = __DIR__ . $attachment;
      // Es wird überprüft, ob die Datei vorhanden ist
      if (file_exists($fileName)) {
        // Das Dokument wird an die Nachricht angehängt
        $message->attach(\Swift_Attachment::fromPath($fileName));
      } else {
        // Fehler
        print "L'attachement [$fileName] n'existe pas\n";
      }
    }
    // Protokoll TLS?
    if ($infos["tls"] === "TRUE") {
      // TLS
      $transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"], 'tls'))
        ->setUsername($infos["user"])
        ->setPassword($infos["password"]);
    } else {
      // kein TLS
      $transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"]));
    }
    // der Versandmanager
    $mailer = new \Swift_Mailer($transport);
    // Versenden der Nachricht
    $result = $mailer->send($message);
    // Ende
    return "Message [$name] envoyé";
  } catch (\Throwable $ex) {
    // Fehler
    return "Erreur lors de l'envoi du message [$name] : " . $ex->getMessage();
  }
}

Kommentare

  • Zeile 10: Wir laden die Datei [autoload.php], die sich im Ordner [<lagagon>/www/vendor] befindet, wobei <laragon> der Installationsordner von Laragon ist. Diese Datei ermöglicht es, die Klassendefinitionsdateien von [SwiftMailer] bereits bei der ersten Verwendung dieser Klassen zu laden. Dadurch müssen wir nicht für jede Klasse und jedes Interface von SwiftMailer, die wir verwenden werden, eine eigene [require]-Datei erstellen;
  • Zeile 32: Die neue Funktion [sendmail], die zwei Parameter hat:
    • [$name], das dazu dient, die Nachrichten voneinander zu unterscheiden;
    • [$infos]: die erforderlichen Informationen zum Versenden der Nachricht an den Empfänger;
  • Zeile 42: Es gibt zwei Versionen der Nachricht: eine im Klartext und eine im Format HTML. Hier ändern wir die Zeilenendezeichen in den Code HTML <br/>;
  • Zeilen 45–69: Wir definieren die Nachricht mithilfe der Klasse [\SwiftMessage];
  • Zeile 47: Die Methode [SwiftMessage→setSubject] dient dazu, den Betreff der Nachricht festzulegen;
  • Zeile 49: Die Methode [SwiftMessage→setFrom] dient dazu, den Absender der Nachricht festzulegen;
  • Zeile 51: Die Methode [SwiftMessage→setTo] dient dazu, den Empfänger der Nachricht festzulegen;
  • Zeile 53: Die Methode [SwiftMessage→setBody] dient dazu, den Nachrichtentext festzulegen;
  • Zeile 55: Die Methode [SwiftMessage→addPart] dient dazu, verschiedene Versionen der Nachricht festzulegen, hier die Nachricht im Format HTML. Wenn die Nachricht Varianten enthält, zeigen E-Mail-Programme die vom Benutzer bevorzugte Variante an;
  • Zeilen 58–69: Die Methode [SwiftMessage→addAttachment] (64) ermöglicht es, eine Datei an die Nachricht anzuhängen;
  • Zeilen 70–79: Sobald die zu versendende Nachricht definiert ist, muss festgelegt werden, wie sie versendet werden soll. Der Transportmodus der Nachricht wird durch die Klasse [\Swift_SmtpTransport] definiert. Es müssen mindestens zwei Angaben gemacht werden: die nom und die port des Servers SMTP. Es gibt noch eine dritte Angabe: Verlangt der Server SMTP eine sichere Authentifizierung?
  • Zeilen 73–75: die Instanz [\Swift_SmtpTransport] für eine sichere Verbindung zum Server SMTP;
  • Zeile 78: die Instanz [\Swift_SmtpTransport] für eine ungesicherte Verbindung zum Server SMTP;
  • Zeile 81: Die Klasse [\SwiftMailer] versendet die Nachrichten. Ihr muss der gewählte Transportmodus übergeben werden;
  • Zeile 83: Die Nachricht [\SwiftMessage] wird über den ausgewählten Transport [\Swift_SmtpTransport] gesendet. Die Methode [SwiftMailer→send] gibt den Booleschen Wert FALSE zurück, wenn die Nachricht nicht gesendet werden konnte;
  • Zeilen 86–89: Die Bibliothek [SwiftMailer] löst eine Ausnahme aus, sobald etwas schiefgeht;

Hinweis: Es ist zu beachten, dass der Namensraum der Klassen der Bibliothek [SwiftMailer] die Wurzel \ ist. Die Klassen [\SwiftMessage, \Swift_SmtpTransport, \SwiftMailer] wurden explizit angegeben, um darauf hinzuweisen;

Ergebnisse

Bei der Ausführung des Skripts [smtp-02.php] erhält man die folgenden Konsolenausgaben:

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é

Wenn man das Gmail-Konto des Nutzers [php7parlexemple] aufruft, sieht man Folgendes:

Image

  • in [1], der Betreff;
  • bei [2] der Absender;
  • in [3] der Empfänger;
  • in [4] die Nachricht;
  • in [5-10] die Anhänge;

Wenn man die Originalnachricht anzeigt, erhält man das folgende Dokument:


Return-Path: <php7parlexemple@gmail.com>
Received: from [127.0.0.1] (lfbn-1-11924-110.w90-93.abo.wanadoo.fr. [90.93.230.110])
        by smtp.gmail.com with ESMTPSA id e14sm7773816wma.41.2019.05.26.03.11.53
        for <php7parlexemple@gmail.com>
        (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
        Sun, 26 May 2019 03:11:54 -0700 (PDT)
Message-ID: <e613c47a421a66e2cf7f8e319616ec49@swift.generated>
Date: Sun, 26 May 2019 10:11:53 +0000
Subject: test-gmail-via-gmail
From: php7parlexemple@gmail.com
To: php7parlexemple@gmail.com
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_"

--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: multipart/alternative; boundary="_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_"

--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

ligne 1
ligne 2
ligne 3

--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_
Content-Type: text/HTML; charset=utf-8
Content-Transfer-Encoding: quoted-printable

<b>ligne 1<br/>ligne 2<br/>ligne 3</b>

--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_--
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document; name="Hello from SwiftMailer.docx"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.docx"


--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: application/pdf; name="Hello from SwiftMailer.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.pdf"


--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: application/vnd.oasis.opendocument.text; name="Hello from SwiftMailer.odt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.odt"


--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: image/png; name="Cours-Tutoriels-Serge-Tahé-1568x268.png"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Cours-Tutoriels-Serge-Tahé-1568x268.png"


--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: message/rfc822; name=test-localhost.eml
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=test-localhost.eml

Return-Path: guest@localhost
Received: from [127.0.0.1] (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Sat, 25 May 2019 09:48:23 +0200
Message-ID: <620f4628882b011feebe4faa30b45092@swift.generated>
Date: Sat, 25 May 2019 07:48:22 +0000
Subject: test-localhost
From: guest@localhost
To: guest@localhost
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_"

--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: multipart/alternative; boundary="_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_"

--_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

j'ai =C3=A9t=C3=A9 invit=C3=A9 =C3=A0 d=C3=A9je=C3=BBner

--_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_
Content-Type: text/HTML; charset=utf-8
Content-Transfer-Encoding: quoted-printable

<b>j'ai =C3=A9t=C3=A9 invit=C3=A9 =C3=A0 d=C3=A9je=C3=BBner</b>

--_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_--
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document; name="Hello from SwiftMailer.docx"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.docx"


--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: application/pdf; name="Hello from SwiftMailer.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.pdf"


--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: application/vnd.oasis.opendocument.text; name="Hello from SwiftMailer.odt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.odt"


--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: image/png; name="Cours-Tutoriels-Serge-Tahé-1568x268.png"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Cours-Tutoriels-Serge-Tahé-1568x268.png"


--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_--

--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_--

  • Zeile 9: der Betreff;
  • Zeile 10: der Absender;
  • Zeile 11: der Empfänger;
  • Zeile 13: Die Nachricht enthält mehrere Abschnitte, die durch die Tags [--_=_swift_xx] abgegrenzt sind;
  • Zeilen 19–24: die Nachricht im Klartext;
  • Zeilen 27–30: die Nachricht im Format HTML;
  • Zeilen 34–36: die angehängte Datei [Hello from SwiftMailer.docx];
  • Zeilen 40–42: die angehängte Datei [Hello from SwiftMailer.pdf];
  • Zeilen 46–48: die angehängte Datei [Hello from SwiftMailer.odt];
  • Zeilen 58–60: die angehängte Datei [Cours-Tutoriels-Serge-Tahé-1568x268.png];
  • Zeilen 58–60: die angehängte Datei [test-localhost.eml];
  • Zeilen 62–114: Die angehängte Datei [test-localhost.eml] ist selbst eine Nachricht, deren Inhalt in den Zeilen 62–114 angezeigt wird. Es ist zu erkennen, dass diese Nachricht ihrerseits Anhänge enthält;

16.6. Die Protokolle POP3 (Post Office Protocol) und IMAP (Internet Message Access Protocol)

16.6.1. Einleitung

Um die auf einem Mailserver gespeicherten E-Mails zu lesen, gibt es zwei Protokolle:

  • das Protokoll POP3 (Post Office Protocol), historisch gesehen das erste Protokoll, das heute jedoch kaum noch verwendet wird;
  • das Protokoll IMAP (Internet Message Access Protocol), das neuer ist als POP3 und derzeit am häufigsten verwendet wird;

Um das Protokoll POP3 kennenzulernen, verwenden wir die folgende Architektur:

Image

  • [Serveur B] ist ein lokaler Server POP3 / IMAP, der vom Mailserver [hMailServer] implementiert wird;
  • [Client A] ist ein Client POP3 / IMAP in verschiedenen Formen:
    • der Client [RawTcpClient] zur Erkundung des Protokolls POP3;
    • ein Skript PHP, das das Protokoll POP3 des Clients [RawTcpClient] nachbildet;
    • ein Skript PHP, das die Bibliothek IMAP von PHP verwendet, mit der sowohl Clients vom Typ IMAP als auch vom Typ POP3 implementiert werden können;

16.6.2. Einführung in das Protokoll POP3

Zunächst verwenden wir das Skript [smtp-01.php], um eine E-Mail an den Benutzer [guest@localhost] zu senden. Wenn Sie die zum Skript gehörenden Tests durchgeführt haben, hat dieser Benutzer normalerweise E-Mails erhalten, was wir jedoch nicht überprüfen konnten. Um ihm eine neue E-Mail zu senden, verwenden Sie beispielsweise die folgende Konfigurationsdatei [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"
    }
}

Sehen wir uns nun mit dem Client [RawTcpClient] an, wie man das Postfach des Benutzers [guest@localhost] einsehen kann:


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…
  • Zeile 1: Der Server POP3 arbeitet in der Regel mit Port 110. Das ist hier der Fall;
  • Zeile 5: Der Befehl [USER] dient dazu, den Benutzer anzugeben, dessen E-Mail-Postfach man lesen möchte;
  • Zeile 7: Der Befehl [PASS] dient dazu, das Passwort festzulegen;
  • Zeile 9: Der Befehl [LIST] fordert die Liste der im Postfach des Benutzers vorhandenen Nachrichten an;
  • Zeile 14: Der Befehl [RETR] fordert die Anzeige der Nachricht an, deren Nummer übergeben wird;
  • Zeile 29: Der Befehl [DELE] fordert das Löschen der Nachricht an, deren Nummer übergeben wird;
  • Zeile 40: Der Befehl [QUIT] teilt dem Server mit, dass der Vorgang abgeschlossen ist;

Die Antwort des Servers kann verschiedene Formen annehmen:

  • eine einzelne Zeile, die mit [+OK] beginnt, um anzuzeigen, dass der vorherige Befehl des Clients erfolgreich war;
  • eine einzelne Zeile, die mit [-ERR] beginnt, um anzuzeigen, dass der vorherige Befehl des Clients fehlgeschlagen ist;
  • mehrere Zeilen, wobei:
    • die erste Zeile mit [+OK] beginnt;
    • die letzte Zeile aus einem einzigen Punkt besteht;

16.6.3. Ein einfaches Skript zur Implementierung des Protokolls POP3

Image

Da das Protokoll POP3 denselben Aufbau wie das Protokoll SMTP hat, ist das Skript [pop3-01.php] eine Portierung des Skripts [smtp-01.php]. Es wird die folgende Konfigurationsdatei [config-pop3-01.json] haben:

1
2
3
4
5
6
7
8
9
{
    "localhost:110": {
        "server": "localhost",
        "port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "maxmails":5
    }
}
  • Zeilen 3–4: Der abgefragte Server POP3 ist der lokale Server [hMailServer];
  • Zeilen 5–6: Es soll das Postfach des Benutzers [guest@localhost] gelesen werden;
  • Zeile 7: Es werden maximal 5 E-Mails gelesen;

Das Skript [pop3-01.php] lautet wie folgt:


<?php

// POP3-Client (Post Office Protocol) zum Abrufen von Nachrichten aus einem Postfach
// Kommunikationsprotokoll    POP3 Client-Server-Kommunikationsprotokoll
// -> Client verbindet sich über Port 110 mit dem SMTP-Server
// <- Server sendet ihm eine Begrüßungsnachricht
// -> Der Client sendet den Befehl USER „Benutzer“
// <- Der Server antwortet mit „OK“ oder nicht
// -> Der Client sendet den Befehl PASS mot_de_passe
// <- Server antwortet mit OK oder nicht
// -> Der Client sendet den Befehl LIST
// <- Server antwortet mit OK oder nicht
// -> Der Client sendet den Befehl RETR (mit einer Nummer für jede E-Mail)
// <- Der Server antwortet entweder mit „OK“ oder nicht. Wenn „OK“ zurückkommt, wird der Inhalt der angeforderten E-Mail gesendet
// -> Der Server sendet alle Zeilen der E-Mail und schließt mit einer Zeile ab, die das
// einzige Zeichen.
// -> Der Client sendet den Befehl DELE mit der Nummer, um eine E-Mail zu löschen
// <- Server antwortet mit OK oder nicht
// // -> Der Client sendet den Befehl QUIT, um den Dialog mit dem Server zu beenden
// <- Server antwortet mit OK oder nicht
// Die Antworten des Servers haben das Format +OK Text oder -ERR Text
// Die Antwort kann mehrere Zeilen umfassen. In diesem Fall besteht die letzte Zeile aus einem einzigen Punkt
// Die ausgetauschten Textzeilen müssen mit den Zeichen RC(#13) und LF(#10) enden
//
// Client POP3 (SendMail-Übertragungsprotokoll) zum Lesen von E-Mails
//
// Fehlerbehandlung
//ini_set („error_reporting“, E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);
//
// Parameter für den E-Mail-Versand
const CONFIG_FILE_NAME = "config-pop3-01.json";

// die Konfiguration wird abgerufen
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);

// Lesen der Postfächer
foreach ($mailboxes as $name => $infos) {
  // Nachverfolgung
  print "Lecture de la boîte à lettres [$name]\n";
  // Lesen des Postfachs
  $résultat = readmail($name, $infos, TRUE);
  // Anzeige des Ergebnisses
  print "$résultat\n";
}//für
// Ende
exit;

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

function readmail(string $name, array $infos, bool $verbose = TRUE): string {
  // Liest den Inhalt des Postfachs [$name]
  // importiert alle Nachrichten
  // Jede Nachricht wird nach dem Lesen gelöscht
  // Wenn $verbose=1 ist, protokolliert es den Datenverkehr zwischen Client und Server
  //
  // Aufbau einer Verbindung zum Server SMTP
  $connexion = fsockopen($infos["server"], (int) $infos["port"]);
  // Rückgabe bei Fehler
  if ($connexion === FALSE) {
    return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
  }
  // $connexion stellt einen bidirektionalen Kommunikationsfluss dar
  // zwischen dem Client (diesem Programm) und dem kontaktierten POP3-Server
  // Dieser Kanal wird für den Austausch von Befehlen und Informationen genutzt
  // Nach dem Verbindungsaufbau sendet der Server eine Begrüßungsnachricht, die gelesen wird
  $erreur = sendCommand($connexion, "", $verbose, TRUE);
  if ($erreur !== "") {
    // Beenden der Verbindung
    fclose($connexion);
    // Zurück
    return $erreur;
  }
  // Befehl USER
  $erreur = sendCommand($connexion, "USER {$infos["user"]}", $verbose, TRUE);
  if ($erreur !== "") {
    // Verbindung wird geschlossen
    fclose($connexion);
    // Zurück
    return $erreur;
  }
  // Befehl PASS
  $erreur = sendCommand($connexion, "PASS {$infos["password"]}", $verbose, TRUE);
  if ($erreur !== "") {
    // Verbindung geschlossen
    fclose($connexion);
    // Zurück
    return $erreur;
  }
  // Befehl LIST
  $premièreLigne = "";
  $erreur = sendCommand($connexion, "LIST", $verbose, TRUE, $premièreLigne);
  if ($erreur !== "") {
    // Verbindung schließen
    fclose($connexion);
    // Zurück
    return $erreur;
  }
  // Analyse der ersten Zeile, um die Anzahl der Nachrichten zu ermitteln
  $champs = [];
  preg_match("/^\+OK (\d+)/", $premièreLigne, $champs);
  $nbMessages = (int) $champs[1];
  // Schleife über die Nachrichten
  $iMessage = 0;
  while ($iMessage < $nbMessages && $iMessage < $infos["maxmails"]) {
    // Befehl RETR  
    $erreur = sendCommand($connexion, "RETR " . ($iMessage + 1), $verbose, TRUE);
    if ($erreur !== "") {
      // Schließen der Verbindung
      fclose($connexion);
      // Rückkehr
      return $erreur;
    }
    // Befehl DELE
    $erreur = sendCommand($connexion, "DELE " . ($iMessage + 1), $verbose, TRUE);
    if ($erreur !== "") {
      // Verbindung schließen
      fclose($connexion);
      // Zurück
      return $erreur;
    }
    // nächste Nachricht
    $iMessage++;
  }
  // Befehl QUIT
  $erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
  if ($erreur !== "") {
    // Verbindung wird geschlossen
    fclose($connexion);
    // Zurück
    return $erreur;
  }
  // Ende
  fclose($connexion);
  return "Terminé";
}

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

function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF, string &$premièreLigne = ""): string {
  // sendet $commande an den Kanal $connexion
  // Ausführlicher Modus, wenn $verbose=1
  // Wenn $withRCLF=1, fügt die Sequenz RCLF zum Austausch hinzu
  // Fügt die erste Zeile der Antwort in [$premièreLigne] ein
  // ]
  // Daten
  if ($withRCLF) {
    $RCLF = "\r\n";
  } else {
    $RCLF = "";
  }
  // Befehl senden, wenn $commande nicht leer ist
  if ($commande !== "") {
    fputs($connexion, "$commande$RCLF");
    // eventuelles Echo
    if ($verbose) {
      affiche($commande, 1);
    }
  }//if
  // Antwort lesen
  $réponse = fgets($connexion, 1000);
  // die erste Zeile wird gespeichert
  $premièreLigne = $réponse;
  // eventuelles Echo
  if ($verbose) {
    affiche($réponse, 2);
  }
  // Fehlercode abrufen
  $codeErreur = substr($réponse, 0, 1);
  if ($codeErreur === "-") {
    // Es ist ein Fehler aufgetreten
    return substr($réponse, 5);
  }
  // Sonderfälle der Befehle RETR und LIST, deren Antworten aus mehreren Zeilen bestehen
  $commande = substr(strtolower($commande), 0, 4);
  if ($commande === "list" || $commande === "retr") {
    // Letzte Zeile der Antwort?
    $champs = [];
    $match = preg_match("/^\.\s+$/", $réponse, $champs);
    while (!$match) {
      // Antwort auslesen
      $réponse = fgets($connexion, 1000);
      // Mögliches Echo
      if ($verbose) {
        affiche($réponse, 2);
      }
      // Antwort auswerten
      $champs = [];
      $match = preg_match("/^\.\s+$/", $réponse, $champs);
    }//while
  }
  // Rückgabe ohne Fehler
  return "";
}

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

function affiche($échange, $sens) {
  // zeigt $échange auf dem Bildschirm an
  // wenn $sens=1, wird angezeigt -->$echange
  // wenn $sens=2, wird <-- $échange ohne die letzten beiden Zeichen RCLF angezeigt
  switch ($sens) {
    case 1:
      print "--> [$échange]\n";
      break;
    case 2:
      $L = strlen($échange);
      print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
      break;
  }//-Switch
}

Anmerkungen

Wie bereits erwähnt, handelt es sich bei [pop3-01.php] um eine Portierung des Skripts [smtp-01.php], das wir bereits erläutert haben. Wir werden hier nur auf die wichtigsten Unterschiede eingehen:

  • Zeile 55: Die Funktion [readmail] ist dafür zuständig, die E-Mails aus dem Postfach zu lesen. Die Informationen für die Verbindung zu diesem Postfach befinden sich im Wörterbuch [$infos];
  • Zeilen 61–66: Aufbau einer Verbindung zum Server POP3;
  • Zeilen 71–77: Lesen der vom Server gesendeten Begrüßungsnachricht;
  • Zeilen 78–85: Der Befehl [USER] wird gesendet, um den Benutzer zu identifizieren, dessen E-Mails abgerufen werden sollen;
  • Zeilen 86–93: Der Befehl [PASS] wird gesendet, um das Passwort dieses Benutzers anzugeben;
  • Zeilen 94–102: Der Befehl [LIST] wird gesendet, um zu ermitteln, wie viele E-Mails sich im Postfach dieses Benutzers befinden.
  • Zeile 96: Der Parameter [$premièreLigne] wird zu den Parametern der Funktion [readmail] hinzugefügt. In der ersten Zeile seiner Antwort auf den Befehl LIST gibt der Server an, wie viele Nachrichten sich im Postfach befinden;
  • Zeilen 104–106: Die Anzahl der Nachrichten wird aus der ersten Zeile der Antwort abgerufen;
  • Zeilen 109–128: Es wird eine Schleife über alle Nachrichten durchlaufen. Für jede davon werden zwei Befehle gesendet:
    • RETR i: um die Nachricht Nr. i abzurufen (Zeilen 111–117);
    • DELE i: um sie zu löschen, sobald sie gelesen wurde (Zeilen 118–125);
  • Zeilen 129–136: Der Befehl [QUIT] wird gesendet, um dem Server mitzuteilen, dass der Vorgang abgeschlossen ist;
  • Zeilen 178–194: Bei den Befehlen [LIST] und [RETR] umfasst die Antwort des Servers mehrere Zeilen, wobei die letzte Zeile aus einem einzigen Punkt besteht;

Ergebnisse

Bei der Ausführung erhält man folgende Ergebnisse:


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.

Hier haben wir einen einfachen POP3-Client, dem bestimmte Funktionen fehlen:

  1. die Möglichkeit, mit einem gesicherten POP3-Server zu kommunizieren;
  2. die Möglichkeit, an eine Nachricht angehängte Anhänge zu lesen;

Wir werden die erste Möglichkeit mit den Funktionen [imap] aus PHP implementieren.

16.6.4. POP3-/IMAP-Client, implementiert mit den Funktionen [imap] aus PHP

Zunächst müssen wir überprüfen, ob die Funktionen [imap] in der von uns verwendeten Version von PHP verfügbar sind. Wir öffnen die im Abschnitt „Link“ beschriebene Datei [php.ini] und suchen nach den Zeilen, die sich auf [imap] beziehen:

Image

Zeile 895: Überprüfen Sie, ob die Erweiterung [imap] aktiviert ist.

Das Skript [imap-01.php] wird die folgende Datei jSON [config-imap-01.json] verarbeiten:

{

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

Die Datei „[config-imap-01.json]“ definiert ein Array von zu kontaktierenden Servern „IMAP“ / „POP3“. Jedes Element ist eine Struktur „[clé:valeur]“, wobei:

  • [clé]: der zu kontaktierende Server ist. Hier gibt es zwei davon:
    • [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]: Bezeichnet den Server [imap.gmail.com], der auf Port 993 lauscht. Das Client-Server-Protokoll lautet IMAP. Der Parameter /ssl gibt an, dass die Client-Server-Kommunikation gesichert ist. Der Parameter /novalidate-cert weist den Client an, das vom Server gesendete Sicherheitszertifikat nicht zu überprüfen. Schließlich verwaltet ein Server mit der Bezeichnung IMAP eine Reihe von Postfächern für denselben Benutzer. Indem wir INBOX im URL des Servers IMAP angeben, weisen wir darauf hin, dass wir uns für das Postfach mit dem Namen INBOX interessieren, in dem normalerweise die neuen Nachrichten eingehen;
    • [{localhost:110/pop3}INBOX]: Bezeichnet den Server [localhost], der auf Port 110 lauscht. Das Client-Server-Protokoll lautet hier POP3;
  • [valeur]: ist ein Wörterbuch, das folgende Punkte präzisiert:
    • [imap-server]: der Name des Servers IMAP oder POP3;
    • [imap-port]: der Port des Servers IMAP oder POP3;
    • [user]: der Eigentümer, dessen Postfach gelesen werden soll;
    • [password]: dessen Passwort;
    • [output-dir]: der Ordner, in dem die Nachrichten gespeichert werden sollen;
    • [prefix]: Die Dateinamen, unter denen die Nachrichten gespeichert werden, haben das Format prefixN, wobei N eine Nachrichtennummer ist;
    • [pop3]: Ein Boolescher Wert, der auf TRUE gesetzt wird, um anzugeben, dass das verwendete Protokoll POP3 ist. In diesem Fall wird eine Nachricht nach dem Lesen gelöscht. Dies entspricht der üblichen Funktionsweise von POP3-Servern: Eine gelesene Nachricht wird nicht auf dem Server gespeichert;

Das Skript [imap-01.php] lautet wie folgt:


<?php

// Client IMAP (Internet Message Access Protocol) zum Lesen von E-Mails
//
// strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);
// Fehlerbehandlung
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
//
// Parameter zum Abrufen von E-Mails
const CONFIG_FILE_NAME = "config-imap-01.json";

// Konfiguration abrufen
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);

// Abrufen der Postfächer
foreach ($mailboxes as $name => $infos) {
  // Nachverfolgung
  print "------------Lecture de la boîte à lettres [$name]\n";
  // Abfrage des Postfachs
  readmailbox($name, $infos);
}
// Ende
exit;

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

function readmailbox(string $name, array $infos): void {
  // Verbindungsversuch
  $imapResource = imap_open($name, $infos["user"], $infos["password"]);
  // Test der Rückgabe der Funktion imap_open()
  if (!$imapResource) {
    // Fehlgeschlagen
    print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
  } else {
    // Verbindung hergestellt
    print "Connexion établie avec le serveur [$name].\n";
    // Gesamtzahl der Nachrichten im Postfach
    $nbmsg = imap_num_msg($imapResource);
    print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
    // Ungelesene Nachrichten im aktuellen Postfach
    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) {
          // Informationen zur Nachricht Nr. $msgNumber werden abgerufen
          $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);
          }
          // Der Text der Nachricht Nr. $msgNumber wird abgerufen
          getMailBody($imapResource, $msgNumber, $infos);

          // Wenn das Protokoll POP3 lautet, wird die Nachricht gelöscht
          $pop3 = $infos["pop3"];
          if ($pop3 !== NULL) {
            // Die Nachricht wird in zwei Schritten gelöscht
            imap_delete($imapResource, $msgNumber);
            imap_expunge($imapResource);
          }
        }
      }
    }
  }
  // Schließen der Verbindung
  $imapClose = imap_close($imapResource);
  if (!$imapClose) {
    // Fehlgeschlagen
    print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
  } else {
    // Erfolg
    print "Fermeture de la connexion réussie.\n";
  }
}

function getMailBody($imapResource, int $msgNumber, array $infos): void {
  // Der Nachrichtentext mit der Nummer $msgNumber wird abgerufen
  $corpsMail = imap_body($imapResource, $msgNumber);

  print "Enregistrement du message dans le fichier {$infos["output-dir"]}/{$infos["prefix"]}$msgNumber\n";
  // Der Ordner wird bei Bedarf angelegt
  if (!file_exists($infos["output-dir"])) {
    mkdir($infos["output-dir"]);
  }
  // Die Nachricht wird gespeichert
  if (!file_put_contents($infos["output-dir"] . "/" . $infos["prefix"] . $msgNumber, $corpsMail)) {
    print "Echec de l'enregistrement\n";
  }
}

Kommentare

  • Zeilen 19–24: Es wird eine Schleife über alle in der Konfigurationsdatei gefundenen Server durchlaufen;
  • Zeile 32: Die Funktion [raedmailbox] liest das in [$name] angegebene Postfach;
  • Zeile 32: Aufbau einer Verbindung IMAP;
    • Der erste Parameter ist die URL IMAP des zu lesenden Postfachs;
    • der zweite Parameter ist der Name des Benutzers, dem dieses Postfach gehört;
    • der dritte Parameter ist dessen Passwort;

Die Funktion [imap_open] sorgt für die Sicherung der Verbindung, wenn die URL IMAP des Postfachs den Parameter /ssl enthält;

  • Zeile 41: Die Funktion [imap_num_msg] liefert die Gesamtzahl der Nachrichten im Postfach;
  • Zeile 46: Mit der Funktion [imap_search] lassen sich bestimmte Nachrichten suchen. Hier suchen wir nach den noch ungelesenen Nachrichten (UNSEEN). Der zweite Parameter ist ein Auswahlkriterium. Davon gibt es gut zwanzig. Die Funktion [imap_search] gibt ein Array mit Nachrichtennummern zurück. Diese können zwei Formen annehmen: Sequenznummer oder Nachrichten-ID (UID). Standardmäßig gibt die Funktion [imap_search] ein Array mit Sequenznummern zurück. Fügt man einen dritten Parameter [SE_UID] hinzu, erhält man die Identifikatoren UID der Nachrichten;
  • Zeile 47: Die Funktion [imap_search] gibt den booleschen Wert FALSE zurück, wenn sie keine Nachricht gefunden hat;
  • Zeile 50: Es wird eine Schleife über alle ungelesenen Nachrichten durchlaufen;
  • Zeile 52: Eine Nachricht verfügt über Kopfzeilen, die mit der Funktion [imap_headerinfo] abgerufen werden können. Ihr zweiter Parameter ist normalerweise eine Nachrichten-Sequenznummer. Wenn man eine Nachrichten-ID UID festlegen möchte, muss der dritte Parameter auf [FT_UID] gesetzt werden;
  • Zeile 53: Die Funktion [imap_headerinfo] gibt den Booleschen Wert FALSE zurück, wenn sie ihre Aufgabe nicht ausführen konnte. Andernfalls gibt sie ein komplexes Objekt zurück, das mit der Funktion [print_r] in Zeile 57 angezeigt wird;
  • Zeile 60: Nach den Kopfzeilen wird nun der Nachrichtentext mit der Funktion [imap_body] abgerufen. Diese Funktion gibt NULL zurück, wenn sie ihre Aufgabe nicht ausführen konnte;
  • Zeilen 84–87: Der Nachrichtentext wird in einer lokalen Datei gespeichert;
  • Zeilen 63–68: Wenn das verwendete Protokoll POP3 war, wird die gerade gelesene Nachricht gelöscht:
    • Die Funktion [imap_delete] markiert die Nachricht als „zu löschen“, löscht sie jedoch nicht;
    • Die Funktion [imap_expunge] löscht alle Nachrichten, die als „zu löschen“ markiert wurden, physisch;
  • Zeile 74: Die Verbindung zum Server IMAP wird geschlossen. Dazu wird die Funktion [imap_close] verwendet;
  • Zeile 86: Mit der Funktion [imap_body] kann der Textkörper einer Nachricht anhand ihrer Nummer abgerufen werden;

Führen wir das Skript „[smtp-02.json]“ aus, damit der Gmail-Benutzer „[php7parlexemple]“ und der Benutzer „[guest]“ von „[localhost]“ neue Nachrichten erhalten. Anschließend führen wir das Skript [imap-01.php] aus, um ihre Postfächer abzurufen.

Die Konsolenausgabe lautet wie folgt:


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

Wenn wir unmittelbar nach diesen Ergebnissen das Skript [imap-01.php] erneut ausführen, lauten die Ergebnisse wie folgt:


------------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.
  • Zeile 3: Es befindet sich immer noch die gleiche Anzahl an Nachrichten im Gmail-Postfach, aber es gibt keine neuen ungelesenen Nachrichten mehr (Zeile 5). Dies zeigt, dass die vorherige Ausführung die gelesenen Nachrichten vom Status „ungelesen“ in den Status „gelesen“ versetzt hat;
  • Zeile 9: Im Postfach des Nutzers [guest@localhost] befinden sich keine Nachrichten mehr. Dies liegt daran, dass bei der vorherigen Ausführung die gelesenen Nachrichten unter [localhost] anschließend gelöscht wurden;

Die Nachrichten wurden lokal gespeichert:

Image

Betrachtet man beispielsweise den Inhalt der Nachricht Nr. 26 in Gmail, so ergibt sich Folgendes:



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

  • Zeilen 11–13: die Nachricht im Klartext;
  • Zeile 19: die Nachricht HTML;
  • Zeile 25: der Anhang;

Versuchen wir, dieses Skript so zu verbessern, dass die verschiedenen Nachrichtentypen sowie die Anhänge in separaten Dateien gespeichert werden.

16.6.5. Verbesserter Client POP3 / IMAP

Im Skript [imap-01.php] wird der Nachrichtentext Nr. i als Textdatei ausgegeben, die sowohl die verschiedenen Nachrichtentypen als auch den verschlüsselten Inhalt der verschiedenen Anhänge enthält. Es ist möglich, die Struktur der Nachricht zu ermitteln, um diese verschiedenen Teile zu identifizieren. Im Skript [imap-02.php] ändern wir die Funktion [getMailBody] wie folgt:


function getMailBody($imapResource, int $msgNumber, array $infos): void {
  // Die Struktur der Nachricht wird abgerufen
  $structure=imap_fetchstructure($imapResource, $msgNumber);
  // Anzeige
  print_r($structure);
}
  • Zeile 3: Wir fordern die Nachrichtenstruktur an;
  • Zeile 5: Wir geben sie aus;

Ziel ist es, die in der Struktur einer Nachricht enthaltenen Informationen zu ermitteln, um zu sehen, wie man die verschiedenen Teile daraus gewinnen kann. In unserem Beispiel wird die Nachricht vom Skript [smtp-02.php] mit der folgenden Konfiguration [config-smtp-02.json] gesendet:

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

Es handelt sich also um eine Nachricht mit fünf Anhängen, die an [guest@localhost] gesendet wird (Zeilen 11–15). Das Skript [imap-02.php] wird mit der folgenden Konfiguration [config-imap-01.json] ausgeführt:

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

Es wird also das Postfach von [guest@localhost] ausgenutzt (Zeile 5). Das Skript [imap-02.php] zeigt daraufhin die Struktur der von [smtp-02.php] gesendeten Nachricht an. Diese Struktur, die in der Konsole angezeigt wird, sieht wie folgt aus:


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
                        (

                        )

                )

        )

)

Anmerkungen

  • Die Dokumentation PHP zur Funktion [imap_fetchstructure] erläutert die Bedeutung der verschiedenen Felder des von der Funktion zurückgegebenen Objekts:

Image

Die numerischen Werte des Feldes [type] haben folgende Bedeutung:

Image

Die numerischen Werte des Feldes [encoding] haben folgende Bedeutung:

Image

Die von [imap-01.php] gespeicherte Nachricht begann mit folgendem Text:


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_=_
  • Die Zeilen 15) und 33) begrenzen die Nachricht vom Typ [multipart/mixed] (Zeile m);
  • Die Zeilen 18) und 16) begrenzen den ersten Teil der Nachricht: die Klartextnachricht;
  • Die Zeilen 26) und 32) begrenzen den zweiten Teil der Nachricht: die Nachricht HTML;

Die verschiedenen Informationen der oben genannten Nachricht finden wir in dem von [imap_fetchstructure] zurückgegebenen Objekt wieder:


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
                                                )

                                        )

                                )

                        )

                )

  • Zeile 3: Die Nachricht hat den Typ MIME (Multipurpose Internet Mail Extensions) [multipart];
  • Zeile 4: Die Nachricht ist 7-Bit-kodiert;
  • Zeile 5: [ifsubtype]=1 gibt an, dass in der Struktur ein Feld [subtype] vorhanden ist;
  • Zeile 6: Das Feld [subtype] bezeichnet einen Subtyp MIME, in diesem Fall den Typ [mixed]. Insgesamt lautet der Typ MIME des Dokuments [multipart/mixed];
  • Zeile 7: [ifdescription]=0 gibt an, dass in der Struktur kein Feld [description] vorhanden ist;
  • Zeile 8: [ifid]=0 bedeutet, dass in der Struktur kein Feld [id] vorhanden ist;
  • Zeile 10: [ifdisposition]=0 bedeutet, dass das Feld [disposition] in der Struktur nicht vorhanden ist;
  • Zeile 11: [ifdparameters]=0 bedeutet, dass in der Struktur kein Feld [dparameters] vorhanden ist;
  • Zeile 12: [ifparameters]=1 bedeutet, dass in der Struktur ein Feld [parameters] vorhanden ist;
  • Zeile 13: Das Feld [parameters] beschreibt die Parameter der Nachricht. Hier gibt es nur einen;
  • Zeilen 15–19: Dieses Objekt beschreibt die folgende Zeile der Textnachricht:
boundary="_=_swift_1558872295_5bc8ee2ca8b3723c0b39ca8bbfbebdeb_=_"

Diese Zeilen dienen zur Abgrenzung der Nachricht. In der von [imap-01.php] abgerufenen Nachricht entspricht der soeben beschriebene Teil der Nachricht der Zeile m). Das Attribut [boundary] ist nicht identisch, da die Screenshots zwar derselben Nachricht entsprechen, diese jedoch zu unterschiedlichen Zeitpunkten versendet wurde;

  • Zeile 23: Hier beginnt die Struktur der verschiedenen Teile der Nachricht;
  • Zeilen 25–45: Dieser erste Teil hat den Typ [multipart/alternative]. Er entspricht der Zeile p) des Nachrichtentextes;
  • Zeile 47: Dieser erste Teil hat seinerseits Unterabschnitte;
  • Zeilen 47–70: Dieser erste Unterteil hat den Typ [text/plain] (Zeilen 51, 54), ist im Typ [ENCQUOTEDPRINTABLE] (Zeile 52) und verfügt über einen Parameter [charset=utf-8] (Zeilen 66–67);
  • die Zeilen 49–72 beschreiben die Zeilen s–x der Textnachricht;
  • Zeilen 74–99: beschreiben den zweiten Unterteil des Teils [multipart/alternative];
  • Zeilen 74–99: Dieser zweite Unterteil ist vom Typ [text/HTML] (Zeilen 76, 79), ist im Typ [ENCQUOTEDPRINTABLE] (Zeile 77) und verfügt über einen Parameter vom Typ [charset=utf-8] (Zeilen 89–93);
  • die Zeilen 74–99 beschreiben die Zeilen aa–ad der Textnachricht;

Der Abschnitt [multipart/alternative] ist nun beendet. Es beginnt der Abschnitt [application/vnd.openxmlformats-officedocument.wordprocessingml.document], der durch den folgenden Text beschrieben wird:

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"

Auch hier finden sich diese Informationen in dem von der Funktion [imap_fetchstructure] zurückgegebenen Objekt:


[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
                                )

                        )

                )

            
  • Zeile 1: Dies ist der zweite Teil der Gesamtnachricht. Zur Erinnerung: Der erste Teil hatte den Typ [multipart/alternative];
  • Zeilen 3–6: Dieser zweite Teil hat den Typ [application/vnd.openxmlformats-officedocument.wordprocessingml.document] (Zeilen 3 und 6) und ist in Base64 kodiert (Zeile 4);
  • Zeile 11: Dieser zweite Teil ist ein Anhang (Zeile 11) und hat zwei Parameter: [filename=Hello from SwiftMailer.docx] (Zeilen 15–21) und [name=Hello from SwiftMailer.docx] (Zeilen 26–32). Es ist zu beachten, dass dieser letzte Parameter in der Textnachricht nicht vorkommt. Er wurde daher in der Funktion [imap_fetchstructure] hinzugefügt;

Die Zeilen 1–36 werden für jeden der fünf Anhänge der Nachricht wiederholt.

Mit der Funktion [imap_fetch_structure] können wir also die Struktur einer Nachricht abrufen. Diese definiert Abschnitte, die wiederum Unterabschnitte enthalten können. Um den Text eines Abschnitts oder Unterabschnitts zu erhalten, verwenden wir die Funktion [imap_fetchbody].

Wir ändern die Funktion [getMailBody], mit der wir den Hauptteil einer Nachricht abrufen können, wie folgt:


function getMailBody($imapResource, int $msgNumber, array $infos, object $infosMail): void {
  // Die Struktur der Nachricht wird abgerufen
  $structure = imap_fetchstructure($imapResource, $msgNumber);
  if ($structure !== FALSE) {
    // diese verschiedenen Teile werden abgerufen
    getParts($imapResource, $msgNumber, $infos, $infosMail, $structure);
  }
}

function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
  // Berechnung der Abschnittsnummer
  if (substr($sectionNumber, 0, 2) === "0.") {
    $sectionNumber = substr($sectionNumber, 2);
  }
  print "-----contenu de la partie n° [$sectionNumber]\n";
  // Inhaltstyp
  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;
  }
  // Kodierungsart
  $encodings=["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
  print "Transfer-Encoding : ".$encodings[$part->encoding]."\n";
   
  // Weiter zu eventuellen Unterabschnitten
  if (isset($part->parts)) {
    for ($i = 1; $i <= count($part->parts); $i++) {
      // ein neuer Teil der Nachricht
      $subpart = $part->parts[$i - 1];
      // Rekursiver Aufruf – der Hauptteil des Abschnitts wird angefordert: [$subpart]
      getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
    }
  }
}

Kommentare

  • Zeile 3: Wir rufen die Struktur der Nachricht ab;
  • Zeile 6: Wir fordern die verschiedenen Teile der Nachricht an, die sich im Array [parts] der Struktur befinden;
  • Zeile 10: Die Funktion [getParts] erhält folgende Parameter:
    • [$imapResource]: die Verbindung zum Server IMAP;
    • [$msgNumber]: die Sequenznummer der Nachricht, deren Teile benötigt werden;
    • [$infos]: Informationen darüber, wo die gefundenen Teile im lokalen Dateisystem gespeichert werden sollen;
    • [$infosMail]: Allgemeine Informationen zur E-Mail (Absender, Empfänger, Betreff usw.);
    • [$part]: ein Objekt, das einen Teil der Nachricht darstellt;
    • [$sectionNumber]: eine Abschnitts- (oder Teil-)Nummer der Nachricht;
  • Zeilen 17–34: Es wird der Inhaltstyp des Teils Nr. [$section] der Nachricht angezeigt. Dazu werden die Felder [$part→type] und [$part→subtype] des Teils [$part] herangezogen;
  • Zeilen 36–37: Es wird die Kodierungsart des Teils [$sectionNumber] angezeigt;
  • Zeilen 40–47: Möglicherweise enthält der Teil, dessen Informationen gerade angezeigt wurden, selbst Unterteile;
  • Zeilen 41–46: Ist dies der Fall, wird der Inhaltstyp der verschiedenen Unterteile des soeben angezeigten Teils abgefragt. Hier erfolgt ein rekursiver Aufruf der Funktion [getParts];

Erneut senden wir eine E-Mail an den Gmail-Nutzer [php7parlexemple@gmail.com] mit dem Skript [smtp-02.php] und lesen sie mit dem vorherigen Skript [imap-02.php]. Dies führt zu folgenden Konsolenergebnissen:


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

Es gelingt uns tatsächlich, die verschiedenen Inhaltstypen der Nachricht sowie deren Kodierungsart zu ermitteln. Die Nummerierung der Teile folgt der folgenden Regel:

  • Zeilen 6–7: Der Teil [multipart/mixed], der die gesamte Nachricht darstellt, trägt die Nummer 0. Die verschiedenen Teile dieses Objekts tragen dann die Nummern 1, 2 …

Die Nachricht besteht insgesamt aus fünf Teilen:

  • Zeilen 9–10: Der Teil „[multipart/alternative]“ trägt die Nummer 1;
  • Zeilen 17–18: Der Teil „[APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT]“ mit der Nummer 2. Dabei handelt es sich um den Anhang einer Word-Datei;
  • Zeilen 20–21: der Teil „[APPLICATION/PDF]“ mit der Nummer 3. Dabei handelt es sich um den Anhang einer Datei „PDF“;
  • Zeilen 23–24: der Teil [APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT] mit der Nummer 4. Dies ist der Anhang einer Datei mit dem Namen OpenOffice;
  • Zeilen 26–27: der Abschnitt [UNKNOWN/PNG] mit der Nummer 5. Dabei handelt es sich um den Anhang einer Bilddatei;
  • Zeilen 30–31: der Abschnitt [MESSAGE/RFC822] mit der Nummer 6. Dies ist der Anhang einer E-Mail;

Wenn ein Teil Unterteile hat, werden diese wie folgt nummeriert: x.1, x.2…, wobei x die Nummer des übergeordneten Teils ist. Also:

  • Zeilen 11–12: Der erste Teil des Teils [multipart/alternative] trägt die Nummer 1.1. Es handelt sich um einen Inhalt vom Typ [text/plain]: den Text der E-Mail;
  • Zeilen 14–15: Der zweite Teil des Teils [multipart/alternative] trägt die Nummer 1.2. Es handelt sich um einen Inhalt vom Typ [text/HTML]: den E-Mail-Text im Format HTML;
  • Zeilen 32–33: Der erste Teil des Anhangs [MESSAGE/RFC822] trägt die Nummer 6.1. Es handelt sich um einen Inhalt vom Typ [text/plain]. Tatsächlich weicht gemäß dem Standard MIME die Nummerierung der Teile eines E-Mail-Anhangs [MESSAGE/RFC822] von der zuvor beschriebenen Regel ab. So trägt der erste Teil des Anhangs [MESSAGE/RFC822] nicht die Nummer 6.1, sondern eine andere Nummer;

Nachdem wir nun wissen, wie wir die verschiedenen Teile und Unterteile einer E-Mail identifizieren können, müssen wir noch deren Inhalt abrufen.

Der Skriptcode entwickelt sich wie folgt weiter:


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

  // Handelt es sich um eine Nachricht?
  if ($part->type === TYPEMESSAGE) {
    // Die Unterabschnitte dieser Nachricht (E-Mail-Anhang) werden nicht verarbeitet
    // Der Text der angehängten E-Mail wird angezeigt
    print imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
  } else {
    // Wir fahren mit den eventuellen Unterabschnitten fort
    if (isset($part->parts)) {
      for ($i = 1; $i <= count($part->parts); $i++) {
        // ein neuer Teil der Nachricht
        $subpart = $part->parts[$i - 1];
        // Rekursiver Aufruf – der Text des Abschnitts [$subpart] wird angefordert
        getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
      }
    } else {
      // Es gibt keine Unterabschnitte – daher wird der Hauptteil der Nachricht angezeigt
      print imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
    }
  }
}

Kommentare

  • Zeile 46: Die Funktion [imap_fetchbody] ruft den Hauptteil des Abschnitts Nr. [$sectionNumber] der Nachricht ab. Die Nummerierung der Abschnitte einer Nachricht folgt der zuvor erläuterten Regel;
  • Zeile 1: Man beginnt mit dem Abschnitt „0“;
  • Zeile 41: Die Unterabschnitte dieses Abschnitts werden dann mit „0.1“, „0.2“ nummeriert, obwohl sie eigentlich mit „1“, „2“ nummeriert werden müssten…
  • Zeilen 3–5: Diese Anomalie wird korrigiert;
  • Zeilen 37–43: Wenn der aktuelle Abschnitt Unterabschnitte hat, wird jeweils eine Schleife über jeden einzelnen davon durchlaufen (Zeilen 38–43). Ihre Abschnittsnummer lautet [$sectionNumber.$i];
  • Zeilen 44–47: Wenn keine Unterabschnitte mehr vorhanden sind, wird der Hauptteil des aktuellen Abschnitts mit der Funktion [imap_fetchbody] angezeigt. In unserem Beispiel handelt es sich um die Abschnitte [text/plain], [text/HTML] und die Anhänge;

Die Ausführung dieses Skripts liefert folgende Ergebnisse:


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

Anmerkungen

  • Zeilen 14–16: Der Inhalt der Textnachricht, kodiert in [quoted-printable] (Zeile 13);
  • Zeile 20: Der Inhalt der Nachricht HTML, verschlüsselt in [quoted-printable] (Zeile 19);
  • Zeilen 24–28: der Inhalt der Word-Datei, verschlüsselt als [base64] (Zeile 23);
  • Zeilen 32–37: Der Inhalt der Datei PDF, verschlüsselt als [base64] (Zeile 31);
  • Zeilen 41–45: der Inhalt der Datei „OpenOffice“, verschlüsselt als „[base64]“ (Zeile 40);
  • Zeilen 50–55: der Inhalt der Bilddatei, kodiert als [base64] (Zeile 49);
  • Zeilen 59–63: der Inhalt der E-Mail-Anhang, kodiert als [base64] (Zeile 58);

Da wir nun:

  • wir wissen, wie man die Texte der verschiedenen Teile einer E-Mail wiederherstellt;
  • die Kodierung dieser Texte kennen;

können wir diese Texte in Dateien speichern.

Der Code entwickelt sich wie folgt weiter:


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

  // Handelt es sich um eine Nachricht?
  if ($part->type === TYPEMESSAGE) {
    // Die Unterabschnitte dieser Nachricht werden nicht verarbeitet
    savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
  } else {
    // Wir fahren mit den eventuellen Unterabschnitten fort
    if (isset($part->parts)) {
      for ($i = 1; $i <= count($part->parts); $i++) {
        // ein neuer Teil der Nachricht
        $subpart = $part->parts[$i - 1];
        // Rekursiver Aufruf – der Hauptteil des Abschnitts [$subpart] wird angefordert
        getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
      }
    } else {
      // Es gibt keine Unterabschnitte – daher wird der Nachrichtentext gespeichert
      savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
    }
  }
}
  • Zeilen 33 und 45: Die Anzeige des Textes eines Teils [$imapResource, $msgNumber, $sectionNumber] der E-Mail wird nun durch dessen Speicherung in einer Datei ersetzt;

Die Funktion [savePart] lautet wie folgt:


// Speichern eines Nachrichtenteils
function savePart($imapResource, int $msgNumber, string $sectionNumber, array $infos, object $infosMail): void {
  // Speicherordner
  $outputDir = $infos["output-dir"] . "/message-$msgNumber";
  // Wenn der Ordner nicht existiert, wird er angelegt
  if (!file_exists($outputDir)) {
    mkdir($outputDir);
  }
  // Struktur des zu sichernden Teils
  $struct = imap_bodystruct($imapResource, $msgNumber, $sectionNumber);
  // Dokumenttyp
  $type = $struct->type;
  // Dokumentuntertyp
  $subtype = "";
  if (isset($struct->subtype)) {
    $subtype = strtolower($struct->subtype);
  }
  // Der Typ des Teils wird analysiert
  switch ($type) {
    case TYPETEXT:
      // Fall einer Textnachricht: 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:
      // andere Fälle – es werden nur Anhänge berücksichtigt
      if (isset($struct->disposition)) {
        $disposition = strtolower($struct->disposition);
        if ($disposition === "attachment") {
          // Es handelt sich um einen Anhang – dieser wird gespeichert
          saveAttachment($imapResource, $msgNumber, $sectionNumber, $outputDir, $struct);
        }
      } else {
        // Dieser Teil wird nicht verarbeitet
        print "Partie [$sectionNumber] ignorée\n";
      }
      break;
  }
}
  • Zeilen 3–8: Erstellung des Speicherordners. Dieser trägt die Nummer der Nachricht, deren Teile analysiert werden;
  • Zeile 10: Der zu speichernde Nachrichtenteil wird eindeutig durch die drei Parameter [$imapResource, $msgNumber, $sectionNumber] definiert. Die Struktur dieses Teils wird mit der Funktion [imap_bodystruct] abgefragt;
  • Zeile 12: Der Haupttyp des Nachrichtenteils wird abgerufen;
  • Zeilen 13–17: Der Untertyp wird abgerufen;
  • Zeilen 20–30: Die beiden Inhaltstypen werden verarbeitet: [text/plain] (Zeilen 23–25) und [text/HTML] (Zeilen 26–28). Die übrigen Typen [text/xx] werden ignoriert;
  • Zeile 24: Der Text des Teils [text/plain] wird in einer Datei mit dem Namen [message.txt] gespeichert;
  • Zeile 27: Der Text des Teils [text/HTML] wird in einer Datei mit dem Namen [message.HTML] gespeichert;
  • Zeilen 31–43: Es werden die Teile behandelt, deren Haupttyp nicht [text] ist;
  • Zeile 35: Es werden nur die Anhänge der Nachricht berücksichtigt;
  • Zeile 37: Diese werden mithilfe der Funktion [saveAttachment] in einer Datei gespeichert;

Zusammenfassung des vorstehenden Codes:

  • Die Teile [text/plain] und [text/HTML] werden mithilfe der Funktion [saveText] gespeichert. Diese Teile stellen den Inhalt der E-Mail dar;
  • speichert die verschiedenen Anhänge mithilfe der Funktion [saveAttachment];

Die Funktion [saveText] hat folgende Aufgabe:


// Speichern des Textes [$text] der Nachricht
function saveText(string $fileName, int $type, string $text, object $infosMail, object $struct) {
  // Vorbereitung des zu speichernden Textes
  // $text ist verschlüsselt – wird entschlüsselt
  switch ($struct->encoding) {
    case ENCBASE64:
      $text = base64_decode($text);
      break;
    case ENCQUOTEDPRINTABLE:
      $text = quoted_printable_decode($text);
      break;
  }
  // Kopfzeilen der Nachricht
  // Von
  $from = "From: ";
  foreach ($infosMail->from as $expéditeur) {
    $from .= $expéditeur->mailbox . "@" . $expéditeur->host . ";";
  }
  // An
  $to = "To: ";
  foreach ($infosMail->to as $destinataire) {
    $to .= $destinataire->mailbox . "@" . $destinataire->host . ";";
  }
  // Betreff
  $subject = "Subject: " . $infosMail->subject;
  // Erstellung des zu speichernden Textes
  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;
  }
  // Erstellung der Datei
  print "sauvegarde d'un message dans [$fileName]\n";
  // Erstellung der Datei
  if (! file_put_contents($fileName, $contents)) {
    // Fehler beim Erstellen der Datei
    print "Impossible de créer le fichier [$fileName]\n";
  }
}

Kommentare

  • Zeile 1:
    • [$fileName] ist der Name der Datei, in der der Text [$text] gespeichert wird;
    • [$type]: hat den Wert 0 für eine Textdatei, 1 für eine HTML-Datei;
    • [$text]: ist der zu speichernde Text. Dieser muss jedoch zunächst dekodiert werden, da er kodiert ist;
    • [$infosMail]: enthält allgemeine Informationen zur E-Mail. Wir werden die Felder [from, to, subject] verwenden;
    • [$struct]: ist die Struktur, die den Teil der E-Mail beschreibt, den wir gerade speichern. Dadurch können wir die Art der Kodierung des zu speichernden Textes ermitteln;
  • Zeilen 4–12: Der zu speichernde Text wird dekodiert;
  • Zeilen 13–25: Wir extrahieren die Informationen [from, to, subject] aus der E-Mail;
  • Zeilen 27–36: Je nach Typ (0 oder 1) des zu speichernden Textes wird entweder ein Klartext (Zeile 30) oder ein HTML-Text (Zeile 34) erstellt;
  • Zeile 40: Der gesamte Text wird in der Datei [$fileName] gespeichert;

Die Anhänge werden ihrerseits mit der folgenden Funktion „[saveAttachment]“ gespeichert:


// Speichern eines Anhangs
function saveAttachment($imapResource, int $msgNumber, string $sectionNumber, string $outputDir, object $struct) {
  // Die Struktur des Anhangs wird analysiert
  // Es wird versucht, den Namen der Datei zu ermitteln, in der der Anhang gespeichert werden soll
  // Dieser Name befindet sich in den [dparameters] der Struktur
  if (isset($struct->dparameters)) {
    // die [dparameters] werden abgerufen
    $dparameters = $struct->dparameters;
    $fileName = "";
    // man durchläuft das Array der [dparameters]
    foreach ($dparameters as $dparameter) {
      // Jedes [dparameter] ist ein Objekt mit zwei Attributen: [attribute, value]
      $attribute = strtolower($dparameter->attribute);
      // Das Attribut [filename] entspricht dem Namen der zu erstellenden Datei
      // in diesem Fall steht der Dateiname in [$dparameter->value]
      if ($attribute === "filename") {
        $fileName = $dparameter->value;
        break;
      }
    }
    // Wenn kein Dateiname gefunden wurde, wird im Attribut [parameters] der Struktur nachgeschaut
    if ($fileName === "" && isset($struct->parameters)) {
      // und man erhält die Werte [parameters]
      $parameters = $struct->parameters;
      foreach ($parameters as $parameter) {
        // Jeder Parameter ist ein Wörterbuch mit zwei Schlüsseln: [attribute, value]
        $attribute = strtolower($parameter->attribute);
        // Wenn das Attribut [name] lautet, dann ist [value] der Dateiname
        if ($attribute === "name") {
          $fileName = $parameter->value;
          // Der Dateiname kann kodiert sein
          // zum Beispiel =?utf-8?Q?Kurse-Tutorials-Serge-Tah=C3=A9-1568x268=2Ep
          // Die Kodierung wird mit einem regulären Ausdruck ermittelt
          $champs = [];
          $match = preg_match("/=\?(.+?)\?/", $fileName, $champs);
          // Bei Übereinstimmung wird der Dateiname dekodiert
          if ($match) {
            $fileName = iconv_mime_decode($fileName, 0, $champs[1]);
          }
          break;
        }
      }
    }
  }
  // Wenn ein Dateiname gefunden wurde, wird der Anhang gespeichert
  if ($fileName !== "") {
    // Speichern des Anhangs
    $fileName = "$outputDir/$fileName";
    print "sauvegarde de l'attachement dans [$fileName]\n";
    // Datei erstellen
    if ($file = fopen($fileName, "w")) {
      // Der verschlüsselte Text des Anhangs wird abgerufen
      $text = imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
      // Der Anhang ist verschlüsselt – er wird entschlüsselt
      switch ($struct->encoding) {
        // Base64
        case ENCBASE64:
          $text = base64_decode($text);
          break;
        // Quoted-Printable
        case ENCQUOTEDPRINTABLE:
          $text = quoted_printable_decode($text);
          break;
        default:
          // andere Fälle werden ignoriert
          break;
      }
      // Text in die Datei schreiben
      fputs($file, $text);
      // Datei schließen
      fclose($file);
    } else {
      // Fehler beim Erstellen der Datei
      print "L'attachement n'a pu être sauvegardé dans [$fileName]\n";
    }
  }
}

Anmerkungen

  • Zeile 2: Die Funktion [saveAttachment] akzeptiert die folgenden Parameter:
    • [$imapResource, int $msgNumber, string $sectionNumber] definiert eindeutig den zu speichernden Teil IMAP;
    • [string $outputDir] ist der Speicherordner;
    • [object $struct] beschreibt die Struktur des zu speichernden Nachrichtenteils;
  • Zeilen 6–44: Es wird nach dem Dateinamen gesucht, der dem Anhang zugeordnet ist. Dieser Dateiname wird zum Speichern verwendet. Der Dateiname des Anhangs befindet sich in der Tabelle [$struct→dparameters] oder in der Tabelle [$struct→parameters], möglicherweise sogar in beiden;
  • Zeilen 30–40: Wenn der Dateiname Zeichen enthält, die nicht auf 7 Bit kodiert sind, wurde er in [quoted-printable] kodiert. In diesem Fall heißt das Attribut in [$struct→dparameters] „[fileName*]“ statt „[fileName]“. Das bedeutet, dass es die Bedingung in Zeile 16 nicht erfüllt hat. Der Dateiname wird dann in der Tabelle [$struct→parameters] gesucht;
  • Zeile 32: ein Beispiel für einen verschlüsselten Dateinamen. Er hat folgende Form: =?codage_original?codage_actuel?nom_encodé. Der Name [=?utf-8?Q?Cours-Tutoriels-Serge-Tah=C3=A9-1568x268=2Ep] bedeutet also, dass der Dateiname zuvor UTF-8 lautete und sich derzeit in [quoted-printable] (Q) befindet;
  • Zeile 38: Der Dateiname wird mit der Funktion [iconv_mime_decode] dekodiert, die hier drei Parameter akzeptiert:
    • die zu dekodierende Zeichenkette;
    • Standardmäßig auf 0 setzen;
    • der Zeichensatz, der zur Darstellung der dekodierten Zeichenkette verwendet werden soll. Dieser Parameter ist in der zu dekodierenden Zeichenkette enthalten. Er wird in den Zeilen 34–35 mithilfe eines regulären Ausdrucks ermittelt;
  • Zeilen 45–75: Der Anhang wird in einer Datei mit dem ermittelten Namen gespeichert;

Um das Skript [imap-02.php] zu testen, senden wir zunächst eine E-Mail an [guest@localhost] mit folgender Konfiguration:

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

Es gibt also fünf Anhänge.

Man liest die gesendete E-Mail mit [imap-02.php] und der folgenden Konfiguration:

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

Die Konsolenausgaben lauten wie folgt:


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

Die gespeicherten Dateien befinden sich im Ordner [output/localhost-pop3/message-N]:

Image

16.6.6. Client POP3 / IMAP mit der Bibliothek [php-mime-mail-parser]

Im vorherigen Skript [imap-02.php] konnten wir folgende Inhalte speichern:

  • die Inhalte [text/plain] und [text/HTML] der E-Mail;
  • die Anhänge der E-Mail;

Bei einem Anhang vom Typ [message/rfc822] haben wir ebenfalls den Inhalt des Anhangs gespeichert. Dieser Anhangstyp ist jedoch selbst eine E-Mail, die wiederum die Inhalte [text/plain] und [text/HTML] sowie weitere Anhänge enthält. Es kann also folgende Situation auftreten:

  • ein [mail 1], dessen Struktur der eines Anhangs vom Typ [message/rfc822] ähnelt;
  • eine Datei „[mail 2]“, die an E-Mail 1 angehängt ist;
  • ein [mail 3], der an E-Mail 2 angehängt ist;
  • usw.

Das Skript [imap-02.php] speichert den Inhalt von [mail 1] (Texte und Anhänge). Es speichert [mail 2] als angehängtes Dokument, hört aber dort auf. Es wird nicht versucht, [mail 2] zu analysieren, um die Texte und Anhänge zu extrahieren. Man könnte meinen, es reiche aus, bei [mail 2] dasselbe anzuwenden wie bei [mail 1]. Ein rekursiver Aufruf der Methode, die [mail 1] verarbeitet hat, könnte dann ausreichen, um den Inhalt aller ineinander verschachtelten E-Mails zu erhalten. Leider sind die Teile von [mail 2] nach einer anderen Logik nummeriert als bei [mail 1], was die Verwendung desselben Algorithmus in beiden Fällen verhindert, es sei denn, man wendet eine recht komplexe Logik an, um die Nummern der Teile einer E-Mail zu berechnen, unabhängig von deren Position innerhalb der Gesamtheit der verschachtelten E-Mails.

Das Skript [imap-02.php] war bereits komplex. Um zu vermeiden, dass es durch die Verarbeitung der Inhalte verschachtelter E-Mails noch komplexer wird, werden wir die Bibliothek [php-mime-mail-parser] verwenden, die auf GitHub (Mai 2019) unter URL und [https://github.com/php-mime-mail-parser/php-mime-mail-parser] verfügbar ist und von Vincent Dauce geschrieben wurde.

16.6.6.1. Installation der Bibliothek [php-mime-mail-parser]

Auf der Übersichtsseite der Bibliothek wird beschrieben, wie sie unter Windows installiert wird:

Image

Die Installation unter Windows erfolgt in zwei Schritten:


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

LA DLL der Bibliothek [mailparse] ist seit URL [http://pecl.php.net/package/mailparse] (Mai 2019) verfügbar;

Image

  • in [2]: Wählen Sie die aktuellste und stabilste Version der Bibliothek aus;

Image

  • in [3] wählen Sie die Version von PHP, die Sie verwenden (in diesem Dokument ist es PHP 7.2);
  • bei [4] wählen Sie die Version Ihres OS für Windows aus (hier handelt es sich um ein 64-Bit-Windows). Wir nehmen die Version [Thread Safe];

Um die Version von PHP zu ermitteln, die mit Laragon heruntergeladen wurde, öffnen Sie [Terminal] über das Laragon-Fenster und geben Sie den folgenden Befehl ein:


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                      

Die Version von PHP 7.2.11 wird in Zeile 3 angezeigt. In derselben Zeile wird auch die für die Kompilierung verwendete Windows-Version (32 oder 74 Bit) angegeben.

Sobald Sie die Datei DLL erhalten haben, müssen Sie sie in den Ordner [<laragon>/bin/php/<version-php>/ext] [5] kopieren:

Image

Anschließend muss diese Erweiterung in der Datei [php.ini] aktiviert werden, die PHP konfiguriert (siehe Abschnitt „Link“):

Image

Es ist wahrscheinlich, dass die Zeile [7] nicht vorhanden ist und Sie diese selbst hinzufügen müssen.

Sobald die Erweiterung aktiviert ist, kann man ihre Gültigkeit überprüfen, indem man den folgenden Befehl in ein Laragon-Terminal eingibt:


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)                                                              

Der Befehl [php –-ini] lädt die Konfigurationsdatei aus Zeile 4. Anschließend lädt er die DLL-Dateien aller in [php.ini] aktivierten Erweiterungen. Sollte eine davon fehlerhaft sein, wird dies gemeldet. So wird die Gültigkeit der zu [php_mailparse.dll] hinzugefügten DLL überprüft. Sie kann aus verschiedenen Gründen als fehlerhaft eingestuft werden, wobei die häufigsten folgende sind:

  • Sie haben eine DLL heruntergeladen, die nicht mit der verwendeten Version PHP übereinstimmt;
  • Sie haben eine 32-Bit-Version von DLL heruntergeladen, obwohl Sie eine 64-Bit-Version von PHP haben, oder umgekehrt;

Sobald die Erweiterung aktiviert und überprüft wurde, können Sie mit der Installation der Bibliothek [php-mime-mail-parser] fortfahren:

Image

Der Befehl [8] muss in einem Laragon-Terminal eingegeben werden (siehe Abschnitt „Link“):

Image

  • Bei [1] stellen Sie sicher, dass Sie sich im Ordner [<laragon>/www] befinden;
  • bei [2] den Befehl zur Installation der Bibliothek [php-mime-mail-parser];
  • in [3]: Hier wurde nichts installiert, da die Bibliothek [php-mime-mail-parser] bereits installiert war;

Die Installation der Bibliothek [php-mime-mail-parser] erfolgt im Ordner [<laragon>/www/vendor]:

Image

Image

  • in [2-3], die Quelldateien der Bibliothek [php-mime-mail-parser];

Nachdem die Arbeitsumgebung nun eingerichtet ist, können wir mit dem Schreiben des Skripts [imap-03.php] fortfahren.

16.6.6.2. Das Skript [imap-03.php]

Das Skript [imap-03.php] verwendet dieselbe Konfigurationsdatei [config-imap-01.json] wie die vorherigen Skripte:

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

Das Skript [imap-03.php] lautet wie folgt:


<?php

// IMAP-Client (Internet Message Access Protocol) zum Lesen von E-Mails
// geschrieben mit der Bibliothek [php-mime-mail-parser]
// verfügbar unterURL [https://github.com/php-mime-mail-parser/php-mime-mail-parser] (Mai 2019)
//
// Strikte Einhaltung der deklarierten Typen der Funktionsparameter
declare (strict_types=1);
// Fehlerbehandlung
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// Abhängigkeiten
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
// Einstellungen zum Abrufen von E-Mails
const CONFIG_FILE_NAME = "config-imap-01.json";

// Konfiguration abrufen
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);

// Lesen der Postfächer
foreach ($mailboxes as $name => $infos) {
  // Nachverfolgung
  print "------------Lecture de la boîte à lettres [$name]\n";
  // Abfrage des Postfachs
  readmailbox($name, $infos);
}
// Ende
exit;

Anmerkungen

  • Zeilen 18–23: Der Inhalt der Konfigurationsdatei wird in das Wörterbuch [$mailboxes] übernommen;
  • Zeilen 26–31: Jedes Postfach wird von der Funktion [readmailbox] (Zeile 30) gelesen. Diese Funktion liest tatsächlich die ungelesenen Nachrichten des Postfachs. Ein Postfach entspricht der E-Mail-Adresse eines bestimmten Benutzers;

Die Funktion [readmailbox] lautet wie folgt:


function readmailbox(string $name, array $infos): void {
  // Anmeldung
  $imapResource = imap_open($name, $infos["user"], $infos["password"]);
  if (!$imapResource) {
    // Fehlgeschlagen
    print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
    exit;
  }
  // Verbindung hergestellt
  print "Connexion établie avec le serveur [$name].\n";
  // Gesamtzahl der Nachrichten im Postfach
  $nbmsg = imap_num_msg($imapResource);
  print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
  // Ungelesene Nachrichten im aktuellen Postfach
  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 {
      // Die Liste der ungelesenen Nachrichten wird durchlaufen
      foreach ($msgNumbers as $msgNumber) {
        print "---message n° [$msgNumber]\n";
        // Der Text der Nachricht Nr. $msgNumber wird abgerufen
        getMailBody($imapResource, $msgNumber, $infos);
        // Wenn das Protokoll POP3 lautet, wird die Nachricht nach dem Abrufen gelöscht
        $pop3 = $infos["pop3"];
        if ($pop3 !== NULL) {
          // Die Nachricht wird als „zu löschen“ markiert
          imap_delete($imapResource, $msgNumber);
        }
      }
      // Ende des Lesens der ungelesenen Nachrichten
      if ($pop3 !== NULL) {
        // Die als „zu löschen“ markierten Nachrichten werden gelöscht
        imap_expunge($imapResource);
      }
    }
  }
  // Verbindung wird geschlossen
  $imapClose = imap_close($imapResource);
  if (!$imapClose) {
    // Fehler
    print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
  } else {
    // Erfolg
    print "Fermeture de la connexion réussie.\n";
  }
}

Kommentare

Der Code der Funktion [readmailbox] ist derselbe wie in den vorherigen Skripten.

Die Funktion [getMailBody] (Zeile 25), die den Textkörper einer Nachricht (Inhalt + Anhänge) analysiert, lautet wie folgt:


// Analyse des Nachrichtentextes
function getMailBody($imapResource, int $msgNumber, array $infos): void {
  // Der gesamte Text der Nachricht wird abgerufen
  $text = imap_fetchbody($imapResource, $msgNumber, "");
  if ($text === FALSE) {
    print "Le corps du message [$msgNumber] n'a pu être récupéré";
    return;
  }
  // Es wird ein Parser erstellt, der den Text der Nachricht analysiert
  $parser = (new PhpMimeMailParser\Parser())->setText($text);
  // Die verschiedenen Teile der Nachricht werden abgerufen
  $outputDir = $infos["output-dir"] . "/message-$msgNumber";
  getParts($parser, $msgNumber, $outputDir);
}

Kommentare

  • Zeile 2: Die Funktion [getMailBody] akzeptiert drei Parameter:
    • [$imapResource]: die Ressource IMAP, mit der eine Verbindung besteht;
    • [$msgNumber]: die Nummer der zu verarbeitenden Nachricht (im Postfach);
    • [$infos]: verschiedene Informationen zum verarbeiteten Postfach;
  • Zeile 4: Die gesamte Nachricht mit der Nummer [$msgNumber] wird abgerufen;
  • Zeilen 5–8: Fall, in dem der Inhalt der Nachricht nicht abgerufen werden konnte;
  • Zeile 10: Die Bibliothek [php-mime-mail-parser] wird nun verwendet. Das Objekt [$parser] wird mit der Analyse des Nachrichtentextes beauftragt;
  • Zeile 12: [$outputDir] ist der Ordner, in dem die Textinhalte und Anhänge der Nachricht Nr. [$msgNumber] gespeichert werden;
  • Zeile 13: Die Funktion [getParts] wird beauftragt, die verschiedenen Teile (Textinhalte und Anhänge) der Nachricht Nr. [$msgNumber] zu ermitteln und im Ordner [$outputDir] zu speichern;

Die Funktion [getParts] lautet wie folgt:


// Abruf der verschiedenen Teile einer Nachricht
function getParts(PhpMimeMailParser\Parser $parser, int $msgNumber, string $outputDir): void {
  // Bei Bedarf wird der Ordner zur Speicherung der Nachricht erstellt
  if (!file_exists($outputDir)) {
    if (!mkdir($outputDir)) {
      print "Le dossier [$outputDir] n'a pu être créé\n";
      return;
    }
  }
  // Die Kopfzeilen der Nachricht werden abgerufen
  $arrayHeaders = $parser->getHeaders();
  // Die Textnachrichten werden gespeichert
  $parts = $parser->getInlineParts("text");
  for ($i = 1; $i <= count($parts); $i++) {
    print "-- Sauvegarde d'un message de type [text/plain]\n";
    saveMessage($parts[$i - 1], 0, $arrayHeaders, "$outputDir/message_$i.txt");
  }
  // HTML-Nachrichten werden gespeichert
  $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");
  }
  // Die Anhänge der Nachricht werden abgerufen
  $attachments = $parser->getAttachments();
  // Nummer des Anhangs
  $iAttachment = 0;
  // die Liste der Anhänge durchlaufen
  foreach ($attachments as $attachment) {
    // Typ des Anhangs
    $fileType = $attachment->getContentType();
    print "-- Sauvegarde d'un attachement de type [$fileType] dans le fichier [$outputDir/{$attachment->getFilename()}]\n";
    // Der Anhang wird gespeichert
    try {
      $attachment->save($outputDir, PhpMimeMailParser\Parser::ATTACHMENT_DUPLICATE_SUFFIX);
    } catch (Exception $e) {
      print "L'attachement n'a pu être sauvegardé : " . $e->getMessage() . "\n";
    }
    // Sonderfall des Typs „message/rfc822“
    if ($fileType === "message/rfc822") {
      // Der Anhang ist selbst eine Nachricht – diese wird ebenfalls geparst
      // Das Speicherverzeichnis wird gewechselt
      $iAttachment++;
      $outputDir = $outputDir . "/rfc822-$iAttachment";
      // Der zu analysierende Inhalt wird geändert
      $parser->setText($attachment->getContent());
      // Die Nachricht wird rekursiv analysiert
      getParts($parser, $msgNumber, $outputDir);
    }
  }
}

Anmerkungen

  • Zeile 2: Die Funktion [getParts] akzeptiert drei Parameter:
    • einen Parser [$parser], an den der gesamte Text der zu analysierenden Nachricht übergeben wurde;
    • [$msgNumber] ist die Nummer der gerade analysierten Nachricht;
    • [$outputDir] ist der Ordner, in dem die Inhalte und Anhänge der Nachricht gespeichert werden sollen;
  • Zeilen 4–9: Erstellung des Ordners [$outputDir];
  • Zeile 11: Die Kopfzeilen der gerade analysierten Nachricht (From, To, Subject…) werden abgerufen;
  • Zeile 13: Die Teile der E-Mail mit dem Typ „[text/plain]“ werden abgerufen. Es wird ein Array abgerufen;
  • Zeilen 14–17: Alle Elemente des abgerufenen Arrays werden gespeichert, wobei jedem ein anderer Dateiname zugewiesen wird;
  • Zeile 19: Die Teile der E-Mail mit dem Typ „[text/html]“ werden abgerufen. Es wird ein Array zurückgegeben;
  • Zeilen 20–23: Alle Elemente des abgerufenen Arrays werden gespeichert, wobei jedem ein anderer Dateiname zugewiesen wird;
  • Zeile 25: Die Liste der Anhänge der analysierten Nachricht wird abgerufen;
  • Zeile 29: Diese Liste wird durchlaufen;
  • Zeile 24: Der Typ des Anhangs wird abgerufen (Attribut Content-Type);
  • Zeilen 34–38: Speichern des Anhangs im Ordner „[$outputDir]“. Der zweite Parameter „[PhpMimeMailParser\Parser::ATTACHMENT_DUPLICATE_SUFFIX]“ ist eine Namenskonvention für die Anhänge. Wenn [$attachment→getFilename()] den Wert X hat und die Datei X bereits existiert, versucht die Bibliothek [php-mime-mail-parser] die Namen [X_1], [X_2], usw., bis ein Dateiname gefunden wird, der noch nicht existiert;
  • Zeile 40: Es wird geprüft, ob es sich bei dem angehängten Dokument um eine E-Mail handelt;
  • Zeilen 41–48: Ist dies der Fall, wird diese E-Mail ihrerseits analysiert, um Inhalte und Anhänge zu extrahieren;
  • Zeile 44: Wenn [$outputDir] den Wert X hat und sich unter den Anhängen der analysierten Nachricht zwei E-Mails befinden, wird die erste im Ordner [$outputDir/rfc822-1] und die zweite im Ordner [$outputDir/rfc822-2] gespeichert;
  • Zeile 46: Der Inhalt der angehängten E-Mail wird zum neuen zu analysierenden Text;
  • Zeile 48: Die Funktion [getParts] wird rekursiv aufgerufen, um den neuen Text zu analysieren;

Die Funktion [saveMessage] speichert die Textinhalte der zu analysierenden Nachricht:


// Speichern einer Textnachricht
function saveMessage(string $text, int $type, array $arrayHeaders, string $filename): void {
  // zu speichernder Inhalt
  $contents = "";
  // Hinzufügen der Kopfzeilen
  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";
  }
  // Hinzufügen des Nachrichtentextes
  $contents .= $text;
  // Alles speichern
  if (!file_put_contents($filename, $contents)) {
    // Fehlgeschlagen
    print "Le message n'a pu être sauvegardé dans le fichier [$filename]\n";
  } else {
    // erfolgreich
    print "Le message a été sauvegardé dans le fichier [$filename]\n";
  }
}

Kommentare

  • Die Funktion [saveMessage] akzeptiert folgende Parameter:
    • [$text]: der zu speichernde Text;
    • [$type]: der Texttyp (0: text/plain, 1: text/HTML);
    • [$arrayHeaders]: die Kopfzeilen der analysierten Nachricht;
    • [$filename]: Name der Datei, in der [$text] gespeichert werden soll;
  • Zeile 4: [$contents] stellt den gesamten zu speichernden Text dar;
  • Zeilen 6–20: Zunächst werden alle Kopfzeilen der Nachricht (From, To, Subject…) gespeichert;
  • Zeilen 16–19: Im Falle eines Textes wie HTML wird jede Zeile mit dem Tag <br/> abgeschlossen, damit jede Kopfzeile in einem Browser in einer eigenen Zeile erscheint;
  • Zeile 22: Zu den Kopfzeilen wird der zu speichernde Nachrichtentext hinzugefügt;
  • Zeilen 24–30: Das Ganze wird in der Datei „[$filename]“ gespeichert;

Die Verwendung der Bibliothek [php-mime-mail-parser] erleichtert das Schreiben des Skripts zum Lesen von E-Mails erheblich.

Das Skript [smtp-02.php] wird verwendet, um eine E-Mail an den Benutzer [guest@localhost] mit folgender Konfiguration zu versenden:

{
    "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"
        ]
    }
}
  • Zeilen 11–15: Es gibt fünf Anhänge;
  • Zeile 15: [test-localhost-2.eml] ist eine E-Mail mit folgender Struktur:
    • [test-localhost-2.eml] enthält 4 Anhänge (die gleichen wie in den Zeilen 11–14) und eine angehängte E-Mail;
    • die an [test-localhost-2.eml] angehängte E-Mail enthält 4 Anhänge (die gleichen wie in den Zeilen 11–14);

Das Skript [imap-03.php] wird verwendet, um das Postfach des Benutzers [guest@localhost] mit folgender Konfiguration auszulesen:

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

Nach der Ausführung sieht die Ordnerstruktur von [output/localhost-pop3] wie folgt aus:

Image

  • in [1] die 5 Anhänge der von [guest@localhost] empfangenen E-Mail;
  • in [2] die 5 Anhänge der E-Mail [test-localhost-2.eml] von [1];
  • in [3] die 4 Anhänge der E-Mail [test-localhost.eml] von [2];

Die Konsolenausgaben lauten wie folgt:


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

Wenn man [message_1.HTML] aus [3] in einem Browser anzeigt, erhält man Folgendes:

Image