Skip to content

21. Internetfunktionen

Wir werden nun die Internetfunktionen von Python behandeln, die es uns ermöglichen, TCP/IP-Programmierung (Transmission Control Protocol/Internet Protocol) durchzuführen.

Image

21.1. Grundlagen der Internetprogrammierung

21.1.1. Allgemeiner Überblick

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

Image

Wenn eine Anwendung AppA auf Rechner A über das Internet mit einer Anwendung AppB auf Rechner B kommunizieren möchte, muss sie einige Dinge wissen:

  • die IP-Adresse (Internet Protocol) oder den Namen von Rechner B;
  • die von der Anwendung AppB verwendete Portnummer. Tatsächlich kann Rechner B zahlreiche Anwendungen hosten, die im Internet laufen. Wenn er Informationen aus dem Netzwerk empfängt, muss er wissen, für welche Anwendung die Informationen bestimmt sind. Die Anwendungen auf Rechner B greifen über Schnittstellen, auch als Kommunikationsports bezeichnet, auf das Netzwerk zu. Diese Informationen sind 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 Studie werden wir ausschließlich TCP-IP-Protokolle verwenden;
  • das von der Anwendung AppB unterstützte Kommunikationsprotokoll. Tatsächlich werden die Rechner A und B miteinander „kommunizieren“. Was sie austauschen, wird in die TCP/IP-Protokolle eingekapselt. Wenn jedoch am Ende der Kette die Anwendung AppB die von der Anwendung AppA gesendeten Informationen empfängt, muss sie in der Lage sein, diese zu interpretieren. Dies ist vergleichbar mit der Situation, in der zwei Personen, A und B, per Telefon kommunizieren: Ihr Gespräch wird über das Telefon übertragen. Die Sprache wird von Telefon A als Signale codiert, über Telefonleitungen übertragen und kommt bei Telefon B an, um dort decodiert zu werden. Person B hört dann die Worte. Hier kommt das Konzept eines Kommunikationsprotokolls ins Spiel: Wenn A Französisch spricht und B diese Sprache nicht versteht, können A und B nicht effektiv miteinander kommunizieren;

Daher müssen sich die beiden kommunizierenden Anwendungen auf die Art der Kommunikation einigen, die sie verwenden werden. Beispielsweise unterscheidet sich die Kommunikation mit einem FTP-Dienst von der mit einem POP-Dienst: Diese beiden Dienste akzeptieren nicht dieselben Befehle. Sie haben ein unterschiedliches Kommunikationsprotokoll;

21.1.2. Merkmale des TCP-Protokolls

Hier betrachten wir nur die Netzwerkkommunikation unter Verwendung des TCP-Transportprotokolls, dessen Hauptmerkmale wie folgt sind:

  • Der Prozess, der Daten übertragen möchte, baut zunächst eine Verbindung zu dem Prozess auf, der die zu übertragenden Informationen empfangen soll. Diese Verbindung wird zwischen einem Port auf dem sendenden Rechner und einem Port auf dem empfangenden Rechner hergestellt. So entsteht ein virtueller Pfad zwischen den beiden Ports, der ausschließlich für die beiden Prozesse reserviert 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 übertragenen Informationen erscheinen als zusammenhängender Datenstrom. Der sendende Prozess sendet Informationen in seinem eigenen Tempo. Diese Informationen werden nicht unbedingt sofort gesendet: Das TCP-Protokoll wartet, bis es genug Daten zum Senden hat. Sie werden in einer Struktur gespeichert, die als TCP-Segment bezeichnet wird. Sobald dieses Segment voll ist, wird es an die IP-Schicht übertragen, wo es in ein IP-Paket gekapselt wird;
  • Jedes vom TCP-Protokoll gesendete Segment ist nummeriert. Das empfangende TCP-Protokoll überprüft, ob es die Segmente in der richtigen Reihenfolge empfängt. Für jedes korrekt empfangene Segment sendet es eine Bestätigung an den Absender;
  • Wenn der Absender diese Bestätigung erhält, benachrichtigt er den sendenden Prozess. Der sendende Prozess kann somit bestätigen, dass ein Segment sicher angekommen ist;
  • Wenn das TCP-Protokoll, das ein Segment gesendet hat, nach einer bestimmten Zeit keine Bestätigung erhält, sendet es das betreffende Segment erneut und gewährleistet so die Qualität des Informationsübermittlungsdienstes;
  • Die zwischen den beiden kommunizierenden Prozessen hergestellte virtuelle Verbindung ist vollduplexfähig: Das bedeutet, dass Informationen in beide Richtungen fließen können. So kann der Zielprozess Bestätigungen senden, während der Quellprozess weiterhin Informationen sendet. Dies ermöglicht es beispielsweise dem sendenden TCP-Protokoll, mehrere Segmente zu senden, ohne auf eine Bestätigung zu warten. Stellt es nach einer bestimmten Zeit fest, dass es für ein bestimmtes Segment Nr. n keine Bestätigung erhalten hat, setzt es das Senden von Segmenten ab diesem Punkt fort;

21.1.3. Die Client-Server-Beziehung

Die Kommunikation über das Internet ist oft asymmetrisch: Rechner A initiiert eine Verbindung, um einen Dienst von Rechner B anzufordern, und gibt dabei an, dass er eine Verbindung mit dem Dienst SB1 auf Rechner B herstellen möchte. Rechner B akzeptiert dies entweder oder lehnt es ab. Wenn sie akzeptiert, kann Rechner A seine Anfragen an den Dienst SB1 senden. Diese Anfragen müssen dem vom Dienst SB1 verstandenen Kommunikationsprotokoll entsprechen. So entsteht ein Anfrage-Antwort-Dialog zwischen Rechner A, dem sogenannten Client-Rechner, und Rechner B, dem sogenannten Server-Rechner. Einer der beiden Partner wird die Verbindung schließen.

21.1.4. Client-Architektur

Die Architektur eines Netzwerkprogramms, das die Dienste einer Serveranwendung anfordert, 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

21.1.5. Serverarchitektur

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

1
2
3
4
5
ouvrir le service sur la machine locale
tant que le service est ouvert
    se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
    lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que

Das Serverprogramm behandelt die erste Verbindungsanfrage eines Clients anders als dessen nachfolgende Serviceanfragen. Das Programm erbringt den Dienst nicht selbst. Würde es dies tun, würde es während der Dienstausführung nicht mehr auf Verbindungsanfragen warten, und die Clients würden nicht bedient werden. Es geht anders vor: Sobald eine Verbindungsanfrage am Listening-Port empfangen und angenommen wird, erstellt der Server eine Aufgabe, die für die Bereitstellung des vom Client angeforderten Dienstes zuständig ist. Dieser Dienst wird an einem anderen Port des Serverrechners bereitgestellt, dem sogenannten Service-Port. Dadurch können mehrere Clients gleichzeitig bedient werden.

Eine Service-Aufgabe hat die folgende Struktur:

1
2
3
4
5
6
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

21.2. Erfahren Sie mehr über die Kommunikationsprotokolle des Internets

21.2.1. Einführung

Wenn ein Client eine Verbindung zu einem Server herstellt, wird ein Dialog zwischen beiden aufgebaut. Die Art dieses Dialogs bildet das sogenannte Kommunikationsprotokoll des Servers. Zu den gängigsten Internetprotokollen 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 (SMTP-Server);
  • POP: Post Office Protocol – das Protokoll für die Kommunikation mit einem E-Mail-Speicherserver (POP-Server). Dabei geht es um das Abrufen empfangener E-Mails, nicht um deren Versand;
  • IMAP: Internet Message Access Protocol – das Protokoll zur Kommunikation mit einem E-Mail-Speicherserver (IMAP-Server). Dieses Protokoll hat das ältere POP-Protokoll nach und nach abgelöst;
  • FTP: File Transfer Protocol – das Protokoll für die Kommunikation mit einem Dateispeicherserver (FTP-Server);

Alle diese Protokolle sind textbasiert: Client und Server tauschen Textzeilen aus. Wenn Sie über einen Client verfügen, der in der Lage ist:

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

Dann sind wir in der Lage, mit einem TCP-Server über ein textbasiertes Protokoll zu kommunizieren, vorausgesetzt, wir kennen die Regeln dieses Protokolls.

21.2.2. TCP-Dienstprogramme

Image

Im Code zu diesem Dokument gibt es zwei TCP-Kommunikationsdienstprogramme:

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

Es handelt sich um zwei C#-Programme, deren Quellcode bereitgestellt wird. Sie können diese daher ändern.

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

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

Der TCP-Client [RawTcpClient] wird mit der Syntax [RawTcpClient server port] aufgerufen, um eine Verbindung zum Port [port] auf dem Server [server] herzustellen:

  • Vom Benutzer über die Tastatur eingegebene Zeilen werden an den Server gesendet;
  • Die vom Server gesendeten Zeilen werden auf der Konsole angezeigt;
  • Die gesamte Kommunikation wird in einer Textdatei namens [server-port.txt] protokolliert;

Schauen wir uns ein Beispiel an. Öffnen Sie zwei PyCharm-Terminalfenster und navigieren Sie in jedem zum Ordner „utilities“:

Image

Starten Sie in einem der Fenster den Server [RawTcpServer] auf Port 100:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpServer.exe 100
server : Serveur générique lancé sur le port 0.0.0.0:100
server : Attente d'un client...
server : Commandes disponibles : [list, send id [texte], close id, quit]
user :
  • Zeile 1: Wir befinden uns im Ordner „utilities“;
  • Zeile 1: Wir starten den TCP-Server auf Port 100;
  • Zeilen 2–4: Der Server wartet auf einen TCP-Client und zeigt eine Liste von Befehlen an, die der Benutzer über die Tastatur eingeben kann;
  • Zeile 5: Der Server wartet auf einen Befehl, den der Benutzer über die Tastatur eingibt;

Im anderen Befehlsfenster starten wir den TCP-Client:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 100
Client [DESKTOP-30FF5FB:51173] connecté au serveur [localhost-100]
Tapez vos commandes (quit pour arrêter) :
  • Zeile 1: Wir befinden uns im Ordner „utilitaires“;
  • Zeile 1: Wir starten den TCP-Client; wir weisen ihn an, eine Verbindung zu Port 100 auf dem lokalen Rechner herzustellen (dem Rechner, auf dem der [RawTcpClient]-Code läuft);
  • Zeile 2: Der Client hat erfolgreich eine Verbindung zum Server hergestellt. Wir geben die Details des Clients an: Er befindet sich auf dem Rechner [DESKTOP-30FF5FB] (in diesem Beispiel der lokale Rechner) und nutzt Port [51173] für die Kommunikation mit dem Server:
  • Zeile 3: Der Client wartet auf einen Befehl, den der Benutzer über die Tastatur eingibt;

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


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpServer.exe 100
server : Serveur générique lancé sur le port 0.0.0.0:100
server : Attente d'un client...
server : Commandes disponibles : [list, send id [texte], close id, quit]
user : server : Client 1-DESKTOP-30FF5FB-51173 connecté...
server : Attente d'un client...
  • Zeile 5: Ein Client wurde erkannt. Der Server hat ihm die ID 1 zugewiesen. Der Server hat den Remote-Client (Rechner und Port) korrekt identifiziert;
  • Zeile 6: Der Server kehrt zum Warten auf einen neuen Client zurück;

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


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 100
Client [DESKTOP-30FF5FB:51173] connecté au serveur [localhost-100]
Tapez vos commandes (quit pour arrêter) :
hello from client
  • Zeile 4, der an den Server gesendete Befehl;

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


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpServer.exe 100
server : Serveur générique lancé sur le port 0.0.0.0:100
server : Attente d'un client...
server : Commandes disponibles : [list, send id [texte], close id, quit]
user : server : Client 1-DESKTOP-30FF5FB-51173 connecté...
server : Attente d'un client...
client 1 : [hello from client]
  • Zeile 7, in eckigen Klammern, die vom Server empfangene Nachricht;

Senden wir eine Antwort an den Client:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpServer.exe 100
server : Serveur générique lancé sur le port 0.0.0.0:100
server : Attente d'un client...
server : Commandes disponibles : [list, send id [texte], close id, quit]
user : server : Client 1-DESKTOP-30FF5FB-51173 connecté...
server : Attente d'un client...
client 1 : [hello from client]
send 1 [hello from server]
user :
  • Zeile 8, die an Client 1 gesendete Antwort. Es wird nur der Text zwischen den Klammern gesendet, nicht die Klammern selbst;

Kehren wir zum Client-Fenster zurück:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 100
Client [DESKTOP-30FF5FB:51173] connecté au serveur [localhost-100]
Tapez vos commandes (quit pour arrêter) :
hello from client
<-- [hello from server]
  • Zeile 5, die vom Client empfangene Antwort. Der empfangene Text ist der in eckigen Klammern;

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


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpServer.exe 100
server : Serveur générique lancé sur le port 0.0.0.0:100
server : Attente d'un client...
server : Commandes disponibles : [list, send id [texte], close id, quit]
user : server : Client 1-DESKTOP-30FF5FB-51173 connecté...
server : Attente d'un client...
client 1 : [hello from client]
send 1 [hello from server]
user : list
server : id=1-name=DESKTOP-30FF5FB-51173
user : close 1
server : Connexion client 1 fermée...
user : quit
server : fin du service
  • Zeile 9: Wir fordern die Liste der Clients an;
  • Zeile 10: die Antwort;
  • Zeile 11: Wir schließen die Verbindung mit Client Nr. 1;
  • Zeile 12: die Bestätigung des Servers;
  • Zeile 13: Wir fahren den Server herunter;
  • Zeile 14: die Bestätigung des Servers;

Kehren wir zum Client-Fenster zurück:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 100
Client [DESKTOP-30FF5FB:51173] connecté au serveur [localhost-100]
Tapez vos commandes (quit pour arrêter) :
hello from client
<-- [hello from server]
Perte de la connexion avec le serveur...
  • Zeile 6: Der Client hat das Ende des Dienstes erkannt;

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

Image

  • In [1] protokolliert der Server: Der Dateiname ist der Name des Clients im Format [Rechner-Port]. Dies ermöglicht unterschiedliche Protokolldateien für verschiedene Clients;
  • In [2] protokolliert der Client: Der Dateiname ist der Servername im Format [Rechner-Port];

Die Serverprotokolle lauten wie folgt:


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

Die Client-Protokolle lauten wie folgt:


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

21.3. Ermitteln des Namens oder der IP-Adresse eines Rechners im Internet

Image

Computer im Internet werden durch eine IP-Adresse (IPv4 oder IPv6) und meist auch durch einen Namen identifiziert. Letztendlich wird jedoch nur die IP-Adresse von Internet-Kommunikationsprotokollen verwendet. Daher müssen Sie die IP-Adresse eines Computers kennen, der durch seinen Namen identifiziert wird.

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

#  imports
import socket


# ------------------------------------------------
def get_ip_and_name(nom_machine: str):
    #  nom_machine: name of the machine whose address is required IP: name of the machine whose address is required IP: name of the machine whose address is required
    try:
        #  nom_machine-->adresse IP
        ip = socket.gethostbyname(nom_machine)
        print(f"ip[{nom_machine}]={ip}")
    except socket.error as erreur:
        #  error is displayed
        print(f"ip[{nom_machine}]={erreur}")
        return

    try:
        #  address IP --> nom_machine
        names = socket.gethostbyaddr(ip)
        print(f"names[{ip}]={names}")
    except socket.error as erreur:
        #  error is displayed
        print(f"names[{ip}]={erreur}")
        return


#  ---------------------------------------- main

#  internet machines
hosts = ["istia.univ-angers.fr", "www.univ-angers.fr", "sergetahe.com", "localhost", "xx"]

#  IP addresses of HOTES machines
for host in hosts:
    print("-------------------------------------")
    get_ip_and_name(host)
#  end
print("Terminé...")

Kommentare

  • Zeile 2: Das Modul [socket] stellt die Funktionen bereit, die zur Verwaltung von Internet-Sockets benötigt werden. [socket] bezieht sich auf eine Steckdose oder einen Netzwerkanschluss;
  • Zeile 6: Mit der Funktion [get_ip_and_name] können Sie aus dem Hostnamen eines Rechners Folgendes abrufen:
    • die IP-Adresse des Rechners;
    • den Namen des Rechners, der aus der zuvor ermittelten IP-Adresse abgeleitet wird;
  • Zeile 10: Die Funktion [socket.gethostbyname] ermittelt die IP-Adresse eines Rechners anhand eines seiner Namen (ein Internetrechner kann einen primären Namen und Aliase haben);
  • Zeile 12: Socket-Funktionen lösen die Ausnahme [socket.error] aus, sobald ein Fehler auftritt;
  • Zeile 19: Die Funktion [socket.gethostbyaddr] ermittelt den Namen eines Rechners anhand seiner IP-Adresse. Wir werden sehen, dass wir einen anderen Namen erhalten können als den in Zeile 6 übergebenen;
  • Zeile 30: eine Liste von Rechnernamen. Der letzte Name ist falsch. Der Name [localhost] bezieht sich auf den Rechner, an dem Sie gerade arbeiten und auf dem das Skript ausgeführt wird;
  • Zeilen 33–35: Wir zeigen die IP-Adressen dieser Rechner an;

Ergebnisse:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/inet/ip/ip_01.py
-------------------------------------
ip[istia.univ-angers.fr]=193.49.144.41
names[193.49.144.41]=('ametys-fo-2.univ-angers.fr', [], ['193.49.144.41'])
-------------------------------------
ip[www.univ-angers.fr]=193.49.144.41
names[193.49.144.41]=('ametys-fo-2.univ-angers.fr', [], ['193.49.144.41'])
-------------------------------------
ip[sergetahe.com]=87.98.154.146
names[87.98.154.146]=('cluster026.hosting.ovh.net', [], ['87.98.154.146'])
-------------------------------------
ip[localhost]=127.0.0.1
names[127.0.0.1]=('DESKTOP-30FF5FB', [], ['127.0.0.1'])
-------------------------------------
ip[xx]=[Errno 11001] getaddrinfo failed
Terminé...
 
Process finished with exit code 0

21.4. Das HTTP (HyperText Transfer Protocol)

21.4.1. Beispiel 1

Wenn ein Browser eine URL anzeigt, fungiert er als Client eines Webservers, oder anders gesagt, eines HTTP-Servers. Er ergreift die Initiative und beginnt damit, eine Reihe von Befehlen an den Server zu senden. Für dieses erste Beispiel:

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

Zunächst starten wir den Server auf Port 100:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpServer.exe 100
server : Serveur générique lancé sur le port 0.0.0.0:100
server : Attente d'un client...
server : Commandes disponibles : [list, send id [texte], close id, quit]
user :

Anschließend rufen wir über einen Browser die URL [http://localhost:100] auf, d. h. wir geben an, dass der abgefragte HTTP-Server auf Port 100 des lokalen Rechners läuft:

Image

Kehren wir zum Serverfenster zurück:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpServer.exe 100
server : Serveur générique lancé sur le port 0.0.0.0:100
server : Attente d'un client...
server : Commandes disponibles : [list, send id [texte], close id, quit]
user : server : Client 1-DESKTOP-30FF5FB-51438 connecté...
server : Attente d'un client...
server : Client 2-DESKTOP-30FF5FB-51439 connecté...
server : Attente d'un client...
client 1 : [GET / HTTP/1.1]
client 1 : [Host: localhost:100]
client 1 : [Connection: keep-alive]
client 1 : [DNT: 1]
client 1 : [Upgrade-Insecure-Requests: 1]
client 1 : [User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36]
client 1 : [Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9]
client 1 : [Sec-Fetch-Site: none]
client 1 : [Sec-Fetch-Mode: navigate]
client 1 : [Sec-Fetch-User: ?1]
client 1 : [Sec-Fetch-Dest: document]
client 1 : [Accept-Encoding: gzip, deflate, br]
client 1 : [Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7]
client 1 : []
server : Client 3-DESKTOP-30FF5FB-51441 connecté...
server : Attente d'un client...
  • Zeile 5: der Client, der sich verbunden hat;
  • Zeilen 9–22: die Reihe von Textzeilen, die er gesendet hat:
    • Zeile 9: Diese Zeile hat das Format [GET URL HTTP/1.1]. Sie fordert die URL / an und weist den Server an, das HTTP 1.1-Protokoll zu verwenden;
    • Zeile 10: Diese Zeile hat das Format [Host: server:port]. Bei dem Befehl [Host] spielt die Groß-/Kleinschreibung keine Rolle. Beachten Sie, dass der Client einen lokalen Server abfragt, der auf Port 100 läuft;
    • Zeile 14: Der Befehl [User-Agent] identifiziert den Client;
    • Zeile 15: Der Befehl [Accept] gibt an, welche Dokumenttypen vom Client akzeptiert werden;
    • Zeile 21: Die Anweisung [Accept-Language] gibt die Sprache an, in der die angeforderten Dokumente bereitgestellt werden sollen, falls sie in mehreren Sprachen verfügbar sind;
    • Zeile 11: Die Anweisung [Connection] gibt den gewünschten Verbindungsmodus an: [keep-alive] bedeutet, dass die Verbindung bis zum Abschluss des Datenaustauschs aufrechterhalten werden soll;
    • Zeile 22: Der Client beendet seine Befehle mit einer Leerzeile;

Wir beenden die Verbindung, indem wir den Server herunterfahren:


client 1 : []
server : Client 3-DESKTOP-30FF5FB-51441 connecté...
server : Attente d'un client...
quit
server : fin du service

21.4.2. Beispiel 2

Da wir nun die Befehle kennen, die ein Browser sendet, um eine URL anzufordern, werden wir diese URL mit unserem TCP-Client [RawTcpClient] anfordern. Der Apache-Server in Laragon (Abschnitt |Laragon installieren|) wird unser Webserver sein.

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

Image

Image

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

Image

  • in [1] die angeforderte URL. Wir haben ursprünglich [http://localhost:80] eingegeben, und der Browser (hier Firefox) hat dies 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

  • Klicken Sie mit der rechten Maustaste auf die empfangene Seite und wählen Sie Option [2]. Sie erhalten 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.1b PHP/7.2.19<br />
                PHP version: 7.2.19   <span><a title="phpinfo()" href="/?q=info">info</a></span><br />
                Document Root: C:/MyPrograms/laragon/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 [http://localhost:80] mit unserem TCP-Client auf:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 80
Client [DESKTOP-30FF5FB:51541] connecté au serveur [localhost-80]
Tapez vos commandes (quit pour arrêter) :
  • Zeile 1: Wir stellen eine Verbindung zu Port 80 auf dem Localhost-Server her. Hier läuft der Laragon-Webserver;

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


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 80
Client [DESKTOP-30FF5FB:51544] connecté au serveur [localhost-80]
Tapez vos commandes (quit pour arrêter) :
GET / HTTP/1.1
Host: localhost:80
 
<-- [HTTP/1.1 200 OK]
<-- [Date: Sun, 05 Jul 2020 12:42:14 GMT]
<-- [Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19]
<-- [X-Powered-By: PHP/7.2.19]
<-- [Content-Length: 1776]
<-- [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.1b PHP/7.2.19<br />]
<-- [                      PHP version: 7.2.19   <span><a title="phpinfo()" href="/?q=info">info</a></span><br />]
<-- [                      Document Root: C:/MyPrograms/laragon/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>]
Perte de la connexion avec le serveur...
  • Zeile 4, der [GET]-Befehl. Wir fordern das Stammverzeichnis / des Webservers an;
  • Zeile 5, der [Host]-Befehl;
  • das sind die einzigen beiden wesentlichen Befehle. Für die anderen Befehle verwendet der Webserver Standardwerte;
  • Zeile 6, die Leerzeile, die die Befehle des Clients beenden muss;
  • unterhalb von Zeile 6 folgt die Antwort des Webservers;
  • Zeilen 7–12: die HTTP-Header der Antwort des Servers;
  • Zeile 13: die Leerzeile, die das Ende der HTTP-Header signalisiert;
  • Zeilen 14–82: das in Zeile 4 angeforderte HTML-Dokument;

Wir laden die Protokolldatei [localhost-80.txt]:

Image


--> [GET / HTTP/1.1]
--> [Host: localhost:80]
--> []
<-- [HTTP/1.1 200 OK]
<-- [Date: Sun, 05 Jul 2020 12:42:14 GMT]
<-- [Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19]
<-- [X-Powered-By: PHP/7.2.19]
<-- [Content-Length: 1776]
<-- [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.1b PHP/7.2.19<br />]
<-- [                      PHP version: 7.2.19   <span><a title="phpinfo()" href="/?q=info">info</a></span><br />]
<-- [                      Document Root: C:/MyPrograms/laragon/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 HTML-Dokument. Im vorherigen Beispiel hat Firefox dasselbe Dokument empfangen;

Wir verfügen nun über die Grundlagen, um einen TCP-Client zu programmieren, der eine URL anfordert.

21.4.3. Beispiel 3

Image

Das Skript [http/01/main.py] ist ein HTTP-Client, der durch die Datei [config.py] konfiguriert wird. Sein Inhalt lautet wie folgt:

def configure():
    #  URLs to query
    urls = [
        #  site: name of the site to connect to
        #  port: web service port
        #  GET : URL requested
        #  headers: HTTP headers to be sent in the request
        #  endOfLine: end-of-line marker in headers HTTP sent
        #  encoding: encoding the server response
        #  timeout: maximum wait time for a server response
        {
            "site": "localhost",
            "port": 80,
            "GET": "/",
            "headers": {
                "Host": "localhost:80",
                "User-Agent": "client Python",
                "Accept": "text/HTML",
                "Accept-Language": "fr"
            },
            "endOfLine": "\r\n",
            "encoding": "utf-8",
            "timeout": 0.5
        },
        {
            "site": "sergetahe.com",
            "port": 80,
            "GET": "/",
            "headers": {
                "Host": "sergetahe.com:80",
                "User-Agent": "client Python",
                "Accept": "text/HTML",
                "Accept-Language": "fr"
            },
            "endOfLine": "\r\n",
            "encoding": "utf-8",
            "timeout": 5
        },
        {
            "site": "tahe.developpez.com",
            "port": 443,
            "GET": "/",
            "headers": {
                "Host": "tahe.developpez.com:443",
                "User-Agent": "client Python",
                "Accept": "text/HTML",
                "Accept-Language": "fr"
            },
            "endOfLine": "\r\n",
            "encoding": "utf-8",
            "timeout": 2
        },
        {
            "site": "www.sergetahe.com",
            "port": 80,
            "GET": "/cours-tutoriels-de-programmation/",
            "headers": {
                "Host": "sergetahe.com:80",
                "User-Agent": "client Python",
                "Accept": "text/HTML",
                "Accept-Language": "fr"
            },
            "endOfLine": "\r\n",
            "encoding": "utf-8",
            "timeout": 5
        }
    ]
    #  we return the configuration
    return {
        "urls": urls
    }
  • Der Inhalt der Datei ist eine Liste von URLs, wobei jeder Eintrag in der Liste ein Wörterbuch ist. Dieses Wörterbuch legt fest, wie eine Verbindung zu der durch den Schlüssel [site] bezeichneten Website hergestellt wird;
  • Zeilen 4–10: die Bedeutung der Schlüssel in jedem Wörterbuch;

Das Skript [http/01/main.py] lautet wie folgt:

#  imports
import codecs
import socket


# -----------------------------------------------------------------------
def get_url(url: dict, suivi: bool = True):
    #  reads the URL url["GET"] from the url[site] site and stores it in the url[site].html file
    #  client/server dialog is based on the HTTP protocol specified in the [url] dictionary
    #  we let the exceptions rise

    sock = None
    html = None
    try:
        #  connection to [site] on port 80 with a timeout
        site = url['site']
        sock = socket.create_connection((site, int(url['port'])), float(url['timeout']))

        #  connection represents a bidirectional communication flow
        #  between the client (this program) and the contacted web server
        #  this channel is used for the exchange of orders and information
        #  the dialog protocol is HTTP

        #  create file site.html - change troublesome characters for a file name
        site2 = site.replace("/", "_")
        site2 = site2.replace(".", "_")
        html_filename = f'{site2}.html'
        html = codecs.open(f"output/{html_filename}", "w", "utf-8")

        #  the client will start the HTTP dialog with the server
        if suivi:
            print(f"Client : début de la communication avec le serveur [{site}]")

        #  depending on the server, client lines must end with \nor \r\n
        end_of_line = url["endOfLine"]
        #  the customer sends the GET command to request the URL config["GET"]
        #  syntax GET URL HTTP/1.1
        commande = f"GET {url['GET']} HTTP/1.1{end_of_line}"
        #  followed?
        if suivi:
            print(f"--> {commande}", end='')
        #  send the command to the server
        sock.send(bytearray(commande, 'utf-8'))
        #  header transmission HTTP
        for verb, value in url['headers'].items():
            #  build the command to be sent
            commande = f"{verb}: {value}{end_of_line}"
            #  followed?
            if suivi:
                print(f"--> {commande}", end='')
            #  send the command to the server
            sock.send(bytearray(commande, 'utf-8'))
        #  we send the HTTP header [Connection: close] to ask the web server to
        #  close the connection once the requested document has been sent
        sock.send(bytearray(f"Connection: close{end_of_line}", 'utf-8'))
        #  protocol HTTP headers must end with an empty line
        sock.send(bytearray(end_of_line, 'utf-8'))
        #
        #  the server will now respond on the sock channel. It will send all
        #  then close the channel. The client therefore reads everything that arrives from sock
        #  until the channel closes
        #
        #  we first read the HTTP headers sent by the server
        #  they also end with an empty line
        if suivi:
            print(f"Réponse du serveur [{site}]")

        #  read the socket as if it were a text file
        encoding = f"{url['encoding']}" if url['encoding'] else None
        if encoding:
            file = sock.makefile(encoding=encoding)
        else:
            file = sock.makefile()
        #  we process this file line by line
        fini = False
        while not fini:
            #  current line reading
            ligne = file.readline().strip()
            #  do we have a non-empty line?
            if ligne:
                if suivi:
                    #  header HTTP is displayed
                    print(f"<-- {ligne}")
            else:
                #  this was the empty line - HTTP headers are finished
                fini = True
        #  we read the HTML document that will follow the empty line
        #  current line reading
        ligne = file.readline()
        while ligne:
            #  record in log file
            html.write(str(ligne))
            #  next line
            ligne = file.readline()
            #  the loop ends when the server closes the connection
    finally:
        #  the customer closes the connection
        if sock:
            sock.close()
        #  close html file
        if html:
            html.close()


#  -------------------main

#  configure the application
import config
config = config.configure()

#  get the URL from the configuration file
for url in config['urls']:
    print("-------------------------")
    print(url['site'])
    print("-------------------------")
    try:
        #  reading URL from the site [site]
        get_url(url)
    except BaseException as erreur:
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
#  end
print("Terminé...")

Code-Kommentare:

  • Zeilen 108–109: Das [config]-Wörterbuch aus dem Modul [config.py] wird abgerufen;
  • Zeilen 111–122: Dieses Wörterbuch wird verwendet;
  • Zeilen 118, 7: Die Funktion [get_url(url)] fordert ein Dokument von der Website url[site] an und speichert es in der Textdatei url[site].HTML. Standardmäßig werden Client-Server-Kommunikationen in der Konsole protokolliert (tracking=True);
  • Alles erfolgt innerhalb eines [try / finally]-Blocks (Zeilen 14–96). Es gibt keine [except]-Klausel. Ausnahmen werden an den aufrufenden Code weitergeleitet, der sie abfängt und anzeigt (Zeilen 119–120);
  • Zeilen 16–17: Öffnen einer Verbindung zum Webserver. Die Funktion [socket.create_connection] benötigt drei Parameter:
    • [param1]: ist der Name des Internet-Rechners, den Sie erreichen möchten;
    • [param2]: ist die Portnummer des Dienstes, mit dem Sie sich verbinden möchten;
    • [param3]: [socket.create_connection] gibt einen Socket zurück, und [param3] gibt, falls vorhanden, das Timeout für den erstellten Socket an. Das Timeout ist die maximale Wartezeit für den Socket, während er auf eine Antwort vom Remote-Rechner wartet;
  • Zeilen 27–28: Erstellung der Datei [site.html], in der das empfangene HTML-Dokument gespeichert wird;
  • Zeilen 34–43: Der erste Befehl des Clients muss der Befehl [GET URL HTTP/1.1] sein;
  • Zeile 43: Die Funktion [sock.send] ermöglicht es dem Client, Daten an den Server zu senden. Die hier gesendete Textzeichenfolge hat folgende Bedeutung: „Ich möchte die Seite [URL] von der Website, mit der ich verbunden bin, abrufen (GET). Ich verwende HTTP Version 1.1“;
  • Zeile 43: Die Anweisung [sock.send(bytearray(command, 'utf-8'))] sendet ein Byte-Array. Dieses Array wird durch die Umwandlung der Zeichenkette [command] in eine in UTF-8 kodierte Bytefolge erhalten;
  • Zeilen 44–52: Die anderen Zeilen des HTTP-Protokolls [Host, User-Agent, Accept, Accept-Language…] werden gesendet. Ihre Reihenfolge spielt keine Rolle;
  • Zeilen 53–55: Der HTTP-Header [Connection: close] wird gesendet, um den Server anzuweisen, die Verbindung zu schließen, sobald er das angeforderte Dokument gesendet hat. Standardmäßig tut er dies nicht. Daher muss dies ausdrücklich angefordert werden. Der Vorteil besteht darin, dass diese Schließung auf der Client-Seite erkannt wird, und so weiß der Client, dass er das gesamte angeforderte Dokument erhalten hat;
  • Zeilen 56–57: Eine Leerzeile wird an den Server gesendet, um anzuzeigen, dass der Client das Senden seiner HTTP-Header abgeschlossen hat und nun auf das angeforderte Dokument wartet;
  • Zeilen 68–86: Der Server sendet zunächst eine Reihe von HTTP-Headern, die verschiedene Details zum angeforderten Dokument enthalten. Diese Header enden mit einer Leerzeile;
  • Zeilen 69–73: Um die Antwort des Servers Zeile für Zeile zu lesen, verwenden wir die Methode [sock.makefile(encoding=encoding)]. Der optionale Parameter [encoding] gibt die erwartete Textkodierung an. Nach diesem Vorgang kann der vom Server gesendete Zeilenstrom wie eine normale Textdatei gelesen werden;
  • Zeile 78: Wir lesen eine vom Server gesendete Zeile mit der Methode [readline]. Wir entfernen die führenden und nachfolgenden Leerzeichen (Leerzeichen, Zeilenumbruchzeichen);
  • Zeilen 81–83: Wenn die Zeile nicht leer ist und eine Nachverfolgung angefordert wurde, wird die empfangene Zeile auf der Konsole angezeigt;
  • Zeilen 84–86: Wenn die Leerzeile, die das Ende der vom Server gesendeten HTTP-Header markiert, abgerufen wurde, wird die Schleife in Zeile 76 beendet;
  • Zeilen 90–95: Die Textzeilen der Serverantwort können mithilfe einer while-Schleife zeilenweise gelesen und in der Textdatei [html] gespeichert werden. Wenn der Webserver die gesamte angeforderte Seite gesendet hat, schließt er die Verbindung zum Client. Auf der Clientseite wird dies als Dateiende erkannt, und wir verlassen die Schleife in den Zeilen 90–95;
  • Zeilen 96–102: Unabhängig davon, ob ein Fehler auftritt oder nicht, werden alle vom Code verwendeten Ressourcen freigegeben;

Ergebnisse:

Die Konsole zeigt die folgenden Protokolle an:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/inet/http/01/main.py
-------------------------
localhost
-------------------------
Client : début de la communication avec le serveur [localhost]
--> GET / HTTP/1.1
--> Host: localhost:80
--> User-Agent: client Python
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [localhost]
<-- HTTP/1.1 200 OK
<-- Date: Sun, 05 Jul 2020 16:27:46 GMT
<-- Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19
<-- X-Powered-By: PHP/7.2.19
<-- Content-Length: 1776
<-- Connection: close
<-- Content-Type: text/html; charset=UTF-8
-------------------------
sergetahe.com
-------------------------
Client : début de la communication avec le serveur [sergetahe.com]
--> GET / HTTP/1.1
--> Host: sergetahe.com:80
--> User-Agent: client Python
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [sergetahe.com]
<-- HTTP/1.1 302 Found
<-- Date: Sun, 05 Jul 2020 16:27:45 GMT
<-- Content-Type: text/html; charset=UTF-8
<-- Transfer-Encoding: chunked
<-- Connection: close
<-- Server: Apache
<-- X-Powered-By: PHP/7.3
<-- Location: http://sergetahe.com:80/cours-tutoriels-de-programmation
<-- Set-Cookie: SERVERID68971=2620178|XwH/h|XwH/h; path=/
<-- X-IPLB-Instance: 17106
-------------------------
tahe.developpez.com
-------------------------
Client : début de la communication avec le serveur [tahe.developpez.com]
--> GET / HTTP/1.1
--> Host: tahe.developpez.com:443
--> User-Agent: client Python
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [tahe.developpez.com]
<-- HTTP/1.1 400 Bad Request
<-- Date: Sun, 05 Jul 2020 16:27:45 GMT
<-- Server: Apache/2.4.38 (Debian)
<-- Content-Length: 453
<-- Connection: close
<-- Content-Type: text/html; charset=iso-8859-1
-------------------------
www.sergetahe.com
-------------------------
Client : début de la communication avec le serveur [www.sergetahe.com]
--> GET /cours-tutoriels-de-programmation/ HTTP/1.1
--> Host: sergetahe.com:80
--> User-Agent: client Python
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [www.sergetahe.com]
<-- HTTP/1.1 301 Moved Permanently
<-- Date: Sun, 05 Jul 2020 16:27:45 GMT
<-- Content-Type: text/html; charset=iso-8859-1
<-- Content-Length: 263
<-- Connection: close
<-- Server: Apache
<-- Location: https://sergetahe.com/cours-tutoriels-de-programmation/
<-- Set-Cookie: SERVERID68971=2620178|XwH/h|XwH/h; path=/
<-- X-IPLB-Instance: 17095
Terminé...
 
Process finished with exit code 0

Kommentare

  • Zeile 12: Die URL [http://localhost/] wurde gefunden (Code 200);
  • Zeile 29: Die URL [http://sergetahe.com/] wurde nicht gefunden (Code 302). Code 302 bedeutet, dass sich die URL der angeforderten Seite geändert hat. Die neue URL wird durch den HTTP-Header [Location] in Zeile 36 angegeben;
  • Zeile 49: Die an den Server gesendete Anfrage [http://tahe.developpez.com] ist ungültig (Statuscode 400);
  • Zeile 65: Die URL [http://www.sergetahe.com/] wurde nicht gefunden (Code 301). Code 301 bedeutet, dass die angeforderte Seite ihre URL dauerhaft geändert hat. Die neue URL wird durch den HTTP-Header [Location] in Zeile 71 angegeben;

Im Allgemeinen sind 3xx-, 4xx- und 5xx-Codes von einem HTTP-Server Fehlercodes.

Die Ausführung hat die folgenden Dateien erzeugt:

Image

Die empfangene Datei [output/localhost.HTML] lautet 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.1b PHP/7.2.19<br />
                      PHP version: 7.2.19   <span><a title="phpinfo()" href="/?q=info">info</a></span><br />
                      Document Root: C:/MyPrograms/laragon/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.

Das erhaltene Dokument [output/sergetahe_com.html] sieht wie folgt aus:

Image

Die meisten HTTP-Server senden ihre Antworten auf Anfragen in Blöcken. Jedem gesendeten Block geht eine Zeile voraus, die die Anzahl der Bytes im folgenden Block angibt. Dies ermöglicht es dem Client, genau diese Anzahl an Bytes zu lesen, um den Block zu empfangen. Hier bedeutet die 0, dass der folgende Block null Bytes umfasst. Erinnern Sie sich daran, dass der Server angegeben hatte, dass sich die URL des Dokuments [http://sergetahe.com/] geändert hatte. Daher hat er kein Dokument gesendet.

Das Dokument [output/tahe_developpez_com.html] lautet 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.38 (Debian) Server at 2eurocents.developpez.com Port 80</address>
</body></html>
  • Zeilen 1–12: Der Server hat ein HTML-Dokument gesendet, obwohl die Anfrage fehlerhaft war (Zeile 49 der Ergebnisse). Das HTML-Dokument ermöglicht es dem Server, die Ursache des Fehlers anzugeben. Dies ist in den Zeilen 6 und 7 zu sehen:
    • Zeile 7: Unser Client verwendete das HTTP-Protokoll;
    • Zeile 8: Der Server verwendet das HTTPS-Protokoll (S = sicher) und akzeptiert das HTTP-Protokoll nicht;

Das Dokument [output/www_sergetahe_com.html] lautet wie folgt:


<!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="https://sergetahe.com/cours-tutoriels-de-programmation/">here</a>.</p>
</body></html>

Auch hier ist ein Fehler aufgetreten (Zeile 3). Der Server sorgt jedoch dafür, dass ein HTML-Dokument gesendet wird, das den Fehler detailliert beschreibt (Zeilen 1–7).

21.4.4. Beispiel 4

Die vorherigen Beispiele haben uns gezeigt, dass unser HTTP-Client unzureichend war. Wir werden nun ein Tool namens [curl] vorstellen, mit dem wir Webdokumente abrufen und gleichzeitig die genannten Herausforderungen bewältigen können: HTTPS-Protokoll, in Chunks gesendete Dokumente, Weiterleitungen… Das [curl]-Tool wurde mit Laragon installiert:

Image

Öffnen wir ein PyCharm-Terminal [1]:

Image

  • in [1], Zugriff auf PyCharm-Terminals;
  • in [2-3] die bereits aktiven Terminals;
  • in [4] das Verzeichnis, in dem Sie sich gerade befinden. Es spielt keine Rolle, welches Sie verwenden;

Geben Sie im Terminal den folgenden Befehl ein:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>curl --help
Usage: curl [options...] <url>
     --abstract-unix-socket <path> Connect via abstract Unix domain socket
     --anyauth       Pick any authentication method
 -a, --append        Append to target file when uploading
     --basic         Use HTTP Basic Authentication
     --cacert <CA certificate> CA certificate to verify peer against

Die Tatsache, dass der Befehl [curl –help] Ergebnisse lieferte, zeigt, dass der Befehl [curl] im PATH des Terminals enthalten ist. Unter Windows ist der PATH die Menge der Ordner, die durchsucht werden, wenn der Benutzer einen ausführbaren Befehl eingibt, in diesem Fall [curl]. Der Wert des PATH lässt sich wie folgt ermitteln:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>echo %PATH%
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts;C:\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\Program Files\Python38\Scripts\;C:\Program Files\Python38\;C:\windows\system32;C:\windows;C:\windows\System32\Wbem;C:\windows\System32\WindowsPowerShell\v1.0\;C:\windows\System32\OpenSSH\;C:\Program Files\Git\cmd;C:\Users\serge\AppData\Local\Microsoft\WindowsApps;;C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\bin;

Zeile 2: Die PATH-Ordner, getrennt durch Semikolons. In dieser Liste erscheint kein Ordner, der mit Laragon in Verbindung steht. Bei weiterer Untersuchung stellen wir fest, dass sich im Ordner [c:\windows\system32] ein [curl] befindet. Dies ist derjenige, der zuvor geantwortet hat.

Wenn Sie das in Laragon enthaltene [curl]-Tool verwenden möchten, können Sie wie folgt vorgehen:

Image

Image

  • in [2], dem Laragon-Terminal;
  • in [3] können Sie mit dieser Schaltfläche neue Terminals erstellen, die jeweils in einem Tab im obigen Fenster geöffnet werden;
  • in [4] legen wir den PATH für das Laragon-Terminal fest;
  • Sie erhalten etwas ganz anderes als das, was in einem PyCharm-Terminal angezeigt wurde. Dieser PATH enthält viele Ordner, die während der Installation von Laragon erstellt wurden. Der Ordner, der das [curl]-Tool enthält, ist einer davon:

Image

Verwenden Sie anschließend das Terminal Ihrer Wahl. Beachten Sie jedoch, dass das Laragon-Terminal die bevorzugte Option ist, wenn Sie ein von Laragon bereitgestelltes Tool verwenden möchten.

Der Befehl [curl --help] zeigt alle Konfigurationsoptionen von [curl] an. Es gibt Dutzende davon. Wir werden nur sehr wenige davon verwenden. Um eine URL abzufragen, geben Sie einfach den Befehl [curl URL] ein. Dieser Befehl zeigt das angeforderte Dokument auf der Konsole an. Wenn Sie auch den HTTP-Datenaustausch zwischen Client und Server sehen möchten, geben Sie [curl --verbose URL] ein. Um das angeforderte HTML-Dokument schließlich in einer Datei zu speichern, geben Sie [curl --verbose --output Dateiname URL] ein.

Um das Dateisystem unseres Rechners nicht zu überladen, wechseln wir in ein anderes Verzeichnis (ich verwende hier ein Laragon-Terminal):


λ cd \Temp\
 
C:\Temp
λ mkdir curl
 
C:\Temp
λ cd curl\
 
C:\Temp\curl
λ dir
 Le volume dans le lecteur C s’appelle Local Disk
 Le numéro de série du volume est B84C-D958
 
 Répertoire de C:\Temp\curl
 
05/07/2020  19:31    <DIR>          .
05/07/2020  19:31    <DIR>          ..
               0 fichier(s)                0 octets
               2 Rép(s)  892 388 098 048 octets libres                                          
  • Zeile 3: Wir navigieren zum Ordner [c:\temp]. Falls dieser Ordner nicht existiert, können Sie ihn erstellen oder einen anderen wählen;
  • Zeile 6: Erstellen Sie einen Ordner mit dem Namen [curl];
  • Zeile 9: Wir navigieren dorthin;
  • Zeile 12: Listen Sie den Inhalt auf. Er ist leer (Zeile 20);

Stellen Sie sicher, dass der Laragon-Apache-Server läuft, und rufen Sie mit [curl] die URL [http://localhost/] mit dem Befehl [curl –verbose –output localhost.html http://localhost/] auf. Sie erhalten folgende Ergebnisse:


λ 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
*   Trying 127.0.0.1...
* TCP_NODELAY set
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0* Connected to localhost (::1) port 80 (#0)
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sun, 05 Jul 2020 17:35:43 GMT
< Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19
< X-Powered-By: PHP/7.2.19
< Content-Length: 1776
< Content-Type: text/html; charset=UTF-8
<
{ [1776 bytes data]
100  1776  100  1776    0     0   1062      0  0:00:01  0:00:01 --:--:--  1062
* Connection #0 to host localhost left intact
  • Zeilen 10–13: von [curl] an den [localhost]-Server gesendete Zeilen. Das HTTP-Protokoll wird erkannt;
  • Zeilen 14–20: vom Server als Antwort gesendete Zeilen;
  • Zeile 14: 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.

Rufen wir nun die URL [https://tahe.developpez.com:443/] auf. Um auf diese URL zugreifen zu können, muss der HTTP-Client HTTPS unterstützen. Dies ist beim [curl]-Client der Fall.

Die Konsolenausgabe sieht wie folgt aus:


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
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* 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\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):
{ [122 bytes data]
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
{ [25 bytes data]
* TLSv1.3 (IN), TLS handshake, Certificate (11):
{ [2563 bytes data]
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
{ [264 bytes data]
* TLSv1.3 (IN), TLS handshake, Finished (20):
{ [52 bytes data]
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
} [1 bytes data]
* TLSv1.3 (OUT), TLS handshake, Finished (20):
} [52 bytes data]
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: CN=*.developpez.com
*  start date: Jul  1 15:38:30 2020 GMT
*  expire date: Sep 29 15:38:30 2020 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]
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
{ [281 bytes data]
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
{ [297 bytes data]
* old SSL session ID is stale, removing
{ [5 bytes data]
< HTTP/1.1 200 OK
< Date: Sun, 05 Jul 2020 17:39:53 GMT
< Server: Apache/2.4.38 (Debian)
< X-Powered-By: PHP/5.3.29
< Vary: Accept-Encoding
< Transfer-Encoding: chunked
< Content-Type: text/html
<
{ [6 bytes data]
100   99k    0   99k    0     0  79343      0 --:--:--  0:00:01 --:--:-- 79343
* Connection #0 to host tahe.developpez.com left intact
  • Zeilen 10–39: Client-Server-Austausch zur Sicherung der Verbindung: Diese wird verschlüsselt;
  • Zeilen 41–44: Die vom Client [curl] an den Server gesendeten HTTP-Header;
  • Zeile 52: Das angeforderte Dokument wurde gefunden;
  • Zeile 57: Das Dokument wird in Blöcken gesendet;

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

Rufen wir nun die URL [http://sergetahe.com/cours-tutoriels-de-programmation] auf. Wir haben gesehen, dass es für diese URL eine Weiterleitung zur URL [http://sergetahe.com/cours-tutoriels-de-programmation/] gab (mit einem / am Ende).

Die Konsolenausgabe lautet wie folgt:


C:\Temp\curl
λ curl --verbose --output sergetahe.com.html --location http://sergetahe.com/cours-tutoriels-de-programmation
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 87.98.154.146...
* TCP_NODELAY set
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation HTTP/1.1
> Host: sergetahe.com
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Date: Sun, 05 Jul 2020 17:44:17 GMT
< Content-Type: text/html; charset=iso-8859-1
< Content-Length: 262
< Server: Apache
< Location: http://sergetahe.com/cours-tutoriels-de-programmation/
< Set-Cookie: SERVERID68971=2620178|XwIRd|XwIRd; path=/
< X-IPLB-Instance: 17095
<
* Ignoring the response-body
{ [262 bytes data]
100   262  100   262    0     0   1858      0 --:--:-- --:--:-- --:--:--  1858
* Connection #0 to host sergetahe.com left intact
* Issue another request to this URL: 'http://sergetahe.com/cours-tutoriels-de-programmation/'
* Found bundle for host sergetahe.com: 0x14385f8 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) with host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> 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: Sun, 05 Jul 2020 17:44:17 GMT
< Content-Type: text/html; charset=iso-8859-1
< Content-Length: 263
< Server: Apache
< Location: https://sergetahe.com/cours-tutoriels-de-programmation/
< Set-Cookie: SERVERID68971=2620178|XwIRd|XwIRd; path=/
< X-IPLB-Instance: 17095
<
* Ignoring the response-body
{ [263 bytes data]
100   263  100   263    0     0    764      0 --:--:-- --:--:-- --:--:--   764
* Connection #0 to host sergetahe.com left intact
* Issue another request to this URL: 'https://sergetahe.com/cours-tutoriels-de-programmation/'
*   Trying 87.98.154.146...
* TCP_NODELAY set
* Connected to sergetahe.com (87.98.154.146) port 443 (#1)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: C:\MyPrograms\laragon\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):
{ [102 bytes data]
* TLSv1.2 (IN), TLS handshake, Certificate (11):
{ [2572 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]
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* 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 h2
* Server certificate:
*  subject: CN=sergetahe.com
*  start date: May 10 01:41:15 2020 GMT
*  expire date: Aug  8 01:41:15 2020 GMT
*  subjectAltName: host "sergetahe.com" matched cert's "sergetahe.com"
*  issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
} [5 bytes data]
* Using Stream ID: 1 (easy handle 0x2bee870)
} [5 bytes data]
> GET /cours-tutoriels-de-programmation/ HTTP/2
> Host: sergetahe.com
> User-Agent: curl/7.63.0
> Accept: */*
>
{ [5 bytes data]
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
} [5 bytes data]
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0< HTTP/2 200
< date: Sun, 05 Jul 2020 17:44:19 GMT
< content-type: text/html; charset=UTF-8
< server: Apache
< x-powered-by: PHP/7.3
< link: <https://sergetahe.com/cours-tutoriels-de-programmation/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/cours-tutoriels-de-programmation/>; rel=shortlink
< vary: Accept-Encoding
< x-iplb-instance: 17080
< set-cookie: SERVERID68971=2620178|XwIRd|XwIRd; path=/
<
{ [5 bytes data]
100 49634    0 49634    0     0  26040      0 --:--:--  0:00:01 --:--:-- 37830
* Connection #1 to host sergetahe.com left intact
  • Zeile 2: Die Option [--location] wird verwendet, um anzugeben, dass wir den vom Server gesendeten Weiterleitungen folgen wollen;
  • Zeile 13: Der Server gibt an, dass sich die URL des angeforderten Dokuments geändert hat;
  • Zeile 18: Er gibt die neue URL des angeforderten Dokuments an;
  • Zeile 31: [curl] sendet eine neue Anfrage an die neue URL;
  • Zeile 36: Der Server antwortet erneut, dass sich die URL geändert hat;
  • Zeile 41: Die neue URL ist genau dieselbe wie die, zu der umgeleitet wurde, mit einem kleinen Unterschied: Das Protokoll hat sich geändert. Es ist nun HTTPS (Zeile 41), während es zuvor HTTP war (Zeile 31);
  • Zeile 49: Eine neue Anfrage wird an die neue URL gesendet. Diese Anfrage ist verschlüsselt. Folglich findet ein Sicherheitsaushandlungsprozess statt (Zeilen 53–91);
  • Zeile 92: Die neue URL wird angefordert, diesmal unter Verwendung des HTTP/2-Protokolls;
  • Zeile 100: Das Dokument wurde gefunden;

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


C:\Temp\curl
λ dir
 Le volume dans le lecteur C s’appelle Local Disk
 Le numéro de série du volume est B84C-D958
 
 Répertoire de C:\Temp\curl
 
05/07/2020  19:44    <DIR>          .
05/07/2020  19:44    <DIR>          ..
05/07/2020  19:35             1 776 localhost.html
05/07/2020  19:44            49 634 sergetahe.com.html
05/07/2020  19:39           101 639 tahe.developpez.com.html
               3 fichier(s)          153 049 octets
               2 Rép(s)  892 385 628 160 octets libres

21.4.5. Beispiel 5

Python verfügt über ein Modul namens [pyccurl], mit dem Sie die Funktionen des Tools [curl] in einem Python-Programm nutzen können. Wir installieren dieses Modul:

Image

Wir schreiben ein neues Skript [http/02/main.py]:

Image

Die Datei [http/02/config] sieht wie folgt aus:

def configure():
    #  list of URL to be queried
    urls = [
        #  site: server to connect to
        #  timeout: maximum time to wait for a response from the server
        #  target: url to request
        #  encoding: encoding the server response
        {
            "site": "sergetahe.com",
            "timeout": 2000,
            "target": "http://sergetahe.com",
            "encoding": "utf-8"
        },
        {
            "site": "tahe.developpez.com",
            "timeout": 500,
            "target": "https://tahe.developpez.com",
            "encoding": "iso-8859-1"
        },
        {
            "site": "www.polytech-angers.fr",
            "timeout": 500,
            "target": "http://www.polytech-angers.fr",
            "encoding": "utf-8"
        },
        {
            "site": "localhost",
            "timeout": 500,
            "target": "http://localhost",
            "encoding": "utf-8"
        }
    ]
    #  we return the configuration
    return {
        'urls': urls
    }

Die Datei enthält eine Liste von Wörterbüchern, von denen jedes die folgende Struktur aufweist:

  • site: der Name eines Webservers;
  • encoding: der erwartete Dokument-Kodierungstyp;
  • timeout: maximale Wartezeit auf die Antwort des Servers, angegeben in Millisekunden. Nach Ablauf dieser Zeit trennt der Client die Verbindung;
  • url: URL des angeforderten Dokuments;

Der Skriptcode [http/02/main.py] lautet wie folgt:

#  imports
import codecs
from io import BytesIO

import pycurl


# -----------------------------------------------------------------------
def get_url(url: dict, suivi=True):
    #  reads the URL url[url] and stores it in the file output/url['site'].html
    #  if [suivi=True] then there is a console follow-up of the client/server exchange
    #  url[timeout] is the customer call timeout;
    #  url [encoding] is the encoding of the requested document

    #  retrieve configuration data
    server = url['site']
    timeout = url['timeout']
    target = url['target']
    encoding = url['encoding']
    #  follow-up
    print(f"Client : début de la communication avec le serveur [{server}]")

    #  we let the exceptions rise
    html = None
    curl = None
    try:
        #  Session initialization cURL
        curl = pycurl.Curl()
        #  binary flow
        flux = BytesIO()
        #  curl options
        options = {
            #  URL
            curl.URL: target,
            #  WRITEDATA: where received data will be stored
            curl.WRITEDATA: flux,
            #  verbose mode
            curl.VERBOSE: suivi,
            #  new connection - no cache
            curl.FRESH_CONNECT: True,
            #  request timeout (in seconds)
            curl.TIMEOUT: timeout,
            curl.CONNECTTIMEOUT: timeout,
            #  do not check the validity of SSL certificates
            curl.SSL_VERIFYPEER: False,
            #  track redirects
            curl.FOLLOWLOCATION: True
        }
        #  curl settings
        for option, value in options.items():
            curl.setopt(option, value)
        #  Execution of the CURL query with these parameters
        curl.perform()
        #  create file server.html - change troublesome characters for a file name
        server2 = server.replace("/", "_")
        server2 = server2.replace(".", "_")
        html_filename = f'{server2}.html'
        html = codecs.open(f"output/{html_filename}", "w", encoding)
        #  saving the received document in the HTML file
        html.write(flux.getvalue().decode(encoding))
    finally:
        #  freeing up resources
        if curl:
            curl.close()
        if html:
            html.close()


#  -------------------main
#  configure the application
import config
config = config.configure()

#  get the URL from the configuration file
for url in config['urls']:
    print("-------------------------")
    print(url['site'])
    print("-------------------------")
    try:
        #  reading URL from site [site]
        get_url(url)
    #  except BaseException as error:
    #      print(f "The following error has occurred: {error}")
    finally:
        pass
#  end
print("Terminé...")

Kommentare

  • Zeile 5: Wir importieren das Modul [pycurl];
  • Zeile 3: Wir importieren die Klasse [BytesIO], die es uns ermöglicht, die vom Server empfangenen Daten in einem Binärstrom zu speichern;
  • Zeilen 70–72: Wir rufen die Anwendungskonfiguration ab;
  • Zeilen 75–85: Wir durchlaufen die Liste der in der Konfiguration gefundenen URLs;
  • Zeile 81: Für jede URL rufen wir die Funktion [get_url] auf, die die URL url['target'] mit einer Zeitüberschreitung von url['timeout'] herunterlädt;
  • Zeile 9: Die Funktion [get_url] erhält die Konfiguration für die abzufragende URL;
  • Zeilen 16–19: Die URL-Konfiguration wird in separate Variablen geladen;
  • Zeilen 26, 61: Alle Operationen werden innerhalb eines try/finally-Blocks ausgeführt. Ausnahmen werden hier nicht abgefangen; sie werden an den aufrufenden Code weitergeleitet, der sie behandelt;
  • Zeile 28: Eine [curl]-Sitzung wird vorbereitet. [pycurl.Curl()] gibt eine [curl]-Ressource zurück, die die Transaktion mit einem Server ausführt;
  • Zeile 30: Instanziierung des Binärstroms, in dem die empfangenen Daten gespeichert werden;
  • Zeilen 32–48: Das [options]-Wörterbuch konfiguriert die [curl]-Verbindung zum Server. Ihre Funktionen sind in den Kommentaren angegeben;
  • Zeilen 49–51: Die Verbindungsoptionen werden an die [curl]-Ressource übergeben;
  • Zeile 53: stellt mit den definierten Optionen eine Verbindung zur angeforderten URL her. Aufgrund der Option [curl.WRITEDATA: stream] (Zeile 36) speichert die Funktion [curl.perform()] die empfangenen Daten in [stream];
  • Zeilen 54–60: Die HTML-Datei, in der das empfangene HTML-Dokument gespeichert wird, wird erstellt;
  • Zeile 60: Der Binärstrom [flux.getvalue()] wird als Zeichenkette in der HTML-Datei gespeichert. Die Kodierung dieser Zeichenkette wird in der Methode [decode(encoding)] festgelegt. Sie müssen daher die Kodierung des vom Server gesendeten Dokuments kennen. Wenn Sie einen Fehler machen, schlägt die Dekodierung des Binärstroms fehl. Die Kodierung ist in der URL-Konfigurationsdatei festgelegt (zum Beispiel in Zeile 12). Wir hätten diese Informationen dynamisch verarbeiten können, da der Server sie in seinen HTTP-Headern sendet. Das wäre vorzuziehen gewesen. Um den Code einfach zu halten, haben wir dies jedoch nicht getan. Um den Kodierungstyp des Dokuments zu ermitteln, rufen Sie einfach die gewünschte URL mit einem Browser auf und überprüfen Sie die vom Browser im Debug-Modus (F12) gesendeten HTTP-Header oder das Dokument selbst, da auch dort die Kodierung angegeben ist:

Image

Image

  • Zeilen 61–66: Die zugewiesenen Ressourcen werden freigegeben;

Beim Ausführen des Skripts [main.py] wird die folgende Konsolenausgabe angezeigt:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/inet/http/02/main.py
-------------------------
sergetahe.com
-------------------------
Client : début de la communication avec le serveur [sergetahe.com]
*   Trying 87.98.154.146:80...
* TCP_NODELAY set
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET / HTTP/1.1
Host: sergetahe.com
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*
 
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Date: Mon, 06 Jul 2020 06:45:52 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Server: Apache
< X-Powered-By: PHP/7.3
< Location: http://sergetahe.com/cours-tutoriels-de-programmation
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 to host sergetahe.com left intact
* Issue another request to this URL: 'http://sergetahe.com/cours-tutoriels-de-programmation'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) with host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation HTTP/1.1
Host: sergetahe.com
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*
 
* Mark bundle as not supporting multiuse
< HTTP/1.1 301 Moved Permanently
< Date: Mon, 06 Jul 2020 06:45:52 GMT
< Content-Type: text/html; charset=iso-8859-1
< Content-Length: 262
< Server: Apache
< Location: http://sergetahe.com/cours-tutoriels-de-programmation/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 to host sergetahe.com left intact
* Issue another request to this URL: 'http://sergetahe.com/cours-tutoriels-de-programmation/'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) with host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation/ HTTP/1.1
Host: sergetahe.com
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*
 
* Mark bundle as not supporting multiuse
< HTTP/1.1 301 Moved Permanently
< Date: Mon, 06 Jul 2020 06:45:52 GMT
< Content-Type: text/html; charset=iso-8859-1
< Content-Length: 263
< Server: Apache
< Location: https://sergetahe.com/cours-tutoriels-de-programmation/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 to host sergetahe.com left intact
* Issue another request to this URL: 'https://sergetahe.com/cours-tutoriels-de-programmation/'
*   Trying 87.98.154.146:443...
* TCP_NODELAY set
* ….
* Using Stream ID: 1 (easy handle 0x25eaec77010)
> GET /cours-tutoriels-de-programmation/ HTTP/2
Host: sergetahe.com
user-agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
accept: */*
 
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
< HTTP/2 200 
< date: Mon, 06 Jul 2020 06:45:53 GMT
< content-type: text/html; charset=UTF-8
< server: Apache
< x-powered-by: PHP/7.3
< link: <https://sergetahe.com/cours-tutoriels-de-programmation/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/cours-tutoriels-de-programmation/>; rel=shortlink
< vary: Accept-Encoding
< x-iplb-instance: 17080
< set-cookie: SERVERID68971=26218|XwLIp|XwLIp; path=/
< 
* Connection #1 to host sergetahe.com left intact
-------------------------
tahe.developpez.com
-------------------------
Client : début de la communication avec le serveur [tahe.developpez.com]
*   Trying 87.98.130.52:443...
* TCP_NODELAY set
* Connected to tahe.developpez.com (87.98.130.52) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: CN=*.developpez.com
*  start date: Jul  1 15:38:30 2020 GMT
*  expire date: Sep 29 15:38:30 2020 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 result: unable to get local issuer certificate (20), continuing anyway.
> GET / HTTP/1.1
Host: tahe.developpez.com
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*
 
* old SSL session ID is stale, removing
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Mon, 06 Jul 2020 06:45:53 GMT
< Server: Apache/2.4.38 (Debian)
< X-Powered-By: PHP/5.3.29
< Vary: Accept-Encoding
< Transfer-Encoding: chunked
< Content-Type: text/html
< 
* Connection #0 to host tahe.developpez.com left intact
-------------------------
www.polytech-angers.fr
-------------------------
Client : début de la communication avec le serveur [www.polytech-angers.fr]
*   Trying 193.49.144.41:80...
* 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
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*
 
* Mark bundle as not supporting multiuse
< HTTP/1.1 301 Moved Permanently
< Date: Mon, 06 Jul 2020 06:45:54 GMT
< Server: Apache/2.4.29 (Ubuntu)
< Location: http://www.polytech-angers.fr/fr/index.html
< Cache-Control: max-age=1
< Expires: Mon, 06 Jul 2020 06:45:55 GMT
< Content-Length: 339
< Content-Type: text/html; charset=iso-8859-1
< 
* Ignoring the response-body
* Connection #0 to host www.polytech-angers.fr left intact
* Issue another request to this URL: 'http://www.polytech-angers.fr/fr/index.html'
* Found bundle for host www.polytech-angers.fr: 0x25eacafb490 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) with host www.polytech-angers.fr
* Connected to www.polytech-angers.fr (193.49.144.41) port 80 (#0)
> GET /fr/index.html HTTP/1.1
Host: www.polytech-angers.fr
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*
 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Mon, 06 Jul 2020 06:45:54 GMT
< Server: Apache/2.4.29 (Ubuntu)
< Last-Modified: Mon, 06 Jul 2020 04:50:09 GMT
< ETag: "85be-5a9be9bfcf228"
< Accept-Ranges: bytes
< Content-Length: 34238
< Cache-Control: max-age=1
< Expires: Mon, 06 Jul 2020 06:45:55 GMT
< Vary: Accept-Encoding
< Content-Type: text/html; charset=UTF-8
< Content-Language: fr
< 
* Connection #0 to host www.polytech-angers.fr left intact
-------------------------
localhost
-------------------------
Client : début de la communication avec le serveur [localhost]
*   Trying ::1:80...
* TCP_NODELAY set
* Connected to localhost (::1) port 80 (#0)
> GET / HTTP/1.1
Host: localhost
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*
 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Mon, 06 Jul 2020 06:45:54 GMT
< Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19
< X-Powered-By: PHP/7.2.19
< Content-Length: 1776
< Content-Type: text/html; charset=UTF-8
< 
* Connection #0 to host localhost left intact
Terminé...
 
Process finished with exit code 0

Kommentare

  • in Blau die an den Server gesendeten HTTP-Befehle;
  • in Grün die vom Client als Antwort empfangenen Daten;
  • wir erhalten denselben Datenaustausch wie mit dem [curl]-Tool;
    • Zeile 9: Die URL [http://sergetahe.com/] wird angefordert;
    • Zeile 15: Der Server antwortet, dass die Seite verschoben wurde. Zeile 21: die neue URL;
    • Zeile 32: Die URL [http://sergetahe.com/cours-tutoriels-de-programmation] wird angefordert;
    • Zeile 38: Der Server antwortet, dass die Seite verschoben wurde. Zeile 43, die neue URL;
    • Zeile 54: Die URL [http://sergetahe.com/cours-tutoriels-de-programmation/] wird angefordert;
    • Zeile 60: Der Server antwortet, dass die Seite verschoben wurde. Zeile 65: die neue URL. Es wird das sichere Protokoll [HTTPS] verwendet;
    • Zeilen 71–75: Das sichere Protokoll wird mit dem Server hergestellt;
    • Zeile 76: Die URL [https://sergetahe.com/cours-tutoriels-de-programmation/] wird angefordert;
    • Zeile 82: Das angeforderte Dokument wurde gefunden;

21.4.6. Fazit

In diesem Abschnitt haben wir das HTTP-Protokoll untersucht und ein Skript [http/02/main.py] geschrieben, das eine URL aus dem Internet herunterladen kann.

21.5. Das SMTP (Simple Mail Transfer Protocol)

21.5.1. Einführung

In diesem Kapitel:

  • [Server B] ist ein lokaler SMTP-Server, den wir installieren werden;
  • [Client A] wird ein SMTP-Client in verschiedenen Formen sein:
    • den [RawTcpClient]-Client zur Untersuchung des SMTP-Protokolls;
    • ein Python-Skript, das das SMTP-Protokoll des [RawTcpClient]-Clients emuliert;
    • ein Python-Skript, das das [smtplib]-Modul verwendet, um alle Arten von E-Mails zu versenden;

21.5.2. Erstellen einer [Gmail]-Adresse

Um unsere SMTP-Tests durchzuführen, benötigen wir eine E-Mail-Adresse, an die wir senden können. Dazu erstellen wir eine Gmail-Adresse [https://www.google.com/intl/fr/gmail/about/]:

Image

Hinweis: Senden Sie einige E-Mails an die von Ihnen erstellte Adresse. Fahren Sie erst fort, wenn Sie sicher sind, dass das von Ihnen erstellte Konto E-Mails empfangen kann.

21.5.3. Installation eines SMTP-Servers

Für unsere Tests installieren wir den Mailserver [hMailServer], der sowohl als SMTP-Server zum Versenden von E-Mails, als POP3-Server (Post Office Protocol) zum Abrufen von auf dem Server gespeicherten E-Mails als auch als IMAP-Server (Internet Message Access Protocol) dient, mit dem Sie ebenfalls auf dem Server gespeicherte E-Mails abrufen können, der aber noch weitere Funktionen bietet. Insbesondere ermöglicht er Ihnen die Verwaltung des E-Mail-Speichers auf dem Server.

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

Image

Während der Installation werden Sie nach bestimmten Informationen gefragt:

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 Administratorkennwort gefragt: Notieren Sie es sich, da Sie es benötigen werden;

[hMailServer] wird als Windows-Dienst installiert, der beim Hochfahren des Computers automatisch gestartet wird. Es empfiehlt sich, einen manuellen Start zu wählen:

  • Geben Sie in [3] in das Suchfeld der Taskleiste [Dienste] ein;

Image

  • Stellen Sie in [4-8] den Dienst auf den Modus [Manuell] (6) ein und starten Sie ihn anschließend (7);

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

Image

  • Geben Sie in [2] in das Suchfeld der Statusleiste [hmailserver] ein;
  • Starten Sie in [3] den Administrator;
  • Verbinden Sie in [4] den Administrator mit dem [hMailServer]-Server;
  • Geben Sie in [5] das Passwort ein, das Sie bei der Installation von [hMailServer] festgelegt haben;

Falls Sie das Passwort vergessen haben, gehen Sie wie folgt vor:

  • Stoppen Sie den [hMailServer]-Server;
  • Öffnen Sie die Datei [<hmailserver>/bin/hmailserver.ini], wobei <hmailserver> das Installationsverzeichnis des Servers ist: Image
  • Entfernen Sie unter [100] das Passwort aus der Zeile [AdministratorPassword]. Dadurch hat der Administrator kein Passwort mehr. Drücken Sie einfach die [Enter]-Taste, wenn Sie dazu aufgefordert werden;

ValidLanguages=english,swedish
[Security]
AdministratorPassword=
[Database]

Fahren wir mit der Konfiguration des Servers fort:

Image

  • Fügen Sie unter [1-2] eine Domäne hinzu (falls noch keine vorhanden ist);

Image

  • in [3] können Sie für die Tests, die wir durchführen werden, so gut wie alles eingeben. In der Praxis müssten Sie den Namen einer bestehenden Domain eingeben;

Image

Wir werden nun ein Benutzerkonto erstellen:

  • Klicken Sie mit der rechten Maustaste auf [Konten] (7) und dann auf (8), um einen neuen Benutzer hinzuzufügen;
  • Auf der Registerkarte [Allgemein] (9) definieren wir einen Benutzer namens [guest] (10) mit dem Passwort [guest] (11). Seine E-Mail-Adresse lautet [guest@localhost] (10);
  • In [12] ist der Benutzer [guest] aktiviert;

Image

  • In [13-14] wird der Benutzer angelegt; Image
  • In [27] der SMTP-Dienstport;
  • In [28] erfordert dieser Dienst keine Authentifizierung;
  • in [30] geben Sie die Begrüßungsnachricht ein, die der SMTP-Server an seine Clients sendet;

Image

Das Gleiche machen wir mit dem POP3-Server:

Image

Das Gleiche tun wir für den IMAP-Server:

Image

Wir geben die Standarddomäne des [hMailServer]-Servers an (es kann mehrere geben) :

Image

  • Geben Sie in [37] an, dass die Standard-SMTP-Serverdomäne diejenige ist, die Sie in [38] erstellt haben;

Nachdem Sie diese Konfiguration gespeichert haben, können Sie sie wie folgt testen. Öffnen Sie ein PyCharm-Terminal im Ordner „utilities“:

Image

Geben Sie dann den folgenden Befehl ein:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 25
Client [DESKTOP-30FF5FB:50170] connecté au serveur [localhost-25]
Tapez vos commandes (quit pour arrêter) :
<-- [220 Bienvenue sur le serveur SMTP localhost.com]
  • Zeile 1: Wir stellen eine Verbindung zu Port 25 auf dem Rechner [localhost] her. Dort läuft ein ungesicherter SMTP-Server des [hMailServer]-Servers;
  • Zeile 4: Wir erhalten die Begrüßungsnachricht, die wir oben in Schritt 30 konfiguriert haben;

Der SMTP-Server ist nun betriebsbereit. Geben Sie den Befehl [quit] ein, um die Sitzung mit dem SMTP-Server 25 zu beenden.

Nun machen wir dasselbe mit Port 587, dem Standardport für den sicheren SMTP-Mail-Relay-Dienst:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 587
Client [DESKTOP-30FF5FB:50217] connecté au serveur [localhost-587]
Tapez vos commandes (quit pour arrêter) :
<-- [220 Bienvenue sur le serveur SMTP localhost.com]
  • Zeile 4, die Antwort vom SMTP-Server, der auf Port 587 läuft;

Machen wir nun dasselbe mit Port 110, dem Standardport für den POP3-E-Mail-Abrufdienst:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 110
Client [DESKTOP-30FF5FB:50210] connecté au serveur [localhost-110]
Tapez vos commandes (quit pour arrêter) :
<-- [+OK Bienvenue sur le serveur POP3 localhost.com]
  • Zeile 4: Wir haben die Willkommensnachricht vom POP3-Server erhalten;

Nun machen wir dasselbe mit Port 143, dem Standardport für den IMAP-E-Mail-Abrufdienst:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 143
Client [DESKTOP-30FF5FB:50212] connecté au serveur [localhost-143]
Tapez vos commandes (quit pour arrêter) :
<-- [* OK Bienvenue sur le serveur IMAP localhost.com]
  • Zeile 4: Wir haben die Willkommensnachricht vom IMAP-Server erhalten;

21.5.4. Installation eines E-Mail-Clients

Um die E-Mail zu lesen, die wir versenden werden, benötigen wir einen E-Mail-Client. Für diejenigen, die noch keinen haben, zeigen wir Ihnen, wie Sie [Thunderbird] installieren und konfigurieren:

  • Schritt [1]: Laden Sie [Thunderbird] herunter und installieren Sie es;

Image

  • Starten Sie den [hMailServer]-Mailserver, falls er noch nicht läuft;
  • in [2-3]: Sobald Thunderbird läuft, erstellen wir ein E-Mail-Konto für den Benutzer [guest@localhost] auf dem [hMailServer]-Mailserver;

Image

Image

Image

  • in [7-11]: Der POP3-Server, über den wir E-Mails vom [hMailServer] abrufen können, befindet sich unter [localhost] und läuft auf Port 110;
  • in [12-16]: Der SMTP-Server, über den wir E-Mails im Namen der Benutzer des [hMailServer]-Mail-Servers versenden können, befindet sich auf [localhost] und läuft auf Port 25;
  • [18]: Sie können testen, ob diese Konfiguration gültig ist;

Image

Image

  • in [26]: Da keine SSL-Verschlüsselung vorhanden ist, warnt uns Thunderbird, dass unsere Konfiguration Risiken birgt;
  • in [28]: Das Konto wurde erstellt;

Um das erstellte Konto zu testen, verwenden wir Thunderbird, um:

  • eine E-Mail an den Benutzer [guest@localhost.com] zu senden (SMTP-Protokoll);
  • die von diesem Benutzer empfangene E-Mail abrufen (POP3-Protokoll); Image
  • in [3]: der Absender;
  • in [4]: der Empfänger;
  • in [5]: der Betreff der E-Mail;
  • in [6]: den Inhalt der E-Mail;
  • in [7]: zum Versenden der E-Mail;

Image

  • in [8-9]: Abrufen der E-Mail-Adresse des Benutzers [guest@localhost];
  • in [10-15]: die empfangene Nachricht;

Wir senden außerdem eine E-Mail an den Benutzer [pymailparlexemple@gmail.com]. Erstellen wir ein Konto für ihn in Thunderbird, damit er die empfangene E-Mail lesen kann:

Image

Image

  • in [4]: Geben Sie einen beliebigen Namen ein;
  • in [5]: Die Adresse lautet [pymailparlexemple@gmail.com];
  • in [6]: Geben Sie das Passwort ein, das Sie diesem Benutzer bei der Erstellung des Kontos zugewiesen haben;
  • in [7]: Bestätigen Sie diese Konfiguration;

Image

  • in [8]: Thunderbird hat die folgenden Informationen aus seiner Datenbank abgerufen;
  • in [9]: Das E-Mail-Abrufprotokoll ist nicht mehr POP3, sondern IMAP. Der Hauptunterschied zwischen den beiden besteht darin, dass [POP3] gelesene E-Mails auf den lokalen Rechner herunterlädt, auf dem sich der E-Mail-Client befindet, und sie vom Remote-Server löscht, während [IMAP] die E-Mails auf dem Remote-Server belässt;
  • in [10]: SMTP-Server-Identifikation;
  • in [13]: Um weitere Informationen zu den IMAP- und SMTP-Servern zu erhalten, wechseln Sie zur manuellen Konfiguration;

Image

  • in [14-17]: IMAP-Server-Einstellungen;
  • in [18-21]: SMTP-Server-Einstellungen;
  • in [22]: Schließen Sie die Konfiguration ab;

Image

  • in [23-24]: das neue Thunderbird-Konto;
  • in [26]: eine neue Nachricht verfassen;

Image

  • in [27]: Der Absender ist [pymailparlexemple@gmail.com];
  • in [28]: Der Empfänger ist [pymailparlexemple@gmail.com];
  • in [29-30]: die Nachricht;
  • in [31]: zum Versenden;

Image

  • in [32]: Wir überprüfen die E-Mails der verschiedenen Konten; Image
  • in [33-36]: die vom Benutzer [pymailparlexemple@gmail.com] empfangene E-Mail

Außerdem erstellen wir:

  • ein neues Gmail-Konto [pymail2parlexemple@gmail.com];
  • ein neues Thunderbird-Konto [pymail2parlexemple@gmail.com], um Nachrichten für den gleichnamigen Benutzer abzurufen:

Image

Image

Wir verfügen nun über die Werkzeuge, um die Protokolle SMTP, POP3 und IMAP zu erkunden. Wir beginnen mit dem SMTP-Protokoll.

21.5.5. Das SMTP-Protokoll

Image

Wir werden das SMTP-Protokoll untersuchen, indem wir die Protokolle des [hMailServer]-Servers prüfen. Dazu aktivieren wir sie mit dem Tool [hMailServerAdministrator]:

Image

Image

  • In [2] sind die Protokolle aktiviert;
  • in [3-5]: Wir aktivieren sie für die Protokolle SMTP, POP3 und IMAP;
  • in [7] fordern wir die Anzeige der Protokolle an;
  • in [8] öffnen wir die Protokolldatei mit einem beliebigen Texteditor;

Image

Im folgenden Beispiel ist der Client [Thunderbird] und der Server [hMailServer]. Lassen Sie den Benutzer [guest@localhost.com] mit Thunderbird eine Nachricht an sich selbst senden:

Image

Die Protokolle sehen dann wie folgt aus:


"SMTPD"    5828    22    "2020-07-07 10:02:54.263"    "127.0.0.1"    "SENT: 220 Bienvenue sur le serveur SMTP localhost.com"
"SMTPD"    21956    22    "2020-07-07 10:02:54.360"    "127.0.0.1"    "RECEIVED: EHLO [127.0.0.1]"
"SMTPD"    21956    22    "2020-07-07 10:02:54.362"    "127.0.0.1"    "SENT: 250-DESKTOP-30FF5FB[nl]250-SIZE 20480000[nl]250-AUTH LOGIN[nl]250 HELP"
"SMTPD"    5828    22    "2020-07-07 10:02:54.381"    "127.0.0.1"    "RECEIVED: MAIL FROM:<guest@localhost.com> SIZE=433"
"SMTPD"    5828    22    "2020-07-07 10:02:54.386"    "127.0.0.1"    "SENT: 250 OK"
"SMTPD"    21956    22    "2020-07-07 10:02:54.470"    "127.0.0.1"    "RECEIVED: RCPT TO:<guest@localhost.com>"
"SMTPD"    21956    22    "2020-07-07 10:02:54.473"    "127.0.0.1"    "SENT: 250 OK"
"SMTPD"    21956    22    "2020-07-07 10:02:54.478"    "127.0.0.1"    "RECEIVED: DATA"
"SMTPD"    21956    22    "2020-07-07 10:02:54.479"    "127.0.0.1"    "SENT: 354 OK, send."
"SMTPD"    21860    22    "2020-07-07 10:02:54.496"    "127.0.0.1"    "SENT: 250 Queued (0.016 seconds)"
"SMTPD"    21568    22    "2020-07-07 10:02:54.505"    "127.0.0.1"    "RECEIVED: QUIT"
"SMTPD"    21568    22    "2020-07-07 10:02:54.506"    "127.0.0.1"    "SENT: 221 goodbye"

Die obigen Zeilen beschreiben den Dialog, der zwischen dem SMTP-Client (dem E-Mail-Client Thunderbird) und dem SMTP-Server (hMailServer) stattfand. Die [SENT]-Zeilen geben an, was der SMTP-Server an seinen Client gesendet hat. Die [RECEIVED]-Zeilen geben an, was der SMTP-Server von seinem Client empfangen hat.

  • Zeile 1: Unmittelbar nachdem der Client eine Verbindung zum SMTP-Server hergestellt hat, sendet der Server eine Willkommensnachricht an den Client;
  • Zeile 2: Der Client sendet den Befehl [EHLO], um sich zu identifizieren. Dabei gibt er seine IP-Adresse [127.0.0.1] an, die sich auf den Rechner [localhost] bezieht, d. h. den Rechner, auf dem der SMTP-Client läuft;
  • Zeile 3: Der Server sendet eine Reihe von [250]-Antworten. [nl] steht für [newline], d. h. das Zeichen \n. Die Antworten haben die Form [250-], mit Ausnahme der letzten, die die Form [250 ] hat. So erkennt der SMTP-Client, dass die Antwort des SMTP-Servers vollständig ist und er einen Befehl senden kann. Die Reihe von [250]-Befehlen sollte dem SMTP-Client eine Reihe von Befehlen anzeigen, die er verwenden konnte;
  • Zeile 4: Der SMTP-Client sendet den Befehl [MAIL FROM: sender_email_address], der angibt, wer die Nachricht sendet;
  • Zeile 5: Der SMTP-Server antwortet mit [250 OK] und zeigt damit an, dass er den Befehl verstanden hat;
  • Zeile 6: Der SMTP-Client sendet den Befehl [RCPT TO: recipient_email_address], um die Adresse des Empfängers anzugeben;
  • Zeile 7: Der SMTP-Server bestätigt erneut, dass er den Befehl verstanden hat;
  • Zeile 8: Der SMTP-Server sendet den Befehl [DATA]. Dies bedeutet, dass er den Inhalt der Nachricht senden wird;
  • Zeile 9: Der SMTP-Server zeigt mit der Antwort [354 OK] an, dass er bereit ist, die Nachricht zu empfangen. Der Text [send .] bedeutet, dass der SMTP-Client seine Nachricht mit einer Zeile beenden muss, die nur einen einzigen Punkt enthält;
  • Was wir als Nächstes nicht sehen, ist, dass der SMTP-Client seine Nachricht sendet. Die Protokolle zeigen dies nicht an;
  • Zeile 10: Der SMTP-Client hat den Punkt gesendet, der das Ende der Nachricht anzeigt. Der SMTP-Server antwortet, dass er die Nachricht in die Warteschlange gestellt hat;
  • Der SMTP-Client sendet den Befehl [QUIT], um anzuzeigen, dass er die Verbindung schließt;
  • Zeile 12: Der Server antwortet;

Nachdem wir nun den Client-Server-Dialog des SMTP-Protokolls verstanden haben, versuchen wir, ihn mit unserem [RawTcpClient] nachzubilden. Wir verwenden dazu ein PyCharm-Terminal:

Image

Sehen wir uns ein neues Beispiel an:

Image

  • Client A ist der generische TCP-Client [RawTcpClient];
  • Server B ist der Mailserver [hMailServer];
  • Client A wird Server B bitten, eine vom Benutzer [guest@localhost.com] gesendete E-Mail an sich selbst zuzustellen;
  • wir werden überprüfen, ob der Empfänger die gesendete E-Mail tatsächlich erhalten hat;

Wir starten den Client wie folgt:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 25 --quit bye
Client [DESKTOP-30FF5FB:53122] connecté au serveur [localhost-25]
Tapez vos commandes (quit pour arrêter) :
<-- [220 Bienvenue sur le serveur SMTP localhost.com]
  • Zeile [1]: Wir stellen eine Verbindung zu Port 25 auf dem lokalen Rechner her, auf dem der SMTP-Dienst [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]. Allerdings ist [quit] auch ein Befehl des SMTP-Protokolls. Wir müssen daher diese Mehrdeutigkeit vermeiden;
  • Zeile [2]: Der Client ist erfolgreich verbunden;
  • Zeile [3]: Der Client wartet auf Befehle, die über die Tastatur eingegeben werden;
  • Zeile [4]: Der Server sendet dem Client seine Begrüßungsnachricht;

Wir setzen den Dialog wie folgt fort:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 25
Client [DESKTOP-30FF5FB:53155] connecté au serveur [localhost-25]
Tapez vos commandes (quit pour arrêter) :
<-- [220 Bienvenue sur le serveur SMTP localhost.com]
EHLO localhost
<-- [250-DESKTOP-30FF5FB]
<-- [250-SIZE 20480000]
<-- [250-AUTH LOGIN]
<-- [250 HELP]
MAIL FROM: guest@localhost.com
<-- [250 OK]
RCPT TO: guest@localhost.com
<-- [250 OK]
DATA
<-- [354 OK, send.]
from: guest@localhost.com
to: guest@localhost.com
subject: ceci est un test
 
ligne1
ligne2
.
<-- [250 Queued (37.824 seconds)]
QUIT
Fin de la connexion avec le serveur
  • In [5] sendet der Client den Befehl [EHLO client-machine-name]. Der Server antwortet mit einer Reihe von Nachrichten in der Form [250-xx] (6). Der Code [250] zeigt an, dass der vom Client gesendete Befehl erfolgreich war;
  • In [10] gibt der Client den Absender der Nachricht an, in diesem Fall [guest@localhost.com];
  • in [11] die Antwort des Servers;
  • in [12] wird der Empfänger der Nachricht angegeben, in diesem Fall der Benutzer [guest@localhost.com];
  • in [13] die Antwort des Servers;
  • in [14] teilt der Befehl [DATA] dem Server mit, dass der Client im Begriff ist, den Inhalt der Nachricht zu senden;
  • in [15] die Antwort des Servers;
  • in [16-22] muss der Client eine Liste von Textzeilen senden, die mit einer Zeile endet, die nur einen einzigen Punkt enthält. Die Nachricht kann [Subject:, From:, To:]-Zeilen (16-18) enthalten, um den Betreff, den Absender bzw. den Empfänger der Nachricht anzugeben;
  • in [19] muss auf die vorangehenden Kopfzeilen eine Leerzeile folgen;
  • in [20–21] der Nachrichtentext;
  • in [22] die Zeile, die nur einen einzigen Punkt enthält und das Ende der Nachricht anzeigt;
  • in [23] stellt der Server die Nachricht in die Warteschlange, sobald er die Zeile mit dem einzelnen Punkt empfangen hat;
  • in [24] teilt der Client dem Server mit, dass er fertig ist;
  • in [25] sehen wir, dass der Server die Verbindung zum Client geschlossen hat;

Überprüfen wir nun in Thunderbird, ob der Benutzer [guest@localhost.com] die Nachricht tatsächlich erhalten hat:

Image

  • In [1-6] sehen wir, dass der Benutzer [guest@localhost.com] die Nachricht tatsächlich erhalten hat;

Schließlich hat unser Client [RawTcpClient] erfolgreich eine Nachricht über den SMTP-Server [localhost] gesendet. Nun wollen wir dieselbe Methode verwenden, um eine Nachricht an [pymailparlexemple@gmail.com] zu senden:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe smtp.gmail.com 587
Client [DESKTOP-30FF5FB:53210] connecté au serveur [smtp.gmail.com-587]
Tapez vos commandes (quit pour arrêter) :
<-- [220 smtp.gmail.com ESMTP w13sm643278wrr.67 - gsmtp]
EHLO localhost
<-- [250-smtp.gmail.com at your service, [2a01:cb05:80e8:b500:3c4b:2203:91fa:9b00]]
<-- [250-SIZE 35882577]
<-- [250-8BITMIME]
<-- [250-STARTTLS]
<-- [250-ENHANCEDSTATUSCODES]
<-- [250-PIPELINING]
<-- [250-CHUNKING]
<-- [250 SMTPUTF8]
MAIL FROM: pymailparlexemple@gmail.com
<-- [530 5.7.0 Must issue a STARTTLS command first. w13sm643278wrr.67 - gsmtp]
QUIT
Fin de la connexion avec le serveur
  • Zeile 1: Wir verwenden den SMTP-Server von Gmail, der auf Port 587 läuft;
  • Zeile 15: Wir werden blockiert, weil der SMTP-Server von uns verlangt, eine sichere Verbindung herzustellen, was wir nicht können. Im Gegensatz zum vorherigen Beispiel erfordert der Server [smtp.gmail.com] (Zeile 1) eine Authentifizierung über . Er akzeptiert nur Clients, die in der Domain [gmail.com] registriert sind. Diese Authentifizierung ist sicher und erfolgt über eine verschlüsselte Verbindung.

Das erste Beispiel vermittelte uns die Grundlagen für die Erstellung eines einfachen SMTP-Clients in Python. Das zweite zeigte uns, dass einige SMTP-Server (tatsächlich die meisten) eine Authentifizierung über eine verschlüsselte Verbindung erfordern.

21.5.6. Skripte [smtp/01]: ein einfacher SMTP-Client

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

Image

Die Datei [smtp/01/config] konfiguriert die Anwendung wie folgt:

def configure() -> dict:
    return {
        #  description: description of the e-mail sent
        #  smtp-server: SMTP server
        #  smtp-port: server port SMTP
        # from : expéditeur
        #  to: recipient
        #  subject : mail subject
        #  message : mail message
        "mails": [
            {
                "description": "mail to localhost via localhost",
                "smtp-server": "localhost",
                "smtp-port": "25",
                "from": "guest@localhost.com",
                "to": "guest@localhost.com",
                "subject": "to localhost via localhost",
                #  we send UTF-8
                "content-type": 'text/plain; charset="utf-8"',
                #  we test accented characters
                "message": "aglaë séléné\nva au marché\nacheter des fleurs"
            },
            {
                "description": "mail to gmail via gmail",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymailparlexemple@gmail.com",
                "to": "pymailparlexemple@gmail.com",
                "subject": "to gmail via gmail",
                #  we send UTF-8
                "Content-type": 'text/plain; charset="utf-8"',
                #  we test accented characters
                "message": "aglaë séléné\nva au marché\nacheter des fleurs"
            }
        ]
    }
  • Zeilen 10–35: eine Liste der zu versendenden E-Mails. Für jede E-Mail werden die folgenden Informationen angegeben:
    • [description]: ein Text, der die E-Mail beschreibt;
    • [smtp-server]: der zu verwendende SMTP-Server;
    • [smtp-port]: dessen Dienstport;
    • [von]: der Absender der E-Mail;
    • [an]: der E-Mail-Empfänger;
    • [Betreff]: der Betreff der E-Mail;
    • [content-type]: die E-Mail-Kodierung;
    • [Nachricht]: die E-Mail-Nachricht;

Der [01/main]-Code für den SMTP-Client lautet wie folgt:

#  imports
import socket


# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: bool):
    #  sends message to smtp server smtpserver from sender
    #  as recipient. If verbose=True, tracks client-server exchanges

    #  let system errors show up
    connexion = None
    try:
        #  local machine name (required for SMTP protocol)
        client = socket.gethostbyaddr(socket.gethostbyname("localhost"))[0]
        #  open a connection on port 25 of smtpServer
        connexion = socket.create_connection((mail["smtp-server"], 25))

        #  connection represents a bidirectional communication flow
        #  between the client (this program) and the smtp server contacted
        #  this channel is used for the exchange of orders and information

        #  after connection, the server sends a welcome message which is read as follows
        send_command(connexion, "", verbose, True)
        #  cmde ehlo:
        send_command(connexion, f"EHLO {client}", verbose, True)
        # cmde mail from:
        send_command(connexion, f"MAIL FROM: <{mail['from']}>", verbose, True)
        #  cmde rcpt to:
        send_command(connexion, f"RCPT TO: <{mail['to']}>", verbose, True)
        #  cmde data
        send_command(connexion, "DATA", verbose, True)
        #  prepare message to send
        #  it must contain the lines
        # From: expéditeur
        #  To: recipient
        #  blank line
        #  Message
        # .
        data = f"{mail['message']}"
        #  send message
        send_command(connexion, data, verbose, False)
        #  shipping .
        send_command(connexion, "\r\n.\r\n", verbose, False)
        #  cmde quit
        send_command(connexion, "QUIT", verbose, True)
        #  end
    finally:
        #  locking connection
        if connexion:
            connexion.close()


# --------------------------------------------------------------------------
def send_command(connexion: socket, commande: str, verbose: bool, with_rclf: bool):
    #  sends command to connection channel
    #  verbose mode if verbose=True
    #  if with_rclf=True, adds rclf sequence to command

    #  data
    rclf = "\r\n" if with_rclf else ""
    #  send cmde if order not empty
    if commande:
        #  let system errors show up
        #
        #  order dispatch
        connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
        #  possible echo
        if verbose:
            affiche(commande, 1)
        #  read response of less than 1000 characters
        reponse = str(connexion.recv(1000), 'utf-8')
        #  possible echo
        if verbose:
            affiche(reponse, 2)
        #  error code recovery
        codeErreur = int(reponse[0:3])
        #  error returned by the server?
        if codeErreur >= 500:
            #  throw an exception with the error
            raise BaseException(reponse[4:])
        #  error-free return


# --------------------------------------------------------------------------
def affiche(echange: str, sens: int):
    #  displays exchange ? screen
    #  if sens=1 displays -->change
    #  if sens=2 displays <-- exchange without last 2 characters rclf
    if sens == 1:
        print(f"--> [{echange}]")
        return
    elif sens == 2:
        l = len(echange)
        print(f"<-- [{echange[0:l - 2]}]")
        return


#  main ----------------------------------------------------------------

#  client SMTP (SendMail Transfer Protocol) for sending a message
#  information is taken from a config file containing the following information for each server

#  description: description of the e-mail sent
#  smtp-server: SMTP server
#  smtp-port: server port SMTP
# from : expéditeur
#  to: recipient
#  subject : mail subject
#  message : mail message


#  communication protocol SMTP client-server
#  -> client connects to smtp server port 25
#  <- server sends him a welcome message
#  -> customer sends command EHLO: machine name
#  <- server responds OK or not
#  -> customer sends mail from: <exp?diteur> command
#  <- server responds OK or not
#  -> client sends the rcpt to command: <recipient>
#  <- server responds OK or not
#  -> customer sends data order
#  <- server responds OK or not
#  -> client sends all the lines of its message and ends with a line containing the single character .
#  <- server responds OK or not
#  -> customer sends quit order
#  <- server responds OK or not

#  server responses have the form xxx text where xxx is a 3-digit number. Any number xxx >=500
#  indicates an error. The answer may consist of several lines all beginning with xxx- except for the last line
#  of the form xxx(space)

#  exchanged text lines must end with RC(#13) and LF(#10) characters

#  application configuration
import config
config = config.configure()

#  we deal with e-mails one by one
for mail in config['mails']:
    try:
        #  logs
        print("----------------------------------")
        print(f"Envoi du message [{mail['description']}]")
        #  preparing the message to be sent
        mail[
            "message"] = f"From: {mail['from']}\nTo: {mail['to']}\n" \
                         f"Subject: {mail['subject']}\n" \
                         f"Content-type: {mail['content-type']}" \
                         f"\n\n{mail['message']}"
        #  send message in verbose mode
        sendmail(mail, True)
        #  end
        print("Message envoyé...")
    except BaseException as erreur:
        #  error is displayed
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
    #  next mail

Kommentare

  • Zeilen 134–136: Konfigurieren der Anwendung;
  • Zeilen 139–151: Wir verarbeiten alle in der Konfiguration gefundenen E-Mails;
  • Zeilen 141–143: Anzeige der nächsten Schritte;
  • Zeilen 144–149: Definition der zu versendenden Nachricht. Der Nachricht [message] gehen die Kopfzeilen [From, To, Subject, Content-type] voraus;
  • Zeile 151: Die E-Mail wird mit der Funktion [sendmail] versendet, die zwei Parameter benötigt:
    • [mail]: das Wörterbuch, das die zum Versenden der E-Mail erforderlichen Informationen enthält;
    • [verbose]: Ein boolescher Wert, der angibt, ob der Austausch zwischen Client und Server in der Konsole protokolliert werden soll;
  • Zeilen 154–156: Alle von der Funktion [sendmail] ausgelösten Ausnahmen werden abgefangen. Sie werden angezeigt;
  • Zeile 6: [mail] ist das Wörterbuch, das die zu versendende E-Mail beschreibt;
  • Zeile 14: Im SMTP-Protokoll muss der Client seinen Namen senden. Hier rufen wir den Namen des lokalen Rechners ab, der als Client fungiert;
  • Zeile 16: stellt eine Verbindung zu dem SMTP-Server her, an den die Nachricht gesendet wird;
  • Zeilen 22–23: Wenn die Verbindung zum SMTP-Server erfolgreich war, sendet der Server eine Willkommensnachricht, die hier gelesen wird;
  • Die Funktion [sendmail] sendet dann die verschiedenen Befehle, die ein SMTP-Client senden muss:
    • Zeilen 24–25: den Befehl EHLO;
    • Zeilen 26–27: der Befehl MAIL FROM:;
    • Zeilen 28–29: der Befehl RCPT TO:;
    • Zeilen 30–31: der Befehl DATA;
    • Zeilen 32–41: Senden der Nachricht (From, To, Subject, Content-type, Text);
    • Zeilen 42–43: Senden des Zeichen für das Ende der Nachricht;
    • Zeilen 44–457: der Befehl QUIT, der den Dialog des Clients mit dem SMTP-Server beendet;
  • Die Ausführung von [sendmail] läuft innerhalb eines [try / finally]-Blocks, der es ermöglicht, alle Ausnahmen an den aufrufenden Code weiterzugeben. Wir wissen, dass der aufrufende Code alle davon abfängt, um sie anzuzeigen;
  • Zeilen 48–50: Freigabe von Ressourcen;
  • Zeile 54: Die Funktion [send_command] ist für das Senden der Befehle des Clients an den SMTP-Server zuständig. Sie nimmt vier Parameter entgegen:
    • [connection]: die Verbindung zwischen dem Client und dem Server;
    • [command]: der zu sendende Befehl;
    • [verbose]: Wenn TRUE, werden Client-Server-Kommunikationen in der Konsole protokolliert;
    • [with_rclf]: Wenn TRUE, wird der Befehl mit der Sequenz \r\n abgeschlossen gesendet. Dies ist für alle Befehle des SMTP-Protokolls erforderlich, aber [send_command] wird auch zum Senden der Nachricht verwendet. In diesem Fall wird die Sequenz \r\n nicht hinzugefügt;
  • Zeile 62: Der Befehl wird nur gesendet, wenn er nicht leer ist;
  • Zeilen 65–66: Der Befehl wird als UTF-8-Byte-Zeichenkette an den Server gesendet;
  • Zeilen 70–71: Liest alle Zeilen der Antwort. Wir gehen davon aus, dass sie weniger als 1000 Zeichen umfasst. Die Antwort kann mehrere Zeilen enthalten. 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 (ohne Bindestrich) hat;
  • Zeile 76: Liest den Fehlercode XXX aus der ersten Zeile;
  • Zeilen 78–80: Ist der numerische Code XXX größer als 500, hat der Server einen Fehler zurückgegeben. In diesem Fall wird eine Ausnahme ausgelöst;

Ergebnisse

Die Ausführung des Skripts erzeugt die folgende Konsolenausgabe:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/inet/smtp/01/main.py
----------------------------------
Envoi du message [mail to localhost via localhost]
--> [EHLO DESKTOP-30FF5FB]
<-- [220 Bienvenue sur le serveur SMTP localhost.com]
--> [MAIL FROM: <guest@localhost.com>]
<-- [250-DESKTOP-30FF5FB
250-SIZE 20480000
250-AUTH LOGIN
250 HELP]
--> [RCPT TO: <guest@localhost.com>]
<-- [250 OK]
--> [DATA]
<-- [250 OK]
--> [From: guest@localhost.com
To: guest@localhost.com
Subject: to localhost via localhost
Content-type: text/plain; charset="utf-8"
 
aglaë séléné
va au marché
acheter des fleurs]
<-- [354 OK, send.]
--> [
.
]
<-- [250 Queued (0.000 seconds)]
--> [QUIT]
<-- [221 goodbye]
Message envoyé...
----------------------------------
Envoi du message [mail to gmail via gmail]
--> [EHLO DESKTOP-30FF5FB]
<-- [220 smtp.gmail.com ESMTP u1sm1364433wrb.78 - gsmtp]
--> [MAIL FROM: <pymailparlexemple@gmail.com>]
<-- [250-smtp.gmail.com at your service, [2a01:cb05:80e8:b500:3c4b:2203:91fa:9b00]
250-SIZE 35882577
250-8BITMIME
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-CHUNKING
250 SMTPUTF8]
--> [RCPT TO: <pymailparlexemple@gmail.com>]
<-- [530 5.7.0 Must issue a STARTTLS command first. u1sm1364433wrb.78 - gsmtp]
L'erreur suivante s'est produite : 5.7.0 Must issue a STARTTLS command first. u1sm1364433wrb.78 - gsmtp
 
 
Process finished with exit code 0
  • Zeilen 3–30: Das Senden einer E-Mail an [guest@localhost] über den SMTP-Server [hMailServer] funktioniert einwandfrei;
  • Zeilen 32–46: Der Versuch, über den SMTP-Server [smtp.gmail.com] eine E-Mail an [pymailparlexemple@gmail.com] zu senden, schlägt fehl: In Zeile 45 gibt der SMTP-Server den Fehlercode 530 mit einer Fehlermeldung zurück. Dies bedeutet, dass sich der SMTP-Client zunächst über eine sichere Verbindung authentifizieren muss. Unser Client hat dies nicht getan und wird daher abgelehnt;

Die Ergebnisse in Thunderbird lauten wie folgt:

Image

21.5.7. Skripte [smtp/02]: Ein SMTP-Client, der unter Verwendung der Bibliothek [smtplib] geschrieben wurde

Image

Der bisherige Client weist mindestens zwei Mängel auf:

  1. Er kann keine sichere Verbindung verwenden, wenn der Server eine solche erfordert;
  2. er kann keine Dateien an die Nachricht anhängen;

Wir werden den ersten Mangel im Skript [smtp/02] beheben. In unserem neuen Skript werden wir das Python-Modul [smtplib] verwenden.

Das Skript [smtp/02/main] verwendet die folgende JSON-Konfigurationsdatei [smtp/02/config]:

def configure() -> dict:
    return {
        #  description: description of the e-mail sent
        #  smtp-server: SMTP server
        #  smtp-port: server port SMTP
        # from : expéditeur
        #  to: recipient
        #  subject : mail subject
        #  message : mail message
        "mails": [
            {
                "description": "mail to localhost via localhost avec smtplib",
                "smtp-server": "localhost",
                "smtp-port": "25",
                "from": "guest@localhost.com",
                "to": "guest@localhost.com",
                "subject": "to localhost via localhost avec smtplib",
                #  we test accented characters
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
            },
            {
                "description": "mail to gmail via gmail avec smtplib",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymail2parlexemple@gmail.com",
                "to": "pymail2parlexemple@gmail.com",
                "subject": "to gmail via gmail avec smtplib",
                #  we test accented characters
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                #  smtp with authentication
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlh@1QZ3TG",
            }
        ]
    }

Es sind dieselben Felder vorhanden wie in der Datei [smtp/01/config], mit zwei zusätzlichen Feldern, wenn der SMTP-Server eine Authentifizierung erfordert:

  • Zeile 31, [user]: der Benutzername, der zur Authentifizierung der Verbindung verwendet wird;
  • Zeile 32, [password]: das Passwort;

Diese beiden Felder sind nur vorhanden, wenn der angerufene SMTP-Server eine Authentifizierung erfordert. Diese erfolgt dann über eine sichere Verbindung.

Der Code für das Skript [smtp/02/main.py] lautet wie folgt:

#  imports
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate


# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: True):
    #  sends message to smtp server smtpserver from sender
    #  as recipient. If verbose=True, tracks client-server exchanges

    #  we use the smtplib library
    #  we let the exceptions rise
    #
    #  the SMTP server
    server = smtplib.SMTP(mail["smtp-server"])
    #  verbose mode
    server.set_debuglevel(verbose)
    #  secure connection?
    if "user" in mail:
        #  secure connection
        server.starttls()
        #  EHLO order + authentication
        server.login(mail["user"], mail["password"])

   #  construction of a Multipart message - this is the message that Multipart will send
    msg = MIMEText(mail["message"])
    msg['from'] = mail["from"]
    msg['to'] = mail["to"]
    msg['date'] = formatdate(localtime=True)
    msg['subject'] = mail["subject"]
    #  we send the message
    server.send_message(msg)
    #  we leave
    server.quit()


#  main ----------------------------------------------------------------

#  information is taken from a config file containing the following information for each server

#  description: description of the e-mail sent
#  smtp-server: SMTP server
#  smtp-port: server port SMTP
# from : expéditeur
#  to: recipient
#  subject : mail subject
#  content-type: mail encoding
#  message : mail message


#  application configuration
import config
config = config.configure()

#  we deal with e-mails one by one
for mail in config['mails']:
    try:
        #  logs
        print("----------------------------------")
        print(f"Envoi du message [{mail['description']}]")
        #  send message in verbose mode
        sendmail(mail, True)
        #  end
        print("Message envoyé...")
    except BaseException as erreur:
        #  error is displayed
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
    #  next mail

Kommentare

  • Zeilen 8–35: Es wird nur die Funktion [sendmail] verwendet. Diese nutzt nun das Modul [smtplib] (Zeile 2);
  • Zeile 16: Verbindung zum SMTP-Server herstellen;
  • Zeile 18: Wenn [verbose=True], wird der Austausch zwischen Client und Server auf der Konsole angezeigt;
  • Zeilen 20–24: Die Authentifizierung wird durchgeführt, falls vom SMTP-Server verlangt;
  • Zeile 22: Die Authentifizierung erfolgt über eine sichere Verbindung;
  • Zeile 24: Authentifizierung;
  • Zeilen 26–33: Senden der Nachricht. Anschließend findet der Dialog mit dem Skript [smtp/01/main] statt. Falls eine Authentifizierung stattgefunden hat, erfolgt diese über eine sichere Verbindung;
  • Zeile 35: Der Client-Server-Dialog endet;

Bevor Sie das Skript [smtp/02/main] ausführen, müssen Sie die Gmail-Kontoeinstellungen ändern [pymailparlexemple@gmail.com]:

  • Melden Sie sich beim Gmail-Konto an [pymailparlexemple@gmail.com];
  • Ändern Sie die folgenden Einstellungen: Image
  • Erlauben Sie unter [2] weniger sicheren Apps den Zugriff auf das Konto;

Machen Sie dasselbe für das zweite Gmail-Konto [pymail2parlexemple@gmail.com].

Ergebnisse

Beim Ausführen des Skripts [smtp/02/main] wird die folgende Konsolenausgabe angezeigt:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/inet/smtp/02/main.py
----------------------------------
Envoi du message [mail to localhost via localhost avec smtplib]
send: 'ehlo [192.168.43.163]\r\n'
reply: b'250-DESKTOP-30FF5FB\r\n'
reply: b'250-SIZE 20480000\r\n'
reply: b'250-AUTH LOGIN\r\n'
reply: b'250 HELP\r\n'
reply: retcode (250); Msg: b'DESKTOP-30FF5FB\nSIZE 20480000\nAUTH LOGIN\nHELP'
send: 'mail FROM:<guest@localhost.com> size=310\r\n'
reply: b'250 OK\r\n'
reply: retcode (250); Msg: b'OK'
send: 'rcpt TO:<guest@localhost.com>\r\n'
reply: b'250 OK\r\n'
reply: retcode (250); Msg: b'OK'
send: 'data\r\n'
reply: b'354 OK, send.\r\n'
reply: retcode (354); Msg: b'OK, send.'
data: (354, b'OK, send.')
send: b'Content-Type: text/plain; charset="utf-8"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: base64\r\nfrom: guest@localhost.com\r\nto: guest@localhost.com\r\ndate: Wed, 08 Jul 2020 08:35:39 +0200\r\nsubject: to localhost via localhost avec smtplib\r\n\r\nYWdsYcOrIHPDqWzDqW7DqQp2YSBhdSBtYXJjaMOpCmFjaGV0ZXIgZGVzIGZsZXVycw==\r\n.\r\n'
reply: b'250 Queued (0.000 seconds)\r\n'
reply: retcode (250); Msg: b'Queued (0.000 seconds)'
data: (250, b'Queued (0.000 seconds)')
send: 'quit\r\n'
reply: b'221 goodbye\r\n'
reply: retcode (221); Msg: b'goodbye'
Message envoyé...
----------------------------------
Envoi du message [mail to gmail via gmail avec smtplib]
send: 'ehlo [192.168.43.163]\r\n'
reply: b'250-smtp.gmail.com at your service, [37.172.118.130]\r\n'
reply: b'250-SIZE 35882577\r\n'
reply: b'250-8BITMIME\r\n'
reply: b'250-STARTTLS\r\n'
reply: b'250-ENHANCEDSTATUSCODES\r\n'
reply: b'250-PIPELINING\r\n'
reply: b'250-CHUNKING\r\n'
reply: b'250 SMTPUTF8\r\n'
reply: retcode (250); Msg: b'smtp.gmail.com at your service, [37.172.118.130]\nSIZE 35882577\n8BITMIME\nSTARTTLS\nENHANCEDSTATUSCODES\nPIPELINING\nCHUNKING\nSMTPUTF8'
send: 'STARTTLS\r\n'
reply: b'220 2.0.0 Ready to start TLS\r\n'
reply: retcode (220); Msg: b'2.0.0 Ready to start TLS'
send: 'ehlo [192.168.43.163]\r\n'
reply: b'250-smtp.gmail.com at your service, [37.172.118.130]\r\n'
reply: b'250-SIZE 35882577\r\n'
reply: b'250-8BITMIME\r\n'
reply: b'250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH\r\n'
reply: b'250-ENHANCEDSTATUSCODES\r\n'
reply: b'250-PIPELINING\r\n'
reply: b'250-CHUNKING\r\n'
reply: b'250 SMTPUTF8\r\n'
reply: retcode (250); Msg: b'smtp.gmail.com at your service, [37.172.118.130]\nSIZE 35882577\n8BITMIME\nAUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH\nENHANCEDSTATUSCODES\nPIPELINING\nCHUNKING\nSMTPUTF8'
send: 'AUTH PLAIN AHB5bWFpbDJwYXJsZXhlbXBsZUBnbWFpbC5jb20AIzZwcklsaEQmQDFRWjNURw==\r\n'
reply: b'235 2.7.0 Accepted\r\n'
reply: retcode (235); Msg: b'2.7.0 Accepted'
send: 'mail FROM:<pymail2parlexemple@gmail.com> size=320\r\n'
reply: b'250 2.1.0 OK e5sm4132618wrs.33 - gsmtp\r\n'
reply: retcode (250); Msg: b'2.1.0 OK e5sm4132618wrs.33 - gsmtp'
send: 'rcpt TO:<pymail2parlexemple@gmail.com>\r\n'
reply: b'250 2.1.5 OK e5sm4132618wrs.33 - gsmtp\r\n'
reply: retcode (250); Msg: b'2.1.5 OK e5sm4132618wrs.33 - gsmtp'
send: 'data\r\n'
reply: b'354  Go ahead e5sm4132618wrs.33 - gsmtp\r\n'
reply: retcode (354); Msg: b'Go ahead e5sm4132618wrs.33 - gsmtp'
data: (354, b'Go ahead e5sm4132618wrs.33 - gsmtp')
send: b'Content-Type: text/plain; charset="utf-8"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: base64\r\nfrom: pymail2parlexemple@gmail.com\r\nto: pymail2parlexemple@gmail.com\r\ndate: Wed, 08 Jul 2020 08:35:40 +0200\r\nsubject: to gmail via gmail avec smtplib\r\n\r\nYWdsYcOrIHPDqWzDqW7DqQp2YSBhdSBtYXJjaMOpCmFjaGV0ZXIgZGVzIGZsZXVycw==\r\n.\r\n'
reply: b'250 2.0.0 OK  1594190139 e5sm4132618wrs.33 - gsmtp\r\n'
reply: retcode (250); Msg: b'2.0.0 OK  1594190139 e5sm4132618wrs.33 - gsmtp'
data: (250, b'2.0.0 OK  1594190139 e5sm4132618wrs.33 - gsmtp')
send: 'quit\r\n'
Message envoyé...
reply: b'221 2.0.0 closing connection e5sm4132618wrs.33 - gsmtp\r\n'
reply: retcode (221); Msg: b'2.0.0 closing connection e5sm4132618wrs.33 - gsmtp'
 
Process finished with exit code 0
  • Zeile 40: Der Client [smtplib] initiiert den Dialog zum Aufbau einer verschlüsselten Verbindung mit dem SMTP-Server, was uns im Skript [smtp/main/01] nicht gelungen ist;
  • ansonsten sehen wir die bekannten SMTP-Protokollbefehle;

Wenn wir das Gmail-Konto des Benutzers [pymail2parlexemple] überprüfen, sehen wir Folgendes:

Image

21.5.8. Skripte [smtp/03]: Umgang mit angehängten Dateien

Wir vervollständigen das Skript [smtp/02/main], damit die gesendete E-Mail Anhänge enthalten kann.

Image

Das Skript [smtp/03/main] wird durch das folgende Skript [smtp/03/config] konfiguriert:

import os


def configure() -> dict:
    #  application configuration
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        #  description: description of the e-mail sent
        #  smtp-server: SMTP server
        #  smtp-port: server port SMTP
        # from : expéditeur
        #  to: recipient
        #  subject : mail subject
        #  message : mail message
        "mails": [
            {
                "description": "mail to gmail via gmail avec smtplib",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymail2parlexemple@gmail.com",
                "to": "pymail2parlexemple@gmail.com",
                "subject": "to gmail via gmail avec smtplib",
                #  we test accented characters
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                #  smtp with authentication
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                #  here, absolute paths must be set for attached files
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                ]
            }
        ]
    }

Die Datei [smtp/03/config] unterscheidet sich von der zuvor verwendeten Datei [smtp/02/config] lediglich durch das optionale Vorhandensein einer [attachments]-Liste (Zeilen 30–32), die die Liste der Dateien angibt, die an die zu versendende Nachricht angehängt werden sollen.

Das Skript [smtp/03/main] lautet wie folgt:

#  imports
import email
import mimetypes
import os
import smtplib
from email import encoders
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.mime.message import MIMEMessage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate



# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: True):
    #  sends mail[message] to smtp server mail[smtp-server] from mail[from]
    #  for mail[to]. If verbose=True, tracks client-server exchanges

    #  we use the smtplib library
    #  we let the exceptions rise
    #
    #  the SMTP server
    server = smtplib.SMTP(mail["smtp-server"])
    #  verbose mode
    server.set_debuglevel(verbose)
    #  secure connection?
    if "user" in mail:
        server.starttls()
        server.login(mail["user"], mail["password"])

    #  construction of a Multipart message - this is the message that will be sent
    #  credit: https://docs.python.org/3.4/library/email-examples.html
    msg = MIMEMultipart()
    msg['From'] = mail["from"]
    msg['To'] = mail["to"]
    msg['Date'] = formatdate(localtime=True)
    msg['Subject'] = mail["subject"]
    #  attach the text message in MIMEText format
    msg.attach(MIMEText(mail["message"]))
    #  we go through the attachments
    for path in mail["attachments"]:
        #  path must be an absolute path
        #  you can guess the type of file attached
        ctype, encoding = mimetypes.guess_type(path)
        #  if you haven't guessed
        if ctype is None or encoding is not None:
            # No guess could be made, or the file is encoded (compressed), so
            # use a generic bag-of-bits type.
            ctype = 'application/octet-stream'
        #  decompose the type into maintype/subtype
        maintype, subtype = ctype.split('/', 1)
        #  we deal with the various cases
        if maintype == 'text':
            with open(path) as fp:
                # Note: we should handle calculating the charset
                part = MIMEText(fp.read(), _subtype=subtype)
        elif maintype == 'image':
            with open(path, 'rb') as fp:
                part = MIMEImage(fp.read(), _subtype=subtype)
        elif maintype == 'audio':
            with open(path, 'rb') as fp:
                part = MIMEAudio(fp.read(), _subtype=subtype)
        #  message type case / rfc822
        elif maintype == 'message':
            with open(path, 'rb') as fp:
                part = MIMEMessage(email.message_from_bytes(fp.read()))
        else:
            #  other cases
            with open(path, 'rb') as fp:
                part = MIMEBase(maintype, subtype)
                part.set_payload(fp.read())
            # Encode the payload using Base64
            encoders.encode_base64(part)
        # Set the filename parameter
        basename = os.path.basename(path)
        part.add_header('Content-Disposition', 'attachment', filename=basename)
        #  attach the file to the message to be sent
        msg.attach(part)
    #  all attachments have been made - the message is sent as a string
    server.send_message(msg)


#  main ----------------------------------------------------------------

..

Kommentare

  • Zeilen 18–32: Die Funktion [sendmail] bleibt unverändert, so wie sie war, als noch keine Anhänge vorhanden waren;
  • Zeile 35: Der folgende Code stammt aus der offiziellen Python-Dokumentation;
  • Zeile 36: Die zu versendende Nachricht besteht aus mehreren Teilen: Text und angehängten Dateien. Dies wird als [Multipart]-Nachricht bezeichnet;
  • Zeilen 37–40: Die [Multipart]-Nachricht enthält die üblichen Felder, die in jeder E-Mail zu finden sind;
  • Zeile 42: Die verschiedenen Teile der [Multipart]-Nachricht [msg] werden mit der Methode [msg.attach] (Zeile 81) an die Nachricht angehängt. Die angehängten Teile können beliebigen Typs sein. Diese werden durch einen MIME-Typ identifiziert. Der MIME-Typ für reinen Text ist [MIMEText];
  • Zeilen 44–81: Alle Anhänge für die zu versendende Nachricht werden an die [Multipart]-Nachricht [msg] angehängt (Zeile 81);
  • Zeile 44: [path] steht für den absoluten Pfad der anzuhängenden Datei;
  • Zeile 47: Um den für den Anhang zu verwendenden MIME-Typ zu ermitteln, verwenden wir die Dateiendung (.docx, .php usw.) der anzuhängenden Datei. Die Methode [mimetypes.guess_type] übernimmt diese Aufgabe. Sie gibt zwei Informationen zurück:
    • [ctype]: den MIME-Typ der Datei;
    • [encoding]: Informationen über die Kodierung;
  • Zeilen 49–52: Wenn der MIME-Typ der Datei nicht ermittelt werden kann, wird sie als Binärdatei behandelt (Zeile 52);
  • Zeile 54: Der MIME-Typ einer Datei wird in Primärtyp und Sekundärtyp unterteilt, zum Beispiel [application/pdf]. Wir trennen diese beiden Elemente;
  • Zeilen 56–76: Je nach Wert des primären MIME-Typs werden verschiedene Fälle behandelt. Im Fall einer PDF-Datei ([application/pdf]) werden beispielsweise die Zeilen 70–76 ausgeführt:
    • Zeilen 56–59: der Fall, in dem es sich bei der angehängten Datei um eine Textdatei handelt. In diesem Fall wird ein Element vom Typ [MIMEText] mit dem Inhalt [fp.read] erstellt;
    • Zeilen 60–62: der Fall, in dem die Datei ein Bild enthält. In diesem Fall erstellen wir ein Element vom Typ [MIMEImage] mit dem Inhalt [fp.read];
    • Zeilen 63–65: der Fall, in dem es sich bei der Datei um eine Audiodatei handelt. In diesem Fall wird ein Element vom Typ [MIMEAudio] mit dem Inhalt [fp.read] erstellt;
    • Zeilen 66–69: Der Fall, in dem es sich bei der Datei um eine E-Mail handelt. In diesem Fall erstellen wir ein Element vom Typ [MIMEMessage] (Zeile 69) mit dem Inhalt [email.message_from_bytes(fp.read())]. Im Gegensatz zu den vorherigen Fällen, in denen der Inhalt des MIME-Elements der binäre Inhalt der zugehörigen Datei war, ist der Inhalt des MIMEMessage-Elements hier vom Typ [email.message.Message];
    • Zeilen 70–76: andere Fälle. Dazu gehören beispielsweise die Word- und PDF-Dateien in unserem Beispiel;
  • Zeile 72: Die anzuhängende Datei wird im Binärmodus geöffnet (rb=read binary);
  • Zeile 74: [fp.read] liest die gesamte Binärdatei;
  • Zeilen 72–74: Die Struktur [with open(…) as file] bewirkt zwei Dinge:
    • Sie öffnet die Datei und weist ihr den [file]-Deskriptor zu;
    • sie stellt sicher, dass beim Verlassen des [with]-Blocks, unabhängig davon, ob ein Fehler auftritt oder nicht, der [file]-Deskriptor geschlossen wird. Sie ist daher eine Alternative zur [try file=open(…)/ finally]-Struktur;
  • Zeile 73: Es wird ein neues [part]-Element erstellt, das in die Multipart-Nachricht aufgenommen werden soll. Hier wird die Klasse [MIMEBase] verwendet, und die in Zeile 54 festgelegten Elemente [maintype, subtype] werden an den Konstruktor übergeben;
  • Zeile 74: Das in die Multipart-Nachricht einzufügende Element muss Inhalt haben. Dieser kann mit der Methode [set_payload] initialisiert werden;
  • Zeilen 75–76: Angehängte Dateien müssen 7-Bit-kodiert sein. In der Vergangenheit unterstützten einige SMTP-Server nur 7-Bit-kodierte Zeichen. Hier wird die als „Base64“ bekannte Kodierung verwendet;
  • Zeile 77: Ab dieser Zeile entspricht die Verarbeitung der aller MIME-Typen, die wir in den Zeilen 56–76 erstellt haben [MIMEMessage, MIMEImage, MIMEAudio, MIMEBase, MIMEText];
  • Zeile 79: Das Element, das der Multipart-Nachricht hinzugefügt werden soll, verfügt über einen Header, der es beschreibt. Hier geben wir an, dass das hinzugefügte Element einer angehängten Datei entspricht. Der Name dieser Datei ist der dritte Parameter, der an die Methode [add_header] übergeben wird. Dieser Dateiname wird häufig von E-Mail-Clients verwendet, um die angehängte Datei unter diesem Namen im Dateisystem des Clients zu speichern. Bisher haben wir mit dem absoluten Pfad der angehängten Datei gearbeitet. Hier übergeben wir einfach ihren Namen ohne den Pfad (Zeile 78);
  • Zeile 81: Die Binärdaten der Datei werden in die [msg Multipart]-Nachricht eingebettet;
  • Zeile 83: Sobald alle Teile der Nachricht an die [msg Multipart] angehängt wurden, wird diese gesendet;

Ergebnisse

Wenn wir das Skript [smtp/03/main] ausführen, wobei die Datei [smtp/02/config] bereits vorhanden ist, erhält das Konto [pymail2parlexemple@gmail.com] Folgendes:

Image

Die angehängten Dateien sind in [4, 9–11] dargestellt.

Sehen wir uns nun ein Beispiel mit einem E-Mail-Anhang an. Wir speichern die oben in [3] empfangene E-Mail:

Image

Wir speichern die E-Mail unter dem Namen [mail attachment 1.eml] im Ordner [smtp/03/attachments].

Wir werden nun die Datei [smtp/03/config] wie folgt ändern:

import os


def configure() -> dict:
    #  application configuration
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        #  description: description of the e-mail sent
        #  smtp-server: SMTP server
        #  smtp-port: server port SMTP
        # from : expéditeur
        #  to: recipient
        #  subject : mail subject
        #  message : mail message
        "mails": [
            {
                "description": "mail to gmail via gmail avec smtplib",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymail2parlexemple@gmail.com",
                "to": "pymail2parlexemple@gmail.com",
                "subject": "to gmail via gmail avec smtplib",
                #  we test accented characters
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                #  smtp with authentication
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                #  here, absolute paths must be set for attached files
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                    f"{script_dir}/attachments/mail attaché 1.eml",
                ]
            }
        ]
    }
  • Zeile 33 haben wir einen Anhang hinzugefügt;

Nun führen wir das Skript [smtp/03/main] erneut aus. Dies führt zu folgendem Ergebnis im Postfach des Benutzers [pymail2parlexemple@gmail.com]:

Image

  • in [1] die empfangene E-Mail;
  • in [2]: der Nachrichtentext;
  • in [3]: der Text der angehängten E-Mail;
  • in [4]: Thunderbird hat 5 Anhänge gefunden:
    • [attached_file.docx];
    • [angehängte_Datei.pdf];
    • [angehängte-E-Mail-1.eml]. Dieser Anhang ist selbst eine E-Mail, die zwei Anhänge enthält:
      • [angehängte_Datei.docx];
      • [attached file.pdf];

21.6. Das POP3-Protokoll

21.6.1. Einführung

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

  • das POP3-Protokoll (Post Office Protocol), historisch gesehen das erste Protokoll, das heute jedoch nur noch selten verwendet wird;
  • das IMAP-Protokoll (Internet Message Access Protocol), das neuer als POP3 ist und derzeit am weitesten verbreitet ist;

Um das POP3-Protokoll zu untersuchen, verwenden wir die folgende Architektur:

Image

  • [Server B] ist je nach Situation:
    • ein lokaler POP3-Server, implementiert durch den Mailserver [hMailServer];
    • der Server [pop.gmail.com], der der POP3-Server für den E-Mail-Dienst [gmail.com] ist;
  • [Client A] wird ein POP3-Client in verschiedenen Formen sein:
    • der [RawTcpClient]-Client zur Erkundung des POP3-Protokolls;
    • ein Python-Skript, das das POP3-Protokoll des [RawTcpClient]-Clients emuliert;
    • ein Python-Skript, das Python-Module verwendet, um Anhänge zu verarbeiten und bei Bedarf eine verschlüsselte und authentifizierte Verbindung zum POP3-Server herzustellen;

21.6.2. Erkundung des POP3-Protokolls

Wie bereits beim SMTP-Protokoll werden wir das POP3-Protokoll anhand der Protokolle des [hMailServer]-Mail-Servers untersuchen. Dazu müssen wir diesen Server starten.

Mit Thunderbird werden wir:

  • eine E-Mail an den Benutzer [guest@localhost.com] senden;
  • die Mailbox dieses Benutzers lesen;

Image

Image

In [3-6] oben die vom Benutzer [guest@localhost.com] empfangene Nachricht.

Wir werden nun die Protokolle von [hMailServer] überprüfen. Dazu verwenden wir das Verwaltungstool [hMailServer Administrator]:

Image

Die POP3-Protokolle lauten wie folgt (die letzten Zeilen in der heutigen Protokolldatei):


"POP3D"    35084    5    "2020-07-08 14:19:46.392"    "127.0.0.1"    "SENT: +OK Bienvenue sur le serveur POP3 localhost.com"
"POP3D"    34968    5    "2020-07-08 14:19:46.405"    "127.0.0.1"    "RECEIVED: CAPA"
"POP3D"    34968    5    "2020-07-08 14:19:46.407"    "127.0.0.1"    "SENT: +OK CAPA list follows[nl]USER[nl]UIDL[nl]TOP[nl]."
"POP3D"    35076    5    "2020-07-08 14:19:46.410"    "127.0.0.1"    "RECEIVED: USER guest"
"POP3D"    35076    5    "2020-07-08 14:19:46.411"    "127.0.0.1"    "SENT: +OK Send your password"
"POP3D"    34968    5    "2020-07-08 14:19:46.418"    "127.0.0.1"    "RECEIVED: PASS ***"
"POP3D"    34968    5    "2020-07-08 14:19:46.421"    "127.0.0.1"    "SENT: +OK Mailbox locked and ready"
"POP3D"    34968    5    "2020-07-08 14:19:46.423"    "127.0.0.1"    "RECEIVED: STAT"
"POP3D"    34968    5    "2020-07-08 14:19:46.423"    "127.0.0.1"    "SENT: +OK 1 612"
"POP3D"    34968    5    "2020-07-08 14:19:46.426"    "127.0.0.1"    "RECEIVED: LIST"
"POP3D"    34968    5    "2020-07-08 14:19:46.426"    "127.0.0.1"    "SENT: +OK 1 messages (612 octets)"
"POP3D"    34968    5    "2020-07-08 14:19:46.426"    "127.0.0.1"    "SENT: 1 612[nl]."
"POP3D"    35076    5    "2020-07-08 14:19:46.427"    "127.0.0.1"    "RECEIVED: UIDL"
"POP3D"    35076    5    "2020-07-08 14:19:46.428"    "127.0.0.1"    "SENT: +OK 1 messages (612 octets)[nl]1 42[nl]."
"POP3D"    34968    5    "2020-07-08 14:19:46.435"    "127.0.0.1"    "RECEIVED: RETR 1"
"POP3D"    34968    5    "2020-07-08 14:19:46.436"    "127.0.0.1"    "SENT: ."
"POP3D"    34924    5    "2020-07-08 14:19:46.459"    "127.0.0.1"    "RECEIVED: QUIT"
"POP3D"    34924    5    "2020-07-08 14:19:46.459"    "127.0.0.1"    "SENT: +OK POP3 server saying goodbye..."
  • Zeile 1: Der POP3-Server sendet eine Willkommensnachricht an den Client (Thunderbird), der sich gerade verbunden hat;
  • Zeile 2: Der Client sendet den Befehl [CAPA] (Fähigkeiten), um eine Liste der Befehle anzufordern, die er verwenden kann;
  • Zeile 3: Der Server antwortet, dass er die Befehle [USER, UIDL, TOP] verwenden kann. Der POP-Server beginnt seine Antworten mit [+OK] oder [-ERR], um anzugeben, ob die Ausführung des Befehls des Clients erfolgreich war oder fehlgeschlagen ist;
  • Zeile 4: Der Client sendet den Befehl [USER guest], um anzugeben, dass er auf das Postfach des Benutzers [guest] zugreifen möchte;
  • Zeile 5: Der Server antwortet mit [+OK] und fordert das Passwort für [guest] an;
  • Zeile 6: Der Client sendet den Befehl [PASS password], um das Passwort für den Benutzer [guest] zu übermitteln. Hier wird das Passwort im Klartext gesendet, da der POP3-Server keine sichere Verbindung erzwungen hat. Wir werden sehen, dass dies beim POP3-Server von Gmail anders ist;
  • Zeile 7: Der Server hat den Benutzernamen und das Passwort überprüft. Dies bedeutet, dass er das Postfach des Benutzers [guest] sperrt;
  • Zeile 8: Der Client sendet den Befehl [STAT], der Informationen über das Postfach anfordert;
  • Zeile 9: Der Server antwortet, dass eine 612-Byte-Nachricht vorhanden ist. Im Allgemeinen antwortet er, dass N Nachrichten vorhanden sind, und gibt die Gesamtgröße dieser Nachrichten an;
  • Zeile 10: Der Client sendet den Befehl [LIST]. Dieser Befehl fordert die Liste der Nachrichten an;
  • Zeile 11: Der Server sendet die Liste der Nachrichten im folgenden Format:
    • eine Zusammenfassungszeile mit der Anzahl der Nachrichten und ihrer Gesamtgröße;
    • eine Zeile pro Nachricht, die die Nachrichtennummer und deren Größe angibt;
  • Zeile 13: Der Client sendet den Befehl [UIDL], der eine Liste der Nachrichten mit ihren Kennungen anfordert. Jede Nachricht wird durch eine eindeutige Nummer innerhalb des E-Mail-Dienstes identifiziert;
  • Zeile 14: Die Antwort des Servers. Wir sehen, dass Nachricht Nr. 1 in der Liste die Kennung 42 hat;
  • Zeile 15: Der Client sendet den Befehl [RETR 1] und fordert damit an, dass Nachricht Nr. 1 aus der Liste an ihn übertragen wird;
  • Zeile 16: Der POP3-Server führt dies aus;
  • Zeile 17: Der Client sendet den Befehl [QUIT], um anzuzeigen, dass er die Verbindung zum POP3-Server trennt;
  • Zeile 18: Der Server wird ebenfalls seine Verbindung zum Client schließen, sendet jedoch zuvor eine Abschiedsnachricht;

Wir werden nun Elemente des obigen Dialogs mithilfe des in einem PyCharm-Fenster ausgeführten [RawTcpClient]-Clients nachstellen:

Image

Der Dialog verläuft wie folgt:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 110
Client [DESKTOP-30FF5FB:63762] connecté au serveur [localhost-110]
Tapez vos commandes (quit pour arrêter) :
<-- [+OK Bienvenue sur le serveur POP3 localhost.com]
USER guest
<-- [+OK Send your password]
PASS guest
<-- [+OK Mailbox locked and ready]
LIST
<-- [+OK 1 messages (612 octets)]
<-- [1 612]
<-- [.]
RETR 1
<-- [+OK 612 octets]
<-- [Return-Path: guest@localhost.com]
<-- [Received: from [127.0.0.1] (DESKTOP-30FF5FB [127.0.0.1])]
<-- [   by DESKTOP-30FF5FB with ESMTP]
<-- [   ; Wed, 8 Jul 2020 14:19:36 +0200]
<-- [To: guest@localhost.com]
<-- [From: "guest@localhost.com" <guest@localhost.com>]
<-- [Subject: protocole POP3]
<-- [Message-ID: <ca895136-25c5-411e-373a-a68cbd0eca51@localhost.com>]
<-- [Date: Wed, 8 Jul 2020 14:19:33 +0200]
<-- [User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101]
<-- [ Thunderbird/68.10.0]
<-- [MIME-Version: 1.0]
<-- [Content-Type: text/plain; charset=utf-8; format=flowed]
<-- [Content-Transfer-Encoding: 8bit]
<-- [Content-Language: fr]
<-- []
<-- [ceci est un test pour découvrir le protocole POP3]
<-- []
<-- [.]
QUIT
Fin de la connexion avec le serveur
  • Zeile 1: Wir stellen eine Verbindung zu Port 110 auf dem Rechner [localhost] her. Dort läuft der POP3-Dienst von [hMailServer];
  • in den Zeilen 5, 7, 9, 13 und 34 verwenden wir die Befehle [USER, PASS, LIST, RETR, QUIT];
  • Zeile 4: die Begrüßungsnachricht des POP3-Servers;
  • Zeile 5: Wir geben an, dass wir auf das Postfach des Benutzers [guest] zugreifen möchten;
  • Zeile 7: Wir senden das Passwort des Benutzers [guest] im Klartext;
  • Zeile 9: Wir fordern die Liste der Nachrichten im Postfach an;
  • Zeile 13: Anforderung von Nachricht Nr. 1;
  • Zeilen 14–33: Der POP3-Server sendet Nachricht Nr. 1;
  • Zeile 34: Die Sitzung wird beendet;

Hier ist eine Übersicht über einige gängige Befehle, die von einem POP3-Server akzeptiert werden:

  • Der Befehl [USER] wird verwendet, um den Benutzer anzugeben, dessen Postfach Sie lesen möchten;
  • Der Befehl [PASS] dient zur Angabe des Passworts;
  • Der Befehl [LIST] fordert eine Liste der Nachrichten im Postfach des Benutzers an;
  • Der Befehl [RETR] fordert die Nachricht an, die durch die angegebene Nummer gekennzeichnet ist;
  • Der Befehl [DELE] fordert die Löschung der Nachricht an, deren Nummer angegeben wurde;
  • Der Befehl [QUIT] teilt dem Server mit, dass Sie fertig sind;

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 einzelnen Punkt besteht;

21.6.3. Skripte [pop3/01]: ein einfacher POP3-Client

Image

Da das POP3-Protokoll die gleiche Struktur wie das SMTP-Protokoll hat, ist das Skript [pop3/01/main.py] eine Portierung des Skripts [smtp/01/main.py]. Es verfügt über die folgende Konfigurationsdatei [pop3/01/config.py]:

def configure() -> dict:
    #  mailboxes from which e-mails are collected
    mailboxes = [
        #  server: server POP3
        #  port: server port POP3
        #  user: user whose messages are to be read
        #  password: your password
        #  maxmails: maximum number of e-mails to download
        #  timeout: maximum wait time for a server response
        #  encoding: encoding incoming e-mails
        #  delete: if True, then mail is deleted from the mailbox
        #  once they have been downloaded locally

        {
            "server": "localhost",
            "port": "110",
            "user": "guest",
            "password": "guest",
            "maxmails": 10,
            "timeout": 1.0,
            "encoding": "utf-8",
            "delete": False
        }
    ]
    #  we return the configuration
    return {
        "mailboxes": mailboxes
    }
  • Zeilen 3–24: die Liste der zu überprüfenden Postfächer. Hier gibt es nur eines;
  • Zeilen 4–12: Bedeutungen der Wörterbucheinträge, die die einzelnen Postfächer definieren;
  • Zeile 15: Der abgefragte POP3-Server ist der lokale Server [hMailServer];
  • Zeilen 17–18: Wir möchten das Postfach des Benutzers [guest@localhost] lesen;
  • Zeile 19: Wir lesen höchstens 10 E-Mails;
  • Zeile 20: Der Client wartet maximal 1 Sekunde auf eine Antwort vom Server;
  • Zeile 21: Der Kodierungstyp der abgerufenen Nachrichten;
  • Zeile 22: Wir löschen die heruntergeladenen Nachrichten nicht;

Das Skript [pop3/01/main.py] lautet wie folgt:

#  imports
import re
import socket


# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
    #  reads the mailbox described by the dictionary [mailbox]
    #  if verbose=True, tracks client-server exchanges



# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
    #  sends command to connection channel
    #  verbose mode if verbose=True
    #  if with_rclf=True, adds rclf sequence to exchange
    #  returns the 1st line of the answer



# --------------------------------------------------------------------------
def affiche(echange: str, sens: int):
    


#  main ----------------------------------------------------------------

#  client POP3 (Post Office Protocol) for reading mailbox messages
#  communication protocol POP3 client-server
#  -> client connects to smtp server port 110
#  <- server sends him a welcome message
#  -> customer sends command USER user
#  <- server responds OK or not
#  -> customer sends PASS mot_de_passe order
#  <- server responds OK or not
#  -> customer sends LIST command
#  <- server responds OK or not
#  -> customer sends command RETR n° for each email
#  <- server responds OK or not. If OK sends the requested mail content
#  -> server sends all the mail lines and ends with a line containing the
#  single character .
#  -> customer sends command DELE n° to delete an e-mail
#  <- server responds OK or not
#  # -> client sends QUIT command to end dialog with server
#  <- server responds OK or not
#  server responses have the form +OK text where -ERR text
#  The answer may consist of several lines. In this case, the last line consists of a single dot
#  text lines exchanged must end with the characters RC(#13) and LF(#10)
# 

#  retrieve application configuration
import config
config = config.configure()

#  we process mailboxes one by one
for mailbox in config['mailboxes']:
    try:
        #  console display
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
        #  reading the mailbox in verbose mode
        readmails(mailbox, True)
        #  end
        print("Lecture terminée...")
    except BaseException as erreur:
        #  error is displayed
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass

Kommentare

Wie bereits erwähnt, ist [pop3/01/main.py] eine Portierung des Skripts [smtp/01/main.py], das wir bereits besprochen haben. Wir werden nur auf die wichtigsten Unterschiede eingehen:

  • Zeile 64: Die Funktion [readmails] ist für das Abrufen von E-Mails aus einem Postfach zuständig. Die Anmeldedaten für dieses Postfach sind im Wörterbuch [mailbox] gespeichert. Der zweite Parameter [True] ist der Parameter [Verbose], der in diesem Fall die Protokollierung der Client-Server-Kommunikation aktiviert;

Die Funktion [readmails] lautet wie folgt:

# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
    #  reads mail from the mailbox described by the dictionary [mailbox]
    #  if verbose=True, tracks client-server exchanges

    #  isolate mailbox parameters
    #  we assume that the [mailbox] dictionary is valid
    server = mailbox['server']
    port = int(mailbox['port'])
    user = mailbox['user']
    password = mailbox['password']
    maxmails = mailbox['maxmails']
    delete = mailbox['delete']
    timeout = mailbox['timeout']

    #  let system errors show up
    connexion = None
    try:
        #  open a connection on [server] port [port] with a one-second timeout
        connexion = socket.create_connection((server, port), timeout=timeout)

        #  connection represents a bidirectional communication flow
        #  between the client (this program) and the pop3 server contacted
        #  this channel is used for the exchange of orders and information

        #  read welcome message
        send_command(mailbox, connexion, "", verbose, True)
        #  cmde USER
        send_command(mailbox, connexion, f"USER {user}", verbose, True)
        #  cmde PASS
        send_command(mailbox, connexion, f"PASS {password}", verbose, True)
        #  cmde LIST
        première_ligne = send_command(mailbox, connexion, "LIST", verbose, True)
        #  analysis of the 1st line to find out the number of messages
        match = re.match(r"^\+OK (\d+)", première_ligne)
        nbmessages = int(match.groups()[0])
        #  we loop on the messages
        imessage = 0
        while imessage < nbmessages and imessage < maxmails:
            #  cmde RETR
            send_command(mailbox, connexion, f"RETR {imessage + 1}", verbose, True)
            #  cmde DELE
            if delete:
                send_command(mailbox, connexion, f"DELE {imessage + 1}", verbose, True)
            #  next msg
            imessage += 1
        #  cmde QUIT
        send_command(mailbox, connexion, "QUIT", verbose, True)
        #  end
    finally:
        #  locking connection
        if connexion:
            connexion.close()

Kommentare

  • Zeilen 8–14: Abrufen der Konfigurationsinformationen für das zu überprüfende Postfach;
  • Zeilen 19–20: Eine Verbindung zum POP3-Server herstellen;
  • Zeilen 26–27: Lesen der vom Server gesendeten Begrüßungsnachricht;
  • Zeilen 28–29: Senden des Befehls [USER], um den Benutzer anzugeben, dessen E-Mails wir abrufen möchten;
  • Zeilen 30–31: Senden des Befehls [PASS], um das Passwort für diesen Benutzer anzugeben;
  • Zeilen 32–33: Senden des Befehls [LIST], um herauszufinden, wie viele E-Mails sich im Postfach dieses Benutzers befinden. Die Funktion [sendCommand] gibt die erste Zeile der Antwort des Servers zurück. In dieser Zeile gibt der Server an, wie viele Nachrichten sich im Postfach befinden;
  • Zeilen 34–36: Die Anzahl der Nachrichten wird aus der ersten Zeile der Antwort abgerufen;
  • Zeilen 39–46: Wir durchlaufen jede Nachricht in einer Schleife. Für jede senden wir zwei Befehle:
    • RETR i: um Nachricht Nr. i abzurufen (Zeilen 40–41);
    • DELE i: um sie zu löschen, falls die Konfiguration vorsieht, dass gelesene Nachrichten vom Server gelöscht werden müssen (Zeilen 43–44);
  • Zeilen 47–48: Der Befehl [QUIT] wird gesendet, um dem Server mitzuteilen, dass wir fertig sind;

Die Funktion [send_command] sieht wie folgt aus:

# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
    #  sends command to connection channel
    #  verbose mode if verbose=True
    #  if with_rclf=True, adds rclf sequence to exchange
    #  returns the 1st line of the answer

    #  end-of-line mark
    if with_rclf:
        rclf = "\r\n"
    else:
        rclf = ""
    #  send order if not empty
    if commande:
        connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
        #  possible echo
        if verbose:
            affiche(commande, 1)
    #  read the socket as if it were a text file
    encoding = f"{mailbox['encoding']}" if mailbox['encoding'] else None
    file = connexion.makefile(encoding=encoding)
    #  we process this file line by line
    #  read 1st line
    première_ligne = réponse = file.readline().strip()
    #  verbose mode?
    if verbose:
        affiche(première_ligne, 2)
    #  error code recovery
    code_erreur = réponse[0]
    if code_erreur == "-":
        #  there has been an error
        raise BaseException(réponse[5:])
    #  special case of multi-line responses LIST, RETR
    cmd = commande.lower()[0:4]
    if cmd == "list" or cmd == "retr":
        #  last line of the answer?
        dernière_ligne = False
        while not dernière_ligne:
            #  read next line
            ligne_suivante = file.readline().strip()
            #  verbose mode?
            if verbose:
                affiche(ligne_suivante, 2)
            #  last line?
            dernière_ligne = ligne_suivante == "."
    #  finished - we return the 1st line
    return première_ligne

Kommentare

  • Zeilen 13–18: Der [Befehl] wird nur dann an den POP3-Server gesendet, wenn er nicht leer ist. Dies ist notwendig, um die vom POP3-Server gesendete Begrüßungsnachricht zu lesen, auch wenn der Client noch keine Befehle gesendet hat;
  • Zeilen 19–21: Wir lesen den Socket so, als wäre er eine Textdatei. Dies ermöglicht es uns, die Methode [readline] (Zeile 24) zu verwenden und so die Nachricht Zeile für Zeile zu lesen. Wir verwenden den Schlüssel [encoding] aus dem Wörterbuch [mailbox], um die Kodierung der zu lesenden Zeilen anzugeben;
  • Zeile 24: Wir lesen die erste Zeile der Antwort;
  • Zeilen 28–32: Wir behandeln den Fall eines möglichen Fehlers. Diese sind vom Typ [-ERR ungültiges Passwort, -ERR Postfach unbekannt, -ERR Postfach kann nicht gesperrt werden…];
  • Zeile 32: Es wird eine Ausnahme mit der Fehlermeldung ausgelöst;
  • Zeile 35: Nur die Befehle [list, retr] können mehrzeilige Antworten haben;
  • Zeilen 36–45: Im Falle einer mehrzeiligen Antwort zeigen wir alle empfangenen Zeilen an (Zeilen 42–43), bis die letzte Zeile empfangen wurde (Zeile 45);
  • Zeile 46: Wir geben die erste gelesene Zeile zurück, da sie im Fall des Befehls [LIST] die Anzahl der Nachrichten in der Mailbox enthält;

Ergebnisse

Nehmen wir das vorherige Beispiel. Mit Thunderbird haben wir die folgende Nachricht an den Benutzer [guest@localhost] gesendet (der hMailServer muss laufen):

Image

Bei der Ausführung erhalten wir folgende Ergebnisse:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/inet/pop3/01/main.py
----------------------------------
Lecture de la boîte mail POP3 guest@localhost:110
<-- [+OK Bienvenue sur le serveur POP3 localhost.com]
--> [USER guest]
<-- [+OK Send your password]
--> [PASS guest]
<-- [+OK Mailbox locked and ready]
--> [LIST]
<-- [+OK 1 messages (612 octets)]
<-- [1 612]
<-- [.]
--> [RETR 1]
<-- [+OK 612 octets]
<-- [Return-Path: guest@localhost.com]
<-- [Received: from [127.0.0.1] (DESKTOP-30FF5FB [127.0.0.1])]
<-- [by DESKTOP-30FF5FB with ESMTP]
<-- [; Wed, 8 Jul 2020 14:19:36 +0200]
<-- [To: guest@localhost.com]
<-- [From: "guest@localhost.com" <guest@localhost.com>]
<-- [Subject: protocole POP3]
<-- [Message-ID: <ca895136-25c5-411e-373a-a68cbd0eca51@localhost.com>]
<-- [Date: Wed, 8 Jul 2020 14:19:33 +0200]
<-- [User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101]
<-- [Thunderbird/68.10.0]
<-- [MIME-Version: 1.0]
<-- [Content-Type: text/plain; charset=utf-8; format=flowed]
<-- [Content-Transfer-Encoding: 8bit]
<-- [Content-Language: fr]
<-- []
<-- [ceci est un test pour découvrir le protocole POP3]
<-- []
<-- [.]
--> [QUIT]
<-- [+OK POP3 server saying goodbye...]
Lecture terminée...
 
Process finished with exit code 0
  • Zeilen 15–31: Die an [guest@localhost] gesendete Nachricht wird korrekt abgerufen.

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

  1. die Fähigkeit, mit einem sicheren POP3-Server zu kommunizieren;
  2. die Fähigkeit, Anhänge in einer Nachricht zu lesen;

Wir werden diese beiden Funktionen mit einem neuen Skript implementieren, das diesmal etwas komplexer sein wird.

21.6.4. Skripte [pop3/02]: POP3-Client mit den Modulen [poplib] und [email]

Wir werden einen POP3-Client schreiben, der Anhänge verarbeiten und mit sicheren Servern kommunizieren kann. Außerdem werden wir Nachrichten und ihre Anhänge in Dateien speichern.

Wir werden zwei Python-Module verwenden:

  • [poplib]: das das POP3-Protokoll abwickelt;
  • [email]: das zahlreiche Submodule enthält, mit denen wir empfangene Nachrichten analysieren können. Jede Nachricht ist eine strukturierte Zeichenkette, die Folgendes enthält:
    • die Nachrichten-Header [From, To, Subject, Return-Path…];
    • die Nachricht im Text- und gegebenenfalls im HTML-Format;
    • Anhänge;

Image

Das Skript [inet/pop3/02/main] [1] wird durch die Datei [inet/pop3/02/config] [2] konfiguriert und verwendet das Modul [inet/shared/mail_parser] [3].

Die Datei [pop3/02/config] sieht wie folgt aus:

import os


def configure() -> dict:
    #  application configuration
    config = {
        #  list of mailboxes to be managed
        "mailboxes": [
            #  server: server POP3
            #  port: server port POP3
            #  user: user whose messages are to be read
            #  password: your password
            #  maxmails: maximum number of e-mails to download
            #  timeout: maximum wait time for a server response
            #  delete: true if downloaded messages are to be deleted from the server
            #  ssl: true if mail is read over a secure link
            #  output: the storage folder for downloaded messages

            {
                "server": "pop.gmail.com",
                "port": "995",
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                "maxmails": 10,
                "delete": False,
                "ssl": True,
                "timeout": 2.0,
                "output": "output"
            }
        ]
    }
    #  absolute path of script folder
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  absolute paths of folders to be included in the syspath
    absolute_dependencies = [
        #  local file
        f"{script_dir}/../../shared",
   ]

    #  syspath configuration
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  we return the configuration
    return config

Die Datei definiert die Liste der zu überprüfenden Postfächer und legt den Python-Pfad der Anwendung fest.

Hier gibt es nur ein Postfach:

  • Zeilen 22–23: der Benutzer, dessen E-Mails wir lesen möchten;
  • Zeilen 20–21: Name und Port des POP3-Servers, auf dem die E-Mails dieses Benutzers gespeichert sind;
  • Zeile 24: die maximale Anzahl der abzurufenden E-Mails. Wenn Sie dieses Skript auf Ihrem eigenen Postfach ausprobieren, möchten Sie wahrscheinlich nicht die Hunderte von dort gespeicherten E-Mails abrufen;
  • Zeile 25: ein Boolescher Wert, der angibt, ob eine E-Mail nach dem Lesen gelöscht werden soll (delete=True);
  • Zeile 26: Wenn das Attribut [ssl] auf True gesetzt wird, bedeutet dies, dass der in den Zeilen 20–21 definierte POP3-Server eine verschlüsselte Verbindung verwendet;
  • Zeile 27: die maximale Zeitüberschreitung für Serverantworten, ausgedrückt in Sekunden;
  • Zeile 28: Der Ordner, in dem gelesene E-Mails gespeichert werden sollen. Er wird erstellt, falls er nicht existiert. Dies ist ein relativer Pfad. Bei der Ausführung bezieht er sich auf den Ordner, aus dem Sie das Skript ausführen. Bei [Pycharm] ist dies der Ordner, der das Skript [pop3/02] enthält;

Das Skript [pop3/02/main] lautet wie folgt:

#  imports
import email
import os
import poplib
import shutil


#  reading a mailbox
def readmails(mailbox: dict, verbose: bool):
    #  reads the mailbox described by the dictionary [mailbox]
    #  if verbose=True, tracks client-server exchanges


#  main ----------------------------------------------------------------
#   client POP3 (Post Office Protocol) for reading e-mail messages

#  retrieve application configuration
import config
config = config.configure()

#  we process mailboxes one by one
for mailbox in config['mailboxes']:
    try:
        #  console display
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
        #  reading the mailbox in verbose mode
        readmails(mailbox, True)
        #  end
        print("Lecture terminée...")
    except BaseException as erreur:
        #  error is displayed
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
  • Zeilen 17–36: Der Abschnitt [main] des Skripts ähnelt dem des Skripts [pop3/01];

Die Funktion [readmails] sieht wie folgt aus:

#  reading a mailbox
def readmails(mailbox: dict, verbose: bool):
    #  reads the mailbox described by the dictionary [mailbox]
    #  if verbose=True, tracks client-server exchanges

    #  import from mail_parser
    from mail_parser import save_message

    #  isolate mailbox parameters
    #  we assume that the [mailbox] dictionary is valid
    server = mailbox['server']
    port = int(mailbox['port'])
    user = mailbox['user']
    password = mailbox['password']
    maxmails = mailbox['maxmails']
    ssl = mailbox['ssl']
    timeout = mailbox['timeout']
    output = mailbox['output']

    #  let system errors show up
    pop3 = None
    try:
        #  create storage folders if they don't exist
        if not os.path.isdir(output):
            os.mkdir(output)
        #  user
        dir2 = f"{output}/{user}"
        #  delete the [dir2] folder if it exists, then recreate it
        if os.path.isdir(dir2):
            #  delete
            shutil.rmtree(dir2)
        #  creation
        os.mkdir(dir2)
        #  open a connection on port [port] of [server]
        if ssl:
            pop3 = poplib.POP3_SSL(server, port, timeout=timeout)
        else:
            pop3 = poplib.POP3(server, port, timeout=timeout)

        #  connection represents a bidirectional communication flow
        #  between the client (this program) and the pop3 server contacted
        #  this channel is used for the exchange of orders and information

        #  verbose mode
        pop3.set_debuglevel(2 if verbose else 0)
        #  read welcome message
        pop3.getwelcome(    )
        #  cmde USER
        réponse = pop3.user(user)
        #  cmde PASS
        réponse = pop3.pass_(password)
        #  cmde LIST
        liste = pop3.list()
        #  mails are in list[1]
        imail = 0
        nb_mails = len(liste[1])
        fini = imail == maxmails or imail == nb_mails
        éléments = liste[1]
        while not fini:
            #  common feature
            élément = éléments[imail]
            #  element is a list of bytes decoded as a string
            desc = élément.decode()
            #  we have a chain separated by blanks
            #  the 1st element is the message number
            num = desc.split()[0]
            #  we retrieve the message
            message = pop3.retr(int(num))
            #  the message lines are in message [1]
            str_message = ""
            for ligne in message[1]:
                #  line is a sequence of bytes decoded as a string
                str_message += f"{ligne.decode()}\r\n"
            #  message folder
            dir3 = f"{dir2}/message_{num}"
            #  if the folder doesn't exist, we create it
            if not os.path.isdir(dir3):
                os.mkdir(dir3)
            #  object email.message.Message
            save_message(dir3, email.message_from_string(str_message), 0)
            #  one more mail
            imail += 1
            #  have we reached the max?
            fini = imail == maxmails or imail == nb_mails

        #  cmde QUIT
        pop3.quit()
    finally:
        #  locking connection
        if pop3:
            pop3.close()

Kommentare

  • Zeilen 6–7: Wir importieren die Funktion [mail_parser.save_message], die in Zeile 80 verwendet wird;
  • Der Code der Funktion ist in einem try (Zeile 22)/finally (Zeile 88) gekapselt. Auf diese Weise werden alle Ausnahmen an den Hauptcode weitergeleitet, der sie abfängt und anzeigt;
  • Zeilen 11–18: Wir rufen die Konfigurationsinformationen des Postfachs ab;
  • Zeilen 23–33: Alle Nachrichten werden im Ordner [output/user] gespeichert, wobei [output] und [user] in der Konfiguration definiert sind. Wir erstellen daher zuerst den Ordner [output], gefolgt vom Ordner [output/user]. Um letzteren zu erstellen, löschen wir ihn zunächst in Zeile 31. [shutil] ist ein Modul, das importiert werden muss. [shutil.rmtree(dir)] löscht den Ordner [dir] und dessen gesamten Inhalt;
  • für alle Operationen an Systemdateien verwenden wir das Modul [os], das ebenfalls importiert werden muss;
  • Zeilen 34–38: Wir stellen eine Verbindung zum POP3-Server her. Wenn der Server sicher ist, verwenden wir die Klasse [poplib.POP3_SSL]; andernfalls die Klasse [poplib.POP3]. Das in Zeile 35 verwendete Attribut [ssl] stammt aus der Postfachkonfiguration;
  • Zeile 45: Festlegen der Protokollierungsstufe:
    • 0: keine Protokolle;
    • 1: Vom POP3-Client gesendete Befehle werden protokolliert;
    • 2: detaillierte Protokolle. Wir können auch sehen, was der POP3-Client empfängt;
  • Zeile 47: Nach dem Verbindungsaufbau sendet der POP3-Server eine Willkommensnachricht. Wir lesen diese Nachricht;
  • Zeilen 48–49: USER-Befehl des POP3-Protokolls;
  • Zeilen 50–51: PASS-Befehl des POP3-Protokolls;
  • Zeilen 52–53: LIST-Befehl des POP3-Protokolls. Die Antwort ist ein Tupel (Antwort, ['Nachrichtennummer Bytes'…], Bytes), zum Beispiel list = (b'+OK 3 Nachrichten (3859 Bytes)', [b'1 584', b'2 550', b'3 2725'], 22). Wir sehen, dass die ersten beiden Elemente des Tupels Bytes sind (mit dem Präfix b). list[1] ist ein Array, in dem jedes Element eine Folge von Bytes ist, die zwei Informationen enthält: die Nachrichtennummer und ihre Größe in Bytes;
  • Zeile 56: Aus dem Vorstehenden lässt sich ableiten, dass die Anzahl der Nachrichten im Postfach über [len[list1]] ermittelt werden kann;
  • Zeilen 59–84: Wir durchlaufen jede Nachricht in einer Schleife. Wir hören auf, wenn alle gelesen wurden oder wenn wir die in der Konfiguration festgelegte maximale Anzahl an E-Mails erreicht haben;
  • Zeile 61: aktuelles Element des Arrays list[1], also etwa b'1 584', eine Bytefolge;
  • Zeile 63: Wir wandeln die Bytefolge in eine Zeichenkette um. Wir haben nun die Zeichenkette '1 584';
  • Zeile 66: Abrufen der Nachrichtennummer, hier die Zeichenkette '1';
  • Zeile 68: Wir senden den POP3-Befehl RETR. Wir erhalten eine Antwort wie:

[message=(b'+OK 584 octets', [b'Return-Path: guest@localhost', b'Received: from [127.0.0.1] (localhost [127.0.0.1])', b'\tby DESKTOP-528I5CU with ESMTPA', b'\t; Tue, 17 Mar 2020 09:41:50 +0100', b'To: guest@localhost', b'From: "guest@localhost" <guest@localhost>', b'Subject: test', b'Message-ID: <2572d0f0-5b7c-2c31-5a70-c628293d5709@localhost>', b'Date: Tue, 17 Mar 2020 09:41:48 +0100', b'User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101', b' Thunderbird/68.6.0', b'MIME-Version: 1.0', b'Content-Type: text/plain; charset=utf-8; format=flowed', b'Content-Transfer-Encoding: 8bit', b'Content-Language: fr', b'', b'h\xc3\xa9l\xc3\xa8ne est all\xc3\xa9e au march\xc3\xa9 acheter des l\xc3\xa9gumes.', b''], 614)]
  • (Fortsetzung)
    • message ist ein Tupel aus drei Elementen;
    • message[1] ist ein Array von Zeilen. Jede Zeile ist eine Folge von Bytes (mit dem Präfix 'b'). Die vollständige Nachricht wird durch diese Reihe von Zeilen gebildet;
    • [Return-Path, Received, To, Subject, Message-ID, Content-Type, Content-Transfer-Encoding, Content-Language] sind die Nachrichten-Header. Jeder liefert Informationen über die empfangene Nachricht. Diese Informationen werden verwendet, um den Nachrichtentext (das vorletzte Element des Arrays message[1]) abzurufen;
  • Zeilen 71–73: Wir erstellen die Zeichenkette [strMessage], die aus allen Zeilen der Nachricht besteht. Wir haben die Nachricht nun in Form einer Zeichenkette. Diese Nachricht kann sowohl andere Nachrichten als auch Anhänge enthalten. Das liegt daran, dass Anhänge als Zeichenketten gespeichert werden. Ein wichtiger Punkt, den man sich merken sollte, ist also, dass eine E-Mail zunächst eine Zeichenkette ist, und genau diese Zeichenkette muss analysiert werden, um die Anhänge, etwaige andere eingebettete Nachrichten und natürlich den Nachrichtentext – also das, was der Absender geschrieben hat – zu extrahieren;
  • Zeilen 74–78: Wir speichern den Nachrichtentext und die Anhänge der Nachricht im Ordner [dir3];
  • Zeilen 79–80: Wir delegieren die Analyse der Nachricht an eine Funktion [save_message]:
    • Der erste Parameter ist [dir3], der Ordner, in dem der Nachrichteninhalt gespeichert werden soll;
    • der zweite Parameter ist vom Typ [email.message.Message]. Dieses Objekt verfügt über Methoden zum Abrufen der verschiedenen Teile der Nachricht (Text, Anhänge) sowie aller ihrer Kopfzeilen. Sie müssen das Modul [email] importieren, um auf dieses Objekt zugreifen zu können. Die Funktion [email.message_from_string] ermöglicht es Ihnen, aus der Zeichenkette der Nachricht ein [email.message.Message]-Objekt zu erstellen;

Die Funktion [save_message] ist Teil des Moduls [mail_parser]:

Image

Das Modul [mail_parser] wurde in den Zeilen 6–7 der Funktion [readmails] importiert;

In [mail_parser.py] lautet die Funktion [save_message] wie folgt:


# imports
import codecs
import email.contentmanager
import email.header
import email.iterators
import email.message
import os
 
 
# sauvegarde d'un message de type email.message.Message
# cette fonction peut être appelée de façon récursive
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # output : dossier de sauvegarde des messages
    # email_message : le message à sauvegarder
    # irfc822 : n° courant de la numérotation des mails attachés
    #
    # partie du message
    part = email_message
    # les entêtes [From, To, Subject] sont trouvés dans une des parties multipart
    # ou bien dans une partie [text/*] lorsqu'il n'y a pas de partie [multipart]
    keys = part.keys()
    # From doit faire partie des entêtes, sinon la partie n'a pas les entêtes qu'on cherche
    if "From" in keys:
        # on récupère certains entêtes
        headers = [f"From: {decode_header(part.get('From'))}",
                   f"To: {decode_header(part.get('To'))}",
                   f"Subject: {decode_header(part.get('Subject'))}",
                   f"Return-Path: {decode_header(part.get('Return-Path'))}",
                   f"User-Agent: {decode_header(part.get('User-Agent'))}",
                   f"Date: {decode_header(part.get('Date'))}"]
        # sauvegarde des entêtes dans un fichier texte
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # écriture dans fichier
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")
 
    # type de la partie [part]
    main_type = part.get_content_maintype()

Kommentare

  • Zeile 12: Die Funktion nimmt bis zu drei Parameter entgegen:
  • [output]: der Ordner, in dem die Nachricht gespeichert werden soll (2. Parameter);
  • [email_message]: eine Nachricht vom Typ [email.message.Message]. Dies ist ein strukturierter Typ. Er enthält den E-Mail-Text sowie alle angehängten Dateien und bietet Methoden zum Abrufen seiner verschiedenen Elemente;
  • [irfc822]: Dieser Parameter dient zur Nummerierung der in [email_message] gekapselten E-Mails;
  • Zeile 18: Das Objekt [email_message] wird in [part] abgelegt. Der Typ [email.message.Message] enthält Teile [part] (Nachrichtentext, Anhänge, gekapselte E-Mails), die ebenfalls vom Typ [email.message.Message] sind. Jeder [part] kann Unterteile haben. Somit ist der Typ [email.message.Message] ein Baum aus Elementen vom Typ [email.message.Message]:
    • [part.ismultipart()] ist [True], wenn der Teil [part] Unterteile enthält. Diese sind dann über [part.get_payload()] verfügbar;
    • Wenn [part.ismultipart()] [False] ist, bedeutet dies, dass wir einen Blattknoten im ursprünglichen Nachrichtenbaum erreicht haben: Dies kann sein:
      • der Nachrichtentext in Form von Klartext;
      • der Nachrichtentext in Form von HTML-Text;
      • ein Anhang (mit Ausnahme einer gekapselten Nachricht, für die [part.ismultipart()] [True] ist);
  • Aufgrund der baumartigen Struktur des Parameters [email.message.Message] wird die Funktion [save_message] rekursiv aufgerufen. Die Rekursion endet, wenn die Blätter des Baums erreicht sind, d. h. ein Teil [part], für den [part.ismultipart()] [False] ist;
  • Zeile 21: Wir fordern die Schlüssel (oder Header) der aktuell analysierten Nachricht an (die aufgrund der Rekursion ein Teil der ursprünglichen Nachricht sein kann);
  • Zeilen 23–35: Wir möchten die Header aufzeichnen:
    • [From]: der Absender der Nachricht;
    • [To]: der Empfänger der Nachricht;
    • [Subject]: der Betreff der Nachricht;
    • [Return-Path]: der Empfänger, an den eine Antwort gesendet werden soll, falls eine Antwort gewünscht wird. Tatsächlich ist diese Information nicht immer im Feld [From] enthalten;
    • [User-Agent]: der POP3-Client, der mit dem POP3-Server kommuniziert;
    • [Date]: das Datum, an dem die E-Mail gesendet wurde;
  • Zeile 23: Nur einer der Nachrichtenteile enthält diese Header. Für die anderen Teile wird der Code in den Zeilen 23–35 ignoriert;
  • Zeilen 25–30: Wir erstellen eine Liste mit den sechs Headern;
  • Zeile 25: Analysieren wir den ersten Header:
    • [part.get(key)] ruft den mit dem Schlüssel [key] verknüpften Header ab;
    • Dieser Header kann kodiert sein. Wenn die Kodierung nicht UTF-8 ist, wird der Header dekodiert und mithilfe der Funktion [decode_header] in UTF-8 neu kodiert;
    • der erste Header hat die Form [From: pymail2lexemple@gmail.com];
  • Zeilen 31–35: Die Header werden in der Datei [output/headers.txt] gespeichert;

Die Funktion [decode_header] lautet wie folgt (ebenfalls in [mail_parser.py]):

#  decoding headers
def decode_header(header: object) -> str:
    #  decode the header
    header = email.header.decode_header(f"{header}")
    #  the result is an array - here it will have only one element of type (header, encoding)
    #  if encoding==None, then header is a string
    #  otherwise it's a list of bytes encoded by encoding
    header, encoding = header[0]
    if not encoding:
        #  if no encoding
        return header
    else:
        #  if encoded, we decode
        return header.decode(encoding)

Kommentare

  • Zeile 4: Dekodieren des Headers:
    • Sie müssen das Modul [email.header] importieren;
    • Wir erhalten eine Liste von Tupeln [(Header1, Kodierung1), (Header2, Kodierung2), ...];
    • für die Header [From, To, Subject, Return-Path, Date] enthält die Liste nur ein Element;
    • Zeile 8: den einzelnen Header und seine Kodierung abrufen:
      • wenn [encoding == None], dann ist [header] der Header als Zeichenkette;
      • ansonsten ist [header] eine Folge von Bytes, die den kodierten Header darstellen;
  • Zeilen 10–11: Wenn keine Kodierung vorhanden war, geben wir den Header zurück;
  • Zeilen 12–14: Wenn eine Kodierung vorhanden war, dekodieren wir die abgerufene Bytefolge in eine Zeichenkette und geben sie zurück;

Kehren wir zur Funktion [save_message] zurück:

#  save a message of type email.message.Message
#  this function can be called recursively
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    #  output: message backup folder
    #  email_message: the message to be saved
    #  irfc822: current numbering of attached e-mails
    #
    #  part of the message
    part = email_message
    #  the [From, To, Subject] headers are found in one of the multipart parts
    #  or in a [text/*] part when there is no [multipart] part
    keys = part.keys()
    #  From must be part of the headers, otherwise the game won't have the headers you're looking for
    if "From" in keys:
        #  some headers are recovered
        headers = [f"From: {decode_header(part.get('From'))}",
                   f"To: {decode_header(part.get('To'))}",
                   f"Subject: {decode_header(part.get('Subject'))}",
                   f"Return-Path: {decode_header(part.get('Return-Path'))}",
                   f"User-Agent: {decode_header(part.get('User-Agent'))}",
                   f"Date: {decode_header(part.get('Date'))}"]
        #  save headers in a text file
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            #  writing to file
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    #  type of part [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    #  if the message is of type text/plain
    if type_of_part == "text/plain":
        #  text message
        save_textmessage(output, part, 0)

    #  if the message is of type text/html
    elif type_of_part == "text/html":
        #  message HTML
        save_textmessage(output, part, 1)

    #  if the message is a container of parts
    elif part.is_multipart():
        
    else:
        
    #  ignore other parts (not text/plain, not text/html, not attachment)
    #  return the current value of irfc822 (numbering of attached e-mails stored in the output folder)
    return irfc822

Kommentare

  • Zeilen 1–26: Wir haben die Header der ursprünglichen Nachricht verarbeitet;
  • Zeilen 28–31: Teile einer Nachricht vom Typ [email.message.Message] haben einen Haupttyp und einen Untertyp. Wir rufen diese ab;
  • Zeilen 32–35: Wenn der verarbeitete Teil vom Typ [text/plain] ist, haben wir einen Blattknoten im ursprünglichen Nachrichtenbaum erreicht. Dies ist der Text, den der Absender in seine Nachricht geschrieben hat;
  • Zeile 35: Dieser Text wird in eine Datei geschrieben:
    • Der erste Parameter [output] ist der Ordner, in dem der Text gespeichert werden soll;
    • der zweite Parameter ist der Teil der Nachricht, der den zu speichernden Text enthält;
    • der dritte Parameter ist 0 zum Speichern von Klartext, 1 für HTML-Text;
  • Zeilen 37–40: Wenn der Teil vom Typ [text/html] ist, haben wir ebenfalls ein Blatt im ursprünglichen Nachrichtenbaum erreicht. Dies ist der Text, den der Absender in seiner Nachricht geschrieben hat, diesmal im HTML-Format. Nicht alle E-Mail-Clients unterstützen dieses Format;

Die Funktion [save_textmessage] funktioniert wie folgt:

#  saving a text message
def save_textmessage(output: str, part: email.message.Message, type_of_text: int):
    #  headers
    headers = []
    #  message charset
    charset = part.get_content_charset()
    if charset is not None:
        charset = part.get_content_charset().lower()
        headers.append(f"Charset: {charset}")
    #  content coding mode
    content_transfer_encoding = part.get("Content-Transfer-Encoding")
    if content_transfer_encoding is not None:
        headers.append(f"Transfer-Content-Encoding: {content_transfer_encoding}")
    #  8bit mode was a problem
    if content_transfer_encoding == "8bit":
        #  retrieve the mail message
        msg = part.get_payload()
    else:
        #  retrieve the mail message
        msg = email.contentmanager.raw_data_manager.get_content(part)
    #  by text type
    filename = None
    if type_of_text == 0:
        #  save headers
        with codecs.open(f"{output}/headers.txt", "a", "utf-8") as file:
            #  writing to file
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")
        #  text file for content
        filename = f"{output}/mail.txt"
    elif type_of_text == 1:
        #  html file for content
        filename = f"{output}/mail.html"
    #  save message
    with codecs.open(filename, "w", "utf-8") as file:
        #  writing to file
        file.write(msg)

Kommentare

  • Wie die Kopfzeilen kann auch der Nachrichtentext kodiert sein. Es gibt zwei mögliche Kodierungen:
    • die ursprüngliche Kodierung des Textes (UTF-8, ISO-8859-1 usw.). Dies ist die Kodierung, die vom Mailserver verwendet wurde, der die Nachricht gesendet hat. Sie ist aus dem [Content-Type]-Header der empfangenen Nachricht ersichtlich;
    • eine zweite Kodierung, der der Originaltext möglicherweise unterzogen wurde, um gesendet zu werden. Diese ist aus dem [Transfer-Content-Encoding]-Header der empfangenen Nachricht ersichtlich;
  • Zeile 6: die ursprüngliche Kodierung des Textes;
  • Zeile 11: die zweite Kodierung, die der Text für die Übertragung an den Empfänger durchlaufen hat;
  • Zeilen 9, 13: Diese beiden Informationen werden in die Liste [headers] aufgenommen. Sie werden den Informationen in der Datei [headers.txt] hinzugefügt, in der bestimmte Nachrichten-Header gespeichert sind;
  • Zeile 20: [email.contentmanager.raw_data_manager.get_content] ruft die Nachricht mit ihrer ursprünglichen Kodierung 1 ab. Wir haben die Kodierung 2 entfernt. Das Objekt [email.contentmanager.raw_data_manager] unterstützt jedoch nur zwei Arten von [Transfer-Content-Encoding]:
    • [quoted-printable];
    • [base64];

Die anderen werden ignoriert. Thunderbird verwendet jedoch beispielsweise die [Transfer-Content-Encoding] mit dem Namen „8bit“. Diese Kodierung wird ignoriert, und Nachrichten, die Zeichen mit Akzenten enthalten, werden verstümmelt. Die Nachricht kann dann mit der Methode [part.get_payload()] abgerufen werden (Zeilen 15–17);

  • Zeile 21: An dieser Stelle haben wir die Nachricht ohne ihre Übertragungskodierung, d. h. die Nachricht so, wie sie vom Absender geschrieben wurde;
  • Zeilen 22–37: Dies ist der Fall, in dem wir eine Textnachricht speichern müssen;
    • Zeilen 24–28: Wir speichern die beiden in den Zeilen 9 und 13 erstellten Header in der Datei [headers.txt]. Diese Datei existiert bereits und enthält Header. Daher verwenden wir den Modus „a“ (Zeile 25), um diese Datei zu öffnen. „a“ steht für „append“ (anhängen), und die neuen Header werden (am Ende der Datei) an den bestehenden Inhalt der Datei [headers.txt] angehängt;
    • Zeile 30: Der Name der Datei, in der die Textnachricht gespeichert werden soll;
    • Zeile 33: Der Name der Datei, in der die HTML-Nachricht gespeichert werden soll;
    • Zeilen 34–37: Der UTF-8-Text wird in einer Datei gespeichert;

Kommen wir zurück zur Funktion [save_message]:

#  save a message of type email.message.Message
#  this function can be called recursively
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    #  output: message backup folder
    #  email_message: the message to be saved
    #  irfc822: current numbering of attached e-mails
    #
    #  part of the message
    part = email_message
    #  the [From, To, Subject] headers are found in one of the multipart parts
    #  or in a [text/*] part when there is no [multipart] part
    keys = part.keys()
    #  From must be part of the headers, otherwise the game won't have the headers you're looking for
    if "From" in keys:
        #  some headers are recovered
        headers = [f"From: {decode_header(part.get('From'))}",
                   f"To: {decode_header(part.get('To'))}",
                   f"Subject: {decode_header(part.get('Subject'))}",
                   f"Return-Path: {decode_header(part.get('Return-Path'))}",
                   f"User-Agent: {decode_header(part.get('User-Agent'))}",
                   f"Date: {decode_header(part.get('Date'))}"]
        #  save headers in a text file
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            #  writing to file
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    #  type of part [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    #  if the message is of type text/plain
    if type_of_part == "text/plain":
        #  text message
        save_textmessage(output, part, 0)

    #  if the message is of type text/html
    elif type_of_part == "text/html":
        #  message HTML
        save_textmessage(output, part, 1)

    #  if the message is a container of parts
    elif part.is_multipart():
        #  special case of attached mail
        if type_of_part == "message/rfc822":
            #  create a new output2 folder for attached mail
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            #  save irfc822 message subparts in output2
            for subpart in part.get_payload():
                #  in the new irfc822 folder restarts at 0
                save_message(output2, subpart, 0)

        else:
            #  we're not dealing with an attached e-mail
            #  save sub-sections in current folder output
            #  irfc822 must then be incremented for each message/rfc822 subpart
            for subpart in part.get_payload():
                #  save_message returns the last value of irfc822
                #  incremented by 1 if subpart="message/rfc822", not incremented otherwise
                irfc822 = save_message(output, subpart, irfc822)
    else:
        #  other cases (not text/plain, not text/html, not multipart)
        #  attachment?
        disposition = part.get('Content-Disposition')
        if disposition and disposition.startswith('attachment'):
            save_attachment(output, part)
    #  ignore other parts (not text/plain, not text/html, not attachment)
    #  return the current value of irfc822 (numbering of attached e-mails stored in the output folder)
    return irfc822

Kommentare

  • Zeilen 33–40: Wir haben zwei mögliche Fälle für eine Nachricht am Ende des ursprünglichen Nachrichtenbaums behandelt (keine Unterteile). Es bleiben noch zwei Fälle zu behandeln:
    • Zeilen 43–62: den Fall, in dem der analysierte Teil selbst Unterteile enthält (part.ismultipart() == True);
    • Zeilen 63–68: Für die verbleibenden Fälle behandeln wir nur den Fall, in dem der analysierte Teil ein Anhang ist;

Wir behandeln diesen letzten Fall. Wir befinden uns wieder am Ende der ursprünglichen Nachricht (keine Unterteile). Wir sind bereits auf zwei Fälle dieser Art gestoßen: die Typen text/plain und text/html. Wir behandeln nun den Fall der angehängten Datei.

  • Zeile 66: Der Anhang wird anhand des Schlüssels [Content-Disposition] identifiziert;
  • Zeile 67: Wenn dieser Schlüssel vorhanden ist und mit der Zeichenfolge [attachment] beginnt, handelt es sich um eine an die Nachricht angehängte Datei;
  • Zeile 68: Der Anhang wird im Ordner [output] gespeichert;

Die Funktion [save_attachment] lautet wie folgt:

#  safeguarding an attachment
def save_attachment(output: str, part: email.message.Message):
    #  name of attached file
    filename = os.path.basename(part.get_filename())

    #  the file name can be encoded
    #  par exemple =?utf-8?Q?Cours-Tutoriels-Serge-Tah=C3=A9-1568x268=2Ep
    filename = decode_header(filename)
    #  save the attached file
    with open(f"{output}/{filename}", "wb") as file:
        file.write(part.get_payload(decode=True))
  • Zeile 4: Wenn [part] ein Anhang ist, wird der Name der angehängten Datei über [part.get_filename] abgerufen. Es wird nur der Dateiname beibehalten, nicht der Pfad;
  • Zeile 8: Dateinamen werden im Allgemeinen auf die gleiche Weise wie Nachrichten-Header kodiert. Daher verwenden wir die Funktion [decode_header], um sie zu dekodieren;
  • Zeile 11: Der Inhalt der angehängten Datei ist derzeit eine Zeichenkette, die durch die Kodierung (oft Base64) des ursprünglichen Dateiinhalts in Text entsteht. Um diesen ursprünglichen Inhalt abzurufen, verwenden wir die Funktion [part.get_payload(decode=True)]. Der Parameter [decode=True] gibt an, dass der Inhalt der angehängten Datei dekodiert werden muss. Dies ergibt eine Bytefolge;
  • Zeile 10: Diese Bytefolge wird in der Datei [output/filename] gespeichert. Der Modus „wb“ zum Öffnen der Datei steht für „write binary“;

Kehren wir zum Code für die Funktion [save_message] zurück:

def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    #  output: message backup folder
    #  email_message: the message to be saved
    #  irfc822: current numbering of attached e-mails
    #
    #  part of the message
    part = email_message
    #  the [From, To, Subject] headers are found in one of the multipart parts
    #  or in a [text/*] part when there is no [multipart] part
    keys = part.keys()
    #  From must be part of the headers, otherwise the game doesn't have the headers you're looking for
    if "From" in keys:
        #  some headers are recovered
        headers = [f"From: {decode_header(part.get('From'))}",
                   f"To: {decode_header(part.get('To'))}",
                   f"Subject: {decode_header(part.get('Subject'))}",
                   f"Return-Path: {decode_header(part.get('Return-Path'))}",
                   f"User-Agent: {decode_header(part.get('User-Agent'))}",
                   f"Date: {decode_header(part.get('Date'))}"]
        #  save headers in a text file
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            #  writing to file
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    #  type of part [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    #  if the message is of type text/plain
    if type_of_part == "text/plain":
        #  text message
        save_textmessage(output, part, 0)

    #  if the message is of type text/html
    elif type_of_part == "text/html":
        #  message HTML
        save_textmessage(output, part, 1)

    #  if the message is a container of parts
    elif part.is_multipart():
        #  special case of attached mail
        if type_of_part == "message/rfc822":
            #  create a new output2 folder for attached mail
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            #  save irfc822 message subparts in output2
            for subpart in part.get_payload():
                #  in the new irfc822 folder restarts at 0
                save_message(output2, subpart, 0)

        else:
            #  we're not dealing with an attached e-mail
            #  save sub-sections in current folder output
            #  irfc822 must then be incremented for each message/rfc822 subpart
            for subpart in part.get_payload():
                #  save_message returns the last value of irfc822
                #  incremented by 1 if subpart="message/rfc822", not incremented otherwise
                irfc822 = save_message(output, subpart, irfc822)
    else:
        #  other cases (not text/plain, not text/html, not multipart)
        #  attachment?
        disposition = part.get('Content-Disposition')
        if disposition and disposition.startswith('attachment'):
            save_attachment(output, part)
    #  ignore other parts (not text/plain, not text/html, not attachment)
    #  return the current value of irfc822 (numbering of attached e-mails stored in the output folder)
    return irfc822

Kommentare

  • Wir haben die Fälle behandelt, die die Blattknoten des ursprünglichen Nachrichtenbaums betreffen: die Teile [text/plain, text/html und Content-Disposition=attachment;…] Wir müssen noch den Fall behandeln, in dem der analysierte Teil ein Container für Teile ist, d. h. er enthält Unterteile [part.is_multipart()==True], Zeile 41. Um die Endknoten des Nachrichtenbaums zu erreichen, müssen wir daher diese Unterteile analysieren;
  • Zeile 43: Wir behandeln den Fall, in dem der analysierte Teil den Typ [message/rfc822] hat, auf besondere Weise. Dies ist der Typ einer E-Mail. Dies ist also der Fall, in dem eine E-Mail eine andere E-Mail als Anhang enthält;

Der Code lautet wie folgt:


    # si le message est un conteneur de parties
    elif part.is_multipart():
        # cas particulier du mail attaché
        if type_of_part == "message/rfc822":
            # création d'un nouveau dossier output2 pour le mail attaché
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # sauvegarde des sous-parties du message irfc822 dans output2
            for subpart in part.get_payload():
                # dans le nouveau dossier irfc822 redémarre à 0
                save_message(output2, subpart, 0)
 
        else:
            # on n'a pas affaire à un mail attaché
            # sauvegarde des sous-parties dans le dossier courant output
            # irfc822 doit alors être incrémenté pour chaque sous-partie message/rfc822
            for subpart in part.get_payload():
                # save_message rend la dernière valeur de irfc822
                # incrémentée de 1 si subpart="message/rfc822", pas incrémentée sinon
                irfc822 = save_message(output, subpart, irfc822)

    return irfc822
  • Der Unterschied zwischen einem [message/rfc822]-Teil und den anderen Multipart-Teilen besteht darin, dass sich das Speicherverzeichnis ändert;
    • Zeilen 6–8: Für den [message/rfc822]-Teil wird das Speicherverzeichnis zu dem in Zeile 7 [output/rfc822_x], wobei x die Nummer der angehängten E-Mail ist, 1 für die erste, 2 für die zweite …;
    • Zeile 21: Für die anderen Multipart-Teile bleibt das Speicherverzeichnis das [output]-Verzeichnis der ursprünglichen Nachricht. Das Verzeichnis wird nicht geändert;
  • Zeilen 10–12: Jeder Teil wird über einen rekursiven Aufruf von [save_message] gespeichert. Der dritte Parameter ist die Indexnummer der in [subpart] gekapselten E-Mails. Zu Beginn ist dieser Index 0;
  • Zeile 21: gleiche Erklärung wie für Zeile 12, jedoch ändert sich der Wert des dritten Parameters [irfc822]. Befinden sich in der Schleife in den Zeilen 18–21 mehrere gekapselte E-Mails, müssen diese in den Ordnern […/rfc822-1…/rfc822_2…] gespeichert werden. Daher muss der dritte Parameter der Funktion [save_message] die Werte 1, 2, 3 usw. annehmen. Zu diesem Zweck setzt [save_message] den Wert von [irfc822] (Zeile 21).

Nehmen wir ein Beispiel und nehmen an, dass die Liste der Unterteile in Zeile 18 [subpart1, subpart2, subpart3, subpart4, subpart5] lautet und dass [subpart1, subpart3, subpart5] angehängte E-Mails sind, [subpart2] ein Text/Plain-Teil und [subpart4] ein Anhang ist und dass wir in der Nachricht noch auf keine angehängte E-Mail gestoßen sind [irfc822=0]. In diesem Fall:

  • (Fortsetzung)
    • wird [subpart1] in Zeile 21 gespeichert: Die Funktion [saveMessage] wird mit irfc822=0 ausgeführt;
    • [subpart1] ist ein E-Mail-Anhang, daher wird irfc822 auf 1 gesetzt (Zeile 6 des Codes). Ein Ordner [output/irfc822_1] wird erstellt. Der von [saveMessage(output,subpart1,0)] zurückgegebene Wert ist daher 1 (Zeile 23);
    • [subpart2] wird in Zeile 21 gespeichert: Die Funktion [saveMessage] wird mit irfc822=1 ausgeführt;
    • [subpart2] ist kein E-Mail-Anhang. Daher bleibt irfc822 auf 1 gesetzt. Dies ist der in Zeile 21 abgerufene Wert;
    • [subpart3] wird in Zeile 21 gespeichert: Die Funktion [save_message] wird mit irfc822=1 ausgeführt;
    • [subpart3] ist ein E-Mail-Anhang, daher ändert sich irfc822 auf 2 (Zeile 6 des Codes). Ein Ordner [output/irfc822_2] wird erstellt. Der von [save_message(output,subpart1,1)] zurückgegebene Wert ist daher 2 (Zeile 21);
    • [subpart4] wird in Zeile 21 gespeichert: Die Funktion [save_message] wird mit irfc822=2 ausgeführt;
    • [subpart4] ist kein E-Mail-Anhang. Daher bleibt irfc822 bei 2. Dies ist der in Zeile 21 abgerufene Wert;
    • [subpart5] wird in Zeile 21 gespeichert: Die Funktion [save_message] wird mit irfc822=2 ausgeführt;
    • [subpart5] ist ein E-Mail-Anhang, daher ändert sich irfc822 auf 3 (Zeile 6 des Codes). Ein Ordner [output/irfc822_3] wird erstellt. Der von [save_message(output,subpart1,2)] zurückgegebene Wert ist daher 3 (Zeile 21);

Ausführungsbeispiele

Wir senden 4 E-Mails an [pymail2parlexemple@gmail.com] von: [Gmail, Outlook, E-Mail-Client, Thunderbird]

Alle E-Mails haben den Betreff [Hélène geht auf den Markt] und den Text [Gemüse kaufen]. Wir möchten testen, wie Zeichen mit Akzenten wiedergegeben werden.

Wir lesen sie mit dem Skript [pop3/02/main], das mit der folgenden Konfigurationsdatei [pop3/02/config] konfiguriert ist:

import os


def configure() -> dict:
    #  application configuration
    config = {
        #  list of mailboxes to be managed
        "mailboxes": [
            #  server: server POP3
            #  port: server port POP3
            #  user: user whose messages are to be read
            #  password: your password
            #  maxmails: maximum number of e-mails to download
            #  timeout: maximum wait time for a server response
            #  delete: true if downloaded messages are to be deleted from the server
            #  ssl: true if mail is read over a secure link
            #  output: the storage folder for downloaded messages

            {
                "server": "pop.gmail.com",
                "port": "995",
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prD&@1QZ3TG",
                "maxmails": 10,
                "delete": False,
                "ssl": True,
                "timeout": 2.0,
                "output": "output"
            }
        ]
    }
    #  absolute path of script folder
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  absolute paths of folders to be included in the syspath
    absolute_dependencies = [
        #  local file
        f"{script_dir}/../../shared",
    ]

    #  syspath configuration
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  we return the configuration
    return config

Das Ergebnis lautet wie folgt:

Image

Nachricht 1 ist die von Thunderbird gesendete Nachricht:

Image

  • in [5] verwendet Thunderbird [3] einen [Transfer-Content-Encoding] vom Typ [8bit];
  • in [4]: Die Nachricht ist in UTF-8 kodiert;

Nachricht 2 ist die von em Client gesendete Nachricht:

Image

Image

Beachten Sie, dass [em Client] den Text in UTF-8 [4] kodiert und ihn in [quoted-printable] [5] überträgt. Außerdem hat er eine Kopie der Nachricht im HTML-Format [7-8] gesendet. Alle hier getesteten E-Mail-Clients können dies tun. Es handelt sich um eine Konfigurationseinstellung.

Nachricht 3 ist die von Gmail gesendete Nachricht:

Image

Beachten Sie, dass Gmail den Text in UTF-8 [3] kodiert und ihn im Format [quoted-printable] [4] überträgt. In [6] die HTML-Version der Nachricht.

Nachricht 4 ist die von Outlook gesendete Nachricht:

Image

Beachten Sie, dass Outlook den Text in ISO-8859-1 [3] kodiert und ihn im Format [quoted-printable] [4] überträgt.

Die vorangegangenen Beispiele zeigen zwei Dinge:

  • Unser Client [pop3/02] hat ordnungsgemäß funktioniert;
  • E-Mail-Clients haben unterschiedliche Methoden, eine E-Mail zu versenden;

Betrachten wir nun die angehängten Dateien. Mit Thunderbird leeren wir das Postfach des Benutzers [pymail2parlexemple@gmail.com]. Anschließend verwenden wir das Skript [smtp/03/main], um eine E-Mail mit der folgenden Konfiguration [smtp/03/config] zu versenden:

import os


def configure() -> dict:
    #  application configuration
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        #  description: description of the e-mail sent
        #  smtp-server: SMTP server
        #  smtp-port: server port SMTP
        # from : expéditeur
        #  to: recipient
        #  subject : mail subject
        #  message : mail message
        "mails": [
            {
                "description": "mail to gmail via gmail avec smtplib",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymail2parlexemple@gmail.com",
                "to": "pymail2parlexemple@gmail.com",
                "subject": "to gmail via gmail avec smtplib",
                #  we test accented characters
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                #  smtp with authentication
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                #  here, absolute paths must be set for attached files
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                    f"{script_dir}/attachments/mail attaché 1.eml",
                ]
            }
        ]
    }
  • Zeilen 31–33: Wir fügen der E-Mail Folgendes als Anhang bei:
  • eine Word-Datei;
  • eine PDF-Datei;
  • eine E-Mail, die dieselben beiden angehängten Dateien enthält;

Sobald die E-Mail gesendet wurde, führen wir das Skript [pop3/02] aus, um das Postfach des Benutzers [pymail2parlexemple@gmail.com] auszulesen. Die Ergebnisse lauten wie folgt:

Image

  • in [1]: die Nachricht mit ihren beiden angehängten Dateien;
  • in [2]: die angehängte E-Mail selbst mit ihren beiden angehängten Dateien;

Fazit

Das Modul [mail_parser.py] ist besonders komplex. Dies liegt an der Komplexität der E-Mails selbst. Wir werden dieses Modul für das IMAP-Protokoll wiederverwenden.

21.7. Das IMAP-Protokoll

21.7.1. Einführung

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

  • das POP3-Protokoll (Post Office Protocol), historisch gesehen das erste Protokoll, das heute jedoch nur noch selten verwendet wird;
  • das IMAP-Protokoll (Internet Message Access Protocol), das neuer als POP3 ist und derzeit am weitesten verbreitet ist;

Um das IMAP-Protokoll zu untersuchen, verwenden wir die folgende Architektur:

Image

  • [Server B] wird je nach Situation sein:
    • ein lokaler IMAP-Server, der vom Mailserver [hMailServer] bereitgestellt wird;
    • der Server [imap.gmail.com:993], der der IMAP-Server für den E-Mail-Client [Gmail] ist;
  • [Client A] ist ein Python-Skript, das Python-Module verwendet, um Anhänge zu verwalten und bei Bedarf eine verschlüsselte, authentifizierte Verbindung zum IMAP-Server herzustellen;

Das IMAP-Protokoll geht über das POP3-Protokoll hinaus:

  • E-Mails werden auf dem IMAP-Server gespeichert und können in Ordnern organisiert werden;
  • Der IMAP-Client kann Befehle senden, um diese Ordner zu erstellen, zu ändern oder zu löschen;

Schauen wir uns ein Beispiel mit Thunderbird an. In der folgenden Architektur:

Image

  • ist Thunderbird Client A;
  • [imap.gmail.com] ist Server B (Gmail);

Erstellen wir mit Thunderbird einen Ordner in den E-Mails des Benutzers [pymail2parlexemple@gmail.com]:

Image

  • In [1-6] erstellen wir den Ordner [Ordner1];

Image

  • in [7-8] verschieben wir (mit der Maus) alle Dateien aus dem Ordner [Posteingang] in den Ordner [Ordner1];

Melden wir uns nun auf der Gmail-Website an und loggen uns als Benutzer [pymail2parlexemple@gmail.com] ein:

Image

  • In [2-3] ist der Posteingang leer;
  • in [1] der erstellte Ordner [folder1];

Image

  • in [4-6]: die E-Mails, die in den Ordner [Ordner1] verschoben wurden;

Wir betrachten nun die folgende Architektur:

Image

  • Client A ist die Thunderbird-Anwendung;
  • Client C ist die Gmail-Webanwendung;
  • Server B ist der Gmail-IMAP-Server;

Die Ordnerstruktur des Benutzers wird vom IMAP-Server verwaltet. Anschließend synchronisieren sich alle IMAP-Clients damit, um die Ordner des Benutzerkontos anzuzeigen. In diesem Fall hat Thunderbird mehrere Befehle an folgende Adresse gesendet:

  • den Ordner [folder1] zu erstellen;
  • Nachrichten in diesen Ordner zu verschieben;

21.7.2. Skript [imap/main]: IMAP-Client mit dem Modul [imaplib]

Image

Das Skript [imap/main] wird durch das folgende Skript [imap/config] konfiguriert:

import os


def configure() -> dict:
    #  application configuration
    config = {
        #  list of mailboxes to be managed
        "mailboxes": [
            #  server: server IMAP
            #  port: server port IMAP
            #  user: user whose messages are to be read
            #  password: your password
            #  maxmails: maximum number of e-mails to download
            #  timeout: maximum wait time for a server response
            #  delete: true if downloaded messages are to be deleted from the server
            #  ssl: true if mail is read over a secure link
            #  output: the storage folder for downloaded messages

            {
                "server": "imap.gmail.com",
                "port": "993",
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                "maxmails": 10,
                "ssl": True,
                "timeout": 2.0,
                "output": "output"
            }
        ]
    }
    #  absolute path of script folder
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  absolute paths of folders to be included in the syspath
    absolute_dependencies = [
        #  local file
        f"{script_dir}/../shared",
    ]

    #  syspath configuration
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  we return the configuration
    return config

Kommentare

  • Zeilen 8–29: Der Schlüssel [mailboxes] ist mit der Liste der zu überprüfenden Postfächer verknüpft;
  • Zeile 20: der IMAP-Server;
  • Zeile 21: dessen Dienstport;
  • Zeilen 22–23: der Benutzer, dessen E-Mails Sie lesen möchten;
  • Zeile 24: die maximale Anzahl der abzurufenden E-Mails;
  • Zeile 25: gibt an, ob eine sichere Verbindung zum IMAP-Server hergestellt werden soll (True) oder nicht (False);
  • Zeile 26: die maximale Wartezeit für eine Antwort vom Server;
  • Zeile 27: Ordner zum Speichern gelesener E-Mails;

Das Skript [imap/main] lautet wie folgt:

#  imports
import email
import imaplib
import os
import shutil


# -----------------------------------------------------------------------

def readmails(mailbox: dict):
    


#  main ----------------------------------------------------------------
#   IMAP client for reading e-mails

#  retrieve application configuration
import config
config = config.configure()

#  we process mailboxes one by one
for mailbox in config['mailboxes']:
    try:
        #  console display
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']} / {mailbox['server']}:{mailbox['port']}")
        #  mailbox reading
        readmails(mailbox)
        #  end
        print("Lecture terminée...")
    #  except BaseException as error:
    #      # error is displayed
    #      print(f "The following error has occurred: {error}")
    finally:
        pass

Kommentare

  • Zeilen 14–36: Wir sehen denselben Ansatz, der im Skript |pop3/02/main| verwendet wird;

Die Funktion [readmails] sieht wie folgt aus:

def readmails(mailbox: dict):
    #  we let the exceptions rise
    #
    #  mail parser module
    from mail_parser import save_message

    #  retrieve configuration information
    output = mailbox['output']
    user = mailbox['user']
    password = mailbox['password']
    timeout = mailbox['timeout']
    server = mailbox['server']
    port = int(mailbox['port'])
    maxmails = mailbox['maxmails']
    ssl = mailbox['ssl']
    #
    #  here we go
    imap_resource = None
    try:
        #  create storage folders if they don't exist
        if not os.path.isdir(output):
            os.mkdir(output)
        #  user
        dir2 = f"{output}/{user}"
        #  delete the [dir2] folder if it exists, then recreate it
        if os.path.isdir(dir2):
            #  delete
            shutil.rmtree(dir2)
        #  creation
        os.mkdir(dir2)
        #  server connection IMAP
        if ssl:
            imap_resource = imaplib.IMAP4_SSL(server, port)
        else:
            imap_resource = imaplib.IMAP4(server, port)
        #  customer communication timeout
        sock = imap_resource.socket()
        sock.settimeout(timeout)
        #  authentication
        imap_resource.login(user, password)
        #  select folder INBOX (incoming mail)
        imap_resource.select('INBOX')
        #  retrieve all messages in this folder: criterion ALL
        #  no special encoding: None
        typ1, data1 = imap_resource.search(None, 'ALL')
        #  print(f"typ={typ1}, data={data1}")

        #  data1[0] is an array of bytes containing the numbers of all messages separated by a space
        nums = data1[0].split()
        imail = 0
        fini = imail >= maxmails or imail >= len(nums)
        #  we read your e-mails one by one
        while not fini:
            #  num is a message number in binary
            num = nums[imail]
            #  print(f "message n° {num}")

            #  retrieve msg n° num
            typ2, data2 = imap_resource.fetch(num, '(RFC822)')
            #  print(f"type={typ2}, data={data2}")

            #  data is a list containing tuples, in this case a single tuple
            #  data[0] is the tuple, data[0][1] is the second element of the tuple
            #  data[0][1] contains a sequence of bytes representing all the lines in the message
            #  message means message text + all attached files

            #  the message is retrieved as type email.message.Message
            message = email.message_from_bytes(data2[0][1])
            #  message folder
            dir3 = f"{dir2}/message_{int(num)}"
            #  if the folder doesn't exist, we create it
            if not os.path.isdir(dir3):
                os.mkdir(dir3)
            #  save it
            save_message(dir3, message)
            #  next message
            imail += 1
            fini = imail >= maxmails or imail >= len(nums)
    finally:
        if imap_resource:
            #  close the mailbox connection
            imap_resource.close()
            #  disconnect from server IMAP
            imap_resource.logout()

Kommentare

  • Zeilen 7–15: Abrufen der Konfigurationseinstellungen;
  • Zeilen 19, 79: Der Code wird durch einen try/finally-Block gesteuert. Ausnahmen werden daher nicht abgefangen (keine except-Klausel), sodass sie an den aufrufenden Code weitergeleitet werden, der sie abfängt und anzeigt;
  • Zeilen 23–30: Erstellen des Ordners zum Speichern von E-Mails;
  • Zeilen 31–35: Wir stellen eine Verbindung zum IMAP-Server her. Die verwendete Klasse hängt davon ab, ob es sich um einen sicheren IMAP-Server (IMAP4_SSL) handelt oder nicht (IMAP4);
  • Zeilen 36–38: Legen Sie das Zeitlimit für die Client-Server-Kommunikation fest;
  • Zeilen 39–40: Authentifizierung beim IMAP-Server;
  • Zeilen 41–42: Wir haben gesehen, dass das Postfach eines IMAP-Benutzers in Ordner unterteilt werden kann. Der Ordner [INBOX] ist für eingehende E-Mails vorgesehen. Um den Ordner [folder1] auszuwählen, würden wir [imapResource.select('folder1')] schreiben;
  • Zeilen 43–45: Wir fordern die Liste aller im [INBOX] gefundenen Nachrichten an:
    • Der erste Parameter von [imapResource.search] ist ein Kodierungstyp. [None] bedeutet „kein Kodierungsfilter“;
    • der zweite Parameter ist ein Kriterium. Es gibt verschiedene Möglichkeiten, dies auszudrücken. Das Kriterium [ALL] bedeutet, dass wir alle Nachrichten im Ordner wollen;

Das Ergebnis von [imapResource.search] sieht wie folgt aus:


typ=OK, data=[b'1 2']

[data] ist eine Liste, die die Nummern der abgerufenen Nachrichten enthält. Diese sind binär codiert. Im obigen Beispiel wurden zwei Nachrichten im Ordner [INBOX] gefunden;

  • Zeile 49: Wir rufen die Nachrichten-IDs ab. Oben haben wir die Liste [b'1' b'2'], eine Liste von Zahlen, die binär kodiert sind;
  • Zeilen 53–78: Wir durchlaufen den Ordner [INBOX], um die Nachrichten auszulesen;
  • Zeilen 54–55: Nachrichtennummer;
  • Zeilen 58–59: Die Nachricht Nr. [num] wird vom IMAP-Server angefordert;
    • Der erste Parameter ist die Nummer der gewünschten Nachricht;
    • der zweite Parameter ist eine Zeichenkette „(part1)(part2)…“, wobei [part] der Name eines Teils der Nachricht ist. Ich habe mich damit noch nicht im Detail befasst. Der Name (RFC822) bezieht sich auf die gesamte E-Mail;

Wir erhalten etwas im folgenden Format:


type=OK, data=[(b'1 (RFC822 {614}', b'Return-Path: guest@localhost\r\nReceived: from [127.0.0.1] (localhost [127.0.0.1])\r\n\tby DESKTOP-528I5CU with ESMTPA\r\n\t; Tue, 17 Mar 2020 09:41:50 +0100\r\nTo: guest@localhost\r\nFrom: "guest@localhost" <guest@localhost>\r\nSubject: test\r\nMessage-ID: <2572d0f0-5b7c-2c31-5a70-c628293d5709@localhost>\r\nDate: Tue, 17 Mar 2020 09:41:48 +0100\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101\r\n Thunderbird/68.6.0\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8; format=flowed\r\nContent-Transfer-Encoding: 8bit\r\nContent-Language: fr\r\n\r\nh\xc3\xa9l\xc3\xa8ne est all\xc3\xa9e au march\xc3\xa9 acheter des l\xc3\xa9gumes.\r\n\r\n'), b')']

Das Element [data] ist hier eine Liste mit einem Element, und dieses einzelne Element ist ein Tupel aus drei Elementen:


data = [
    (b'1 (RFC822 {614}',
     b'Return-Path: guest@localhost\r\nReceived: from [127.0.0.1] (localhost [127.0.0.1])\r\n\tby DESKTOP-528I5CU with ESMTPA\r\n\t; Tue, 17 Mar 2020 09:41:50 +0100\r\nTo: guest@localhost\r\nFrom: "guest@localhost" <guest@localhost>\r\nSubject: test\r\nMessage-ID: <2572d0f0-5b7c-2c31-5a70-c628293d5709@localhost>\r\nDate: Tue, 17 Mar 2020 09:41:48 +0100\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101\r\n Thunderbird/68.6.0\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8; format=flowed\r\nContent-Transfer-Encoding: 8bit\r\nContent-Language: fr\r\n\r\nh\xc3\xa9l\xc3\xa8ne est all\xc3\xa9e au march\xc3\xa9 acheter des l\xc3\xa9gumes.\r\n\r\n'),
    b')'
]

Das zweite Element dieses Tupels ist eine binäre Zeichenkette, die die gesamte angeforderte Nachricht darstellt. Wir erkennen hier Elemente, die bereits bei der Betrachtung des Moduls [mail_parser] vorgestellt wurden.

data[0] stellt ein Tupel mit zwei Elementen dar. data[0][1] stellt die Nachrichtenzeilen in binärer Form dar.

  • Zeile 68: Die Funktion [email.message_from_bytes(data2[0][1])] erstellt aus den Nachrichtenzeilen ein Objekt vom Typ [email.message.Message]. Der Typ [email.message.Message] ist der Typ des Parameters des Moduls [mail_parser], das wir zuvor geschrieben haben;
  • Zeilen 69–73: Wir erstellen den Speicherordner für die Nachricht Nr. [num];
  • Zeile 75: Wir rufen die Funktion [save_message] aus dem Modul [mail_parser] in Zeile 5 auf. Diese Funktion wurde im Abschnitt |pop3/02/main| beschrieben;
  • Zeilen 76–78: Schleife zurück, um die nächste Nachricht zu verarbeiten;
  • Zeilen 79–84: Unabhängig davon, ob ein Fehler aufgetreten ist oder nicht:
    • Zeile 82: Schließen der Verbindung zum abgefragten Ordner;
    • Zeile 84: Wir trennen die Verbindung zum IMAP-Server;

Die erhaltenen Ergebnisse sind identisch mit denen, die mit dem Skript [pop3/02/main] erzielt wurden. Dies ist normal, da derselbe Mail-Parser [mail_parser] verwendet wird.