Skip to content

21. Internetfunktionen

Wir wenden uns nun den Internetfunktionen von Python zu, die uns die Programmierung von TCP / IP (Transfer Control Protocol / Internet Protocol) ermöglichen.

Image

21.1. Grundlagen der Internet-Programmierung

21.1.1. Allgemeines

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

Image

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

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

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

21.1.2. Die Merkmale des Protokolls TCP

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

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

21.1.3. Die Client-Server-Beziehung

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

21.1.4. Architektur eines Clients

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

ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
    tant que ce n'est pas fini
        préparer une demande
        l'émettre vers la machine B
        attendre et récupérer la réponse
        la traiter
    fin tant que
finsi
fermer la connexion

21.1.5. Architektur eines Servers

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

Eine Dienst-Task hat folgende Struktur:

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. Entdecken Sie die Kommunikations -Protokolle des Internets

21.2.1. Einführung

Wenn ein Client eine Verbindung zu einem Server hergestellt hat, kommt es zu einem Dialog zwischen beiden. Die Art dieses Dialogs bildet das sogenannte Kommunikationsprotokoll des Servers. Zu den gängigsten Protokollen im Internet gehören die folgenden:

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

Alle diese Protokolle zeichnen sich dadurch aus, dass es sich um textbasierte Protokolle handelt: Client und Server tauschen Textzeilen aus. Verfügt man über einen Client, der in der Lage ist:

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

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

21.2.2. Dienstprogramme TCP

Image

In den zu diesem Dokument gehörenden Codes finden sich zwei Kommunikationsdienstprogramme TCP:

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

Es handelt sich um zwei C#-Programme, deren Quellcode Ihnen zur Verfügung steht. Sie können diese daher ändern.

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

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

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

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

Sehen wir uns ein Beispiel an. Wir öffnen zwei Terminalfenster mit den Namen PyCharm und wechseln in jedem davon in den Ordner „Utilities“:

Image

In einem der Fenster starten wir 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 Server TCP auf Port 100;
  • Zeilen 2–4: Der Server wartet auf einen Client (TCP) 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 Client TCP:


(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 „Utilities“;
  • In Zeile 1 starten wir den Client TCP: Wir weisen ihn an, eine Verbindung zum Port 100 des lokalen Rechners herzustellen (auf dem der Code von [RawTcpClient] ausgeführt wird);
  • Zeile 2: Der Client hat erfolgreich eine Verbindung zum Server hergestellt. Die Daten des Clients werden angegeben: Er befindet sich auf dem Rechner [DESKTOP-30FF5FB] (in diesem Beispiel der lokale Rechner) und nutzt den 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 Nummer 1 zugewiesen. Der Server hat den Remote-Client (Rechner und Port) korrekt identifiziert;
  • Zeile 6: Der Server wartet nun wieder auf einen neuen Client;

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 dem Kunden eine Antwort:


(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 den Kunden gesendete Antwort 1. Es wird nur der Text zwischen den eckigen Klammern gesendet, nicht die Klammern selbst;

Kehren wir zum Kundenfenster 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 zwischen den 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 beenden 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], die Server-Protokolle: Der Dateiname entspricht dem Namen des Clients in der Form [machine-port]. So lassen sich unterschiedliche Protokolldateien für verschiedene Clients erstellen;
  • in [2] befinden sich die Client-Protokolle: Der Dateiname entspricht dem Servernamen in der Form [machine-port];

Die Server-Protokolle lauten wie folgt:


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

Die Client-Protokolle lauten wie folgt:


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

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

Image

Rechner im Internet werden durch eine Adresse (IP, IPv4 oder IPv6) und meist auch durch einen Namen identifiziert. Letztendlich wird jedoch nur die Adresse IP von den Kommunikationsprotokollen des Internets verwendet. Man muss also die Adresse IP eines Geräts kennen, das durch seinen Namen identifiziert wird.

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


# Importe
import socket


# ------------------------------------------------
def get_ip_and_name(nom_machine: str):
    # nom_machine: Name des Rechners, dessen Adresse benötigt wird IP
    try:
        # nom_machine-->Adresse IP
        ip = socket.gethostbyname(nom_machine)
        print(f"ip[{nom_machine}]={ip}")
    except socket.error as erreur:
        # Es wird eine Fehlermeldung angezeigt
        print(f"ip[{nom_machine}]={erreur}")
        return

    try:
        # Adresse IP --> nom_machine
        names = socket.gethostbyaddr(ip)
        print(f"names[{ip}]={names}")
    except socket.error as erreur:
        # es wird eine Fehlermeldung angezeigt
        print(f"names[{ip}]={erreur}")
        return


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

# die Internet-Rechner
hosts = ["istia.univ-angers.fr", "www.univ-angers.fr", "sergetahe.com", "localhost", "xx"]

# Adressen IP der Maschinen HOTES
for host in hosts:
    print("-------------------------------------")
    get_ip_and_name(host)
# Ende
print("Terminé...")

Kommentare

  • Zeile 2: Das Modul [socket] stellt die für die Verwaltung von Internet-Sockets erforderlichen Funktionen bereit. [socket] bedeutet „Steckdose“, „Netzwerkanschluss“;
  • Zeile 6: Die Funktion [get_ip_and_name] ermöglicht es, anhand des Internetnamens eines Rechners Folgendes zu ermitteln:
    • die Adresse IP des Rechners;
    • den Namen des Rechners, der aus der vorherigen Adresse IP ermittelt wurde;
  • Zeile 10: Mit der Funktion [socket.gethostbyname] lässt sich die Adresse IP eines Rechners anhand eines dieser Namen ermitteln (ein Internet-Rechner kann einen Hauptnamen und Aliasnamen haben);
  • Zeile 12: Die Funktionen für Sockets lösen die Ausnahme [socket.error] aus, sobald ein Fehler auftritt;
  • Zeile 19: Mit der Funktion [socket.gethostbyaddr] lässt sich der Name eines Rechners anhand seiner Adresse IP ermitteln. Wir werden sehen, dass dabei ein anderer Name als der in Zeile 6 übergebene ausgegeben werden kann;
  • Zeile 30: Eine Liste mit Rechnernamen. Der letzte Name ist falsch. Der Name [localhost] bezeichnet den Rechner, auf dem Sie gerade arbeiten und auf dem das Skript ausgeführt wird;
  • Zeile 33–35: Die IP-Werte dieser Rechner werden angezeigt;

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 Protokoll HTTP (HyperText Transfer Protocol)

21.4.1. Beispiel 1

Image

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

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

Zunächst starten wir den Server auf Port 100:


(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 fordern wir mit einem Browser die Daten von URL und [http://localhost:100] an, d. h. wir geben an, dass der abgefragte Server HTTP auf Port 100 des lokalen Rechners läuft:

Image

Kehren wir zum Serverfenster zurück:


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

Wir beenden die Verbindung, indem wir den Server beenden:


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 ein URL anzufordern, werden wir dieses URL mit unserem Client TCP [RawTcpClient] anfordern. Der Apache-Server von Laragon (Abschnitt |Installation von Laragon|) wird unser Webserver sein.

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

Image

Image

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

Image

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

Sehen wir uns nun den vom Browser empfangenen Text an:

Image

  • Wir klicken mit der rechten Maustaste auf die empfangene Seite und wählen die Option [2]. Wir 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>

Rufen wir nun die Seiten URL und [http://localhost:80] mit unserem Client TCP 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) :
  • In Zeile 1 stellen wir eine Verbindung zum Port 80 des Servers localhost her. Dort läuft der Webserver von Laragon;

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


(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 Befehl [GET]. Wir fragen das Stammverzeichnis / des Webservers ab;
  • Zeile 5: der Befehl [Host];
  • das sind die einzigen beiden unverzichtbaren Befehle. Für die übrigen Befehle verwendet der Webserver Standardwerte;
  • Zeile 6: die leere Zeile, die die Client-Befehle abschließen muss;
  • unterhalb von Zeile 6 folgt die Antwort des Webservers;
  • Zeilen 7–12: die HTTP-Header der Serverantwort;
  • Zeile 13: die leere Zeile, die das Ende der HTTP-Header signalisiert;
  • Zeilen 14–82: das in Zeile 4 angeforderte Dokument HTML;

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 Dokument „HTML“ wurde empfangen. Im vorherigen Beispiel hatte Firefox dasselbe Dokument empfangen;

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

21.4.3. Beispiel 3

Image

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


def configure():
    # URLs abfragen
    urls = [
        # Website: Name der Website, mit der eine Verbindung hergestellt werden soll
        # Port: Port des Webdienstes
        # GET: URL angefordert
        # Header: HTTP-Header, die in der Anfrage gesendet werden sollen
        # endOfLine: Zeilenendezeichen in den gesendeten HTTP-Header
        # encoding: Kodierung der Serverantwort
        # timeout: maximale Wartezeit auf eine Antwort des Servers
        {
            "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
        }
    ]
    # die Konfiguration wird zurückgegeben
    return {
        "urls": urls
    }
  • Der Inhalt der Datei ist eine Liste von URL, wobei jedes Element der Liste ein Wörterbuch ist. Dieses Wörterbuch gibt an, wie eine Verbindung zu der durch den Schlüssel [site] bezeichneten Website hergestellt wird;
  • Zeilen 4–10: Die Bedeutung der Schlüssel jedes Wörterbuchs;

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


# Importe
import codecs
import socket


# -----------------------------------------------------------------------
def get_url(url: dict, suivi: bool = True):
    # Liest die URL „URL“ der Website „url["GET"]“ und speichert sie in der Datei „url[site].html“
    # Der Client-Server-Dialog erfolgt gemäß dem im Wörterbuch [url] angegebenen Protokoll HTTP
    # Ausnahmen werden weitergeleitet

    sock = None
    html = None
    try:
        # Verbindung zu [site] über Port 80 mit einem Timeout
        site = url['site']
        sock = socket.create_connection((site, int(url['port'])), float(url['timeout']))

        # Die Verbindung stellt einen bidirektionalen Kommunikationsfluss dar
        # zwischen dem Client (diesem Programm) und dem angerufenen Webserver
        # Dieser Kanal wird für den Austausch von Befehlen und Informationen genutzt
        # Das Kommunikationsprotokoll lautet HTTP

        # Erstellung der Datei site.html – störende Zeichen werden durch einen Dateinamen ersetzt
        site2 = site.replace("/", "_")
        site2 = site2.replace(".", "_")
        html_filename = f'{site2}.html'
        html = codecs.open(f"output/{html_filename}", "w", "utf-8")

        # Der Client startet den Dialog HTTP mit dem Server
        if suivi:
            print(f"Client : début de la communication avec le serveur [{site}]")

        # Je nach Server müssen die Zeilen des Clients mit \n oder \r\n enden
        end_of_line = url["endOfLine"]
        # Der Client sendet den Befehl GET, um die Konfiguration URL abzufragen
        # Syntax GET URL HTTP/1.1
        commande = f"GET {url['GET']} HTTP/1.1{end_of_line}"
        # Verfolgung?
        if suivi:
            print(f"--> {commande}", end='')
        # Der Befehl wird an den Server gesendet
        sock.send(bytearray(commande, 'utf-8'))
        # Ausgabe der Header HTTP
        for verb, value in url['headers'].items():
            # Der zu sendende Befehl wird erstellt
            commande = f"{verb}: {value}{end_of_line}"
            # Weiterverfolgung?
            if suivi:
                print(f"--> {commande}", end='')
            # Der Befehl wird an den Server gesendet
            sock.send(bytearray(commande, 'utf-8'))
        # Der Header HTTP [Connection: close] wird gesendet, um den Webserver aufzufordern
        # die Verbindung zu schließen, sobald er das angeforderte Dokument gesendet hat
        sock.send(bytearray(f"Connection: close{end_of_line}", 'utf-8'))
        # Die Header des Protokolls HTTP müssen mit einer Leerzeile enden
        sock.send(bytearray(end_of_line, 'utf-8'))
        #
        # Der Server antwortet nun über den Sock-Kanal. Er sendet alle
        # seine Daten senden und anschließend den Kanal schließen. Der Client liest also alles, was über sock
        # bis zum Schließen des Kanals
        #
        # Zunächst werden die vom Server gesendeten Header HTTP gelesen
        # auch diese enden mit einer Leerzeile
        if suivi:
            print(f"Réponse du serveur [{site}]")

        # Das Socket wird so gelesen, als wäre es eine Textdatei
        encoding = f"{url['encoding']}" if url['encoding'] else None
        if encoding:
            file = sock.makefile(encoding=encoding)
        else:
            file = sock.makefile()
        # Diese Datei wird Zeile für Zeile verarbeitet
        fini = False
        while not fini:
            # Aktuelle Zeile lesen
            ligne = file.readline().strip()
            # Ist die Zeile nicht leer?
            if ligne:
                if suivi:
                    # Der Header wird angezeigt HTTP
                    print(f"<-- {ligne}")
            else:
                # Das war die leere Zeile – die Kopfzeilen HTTP sind beendet
                fini = True
        # Das Dokument HTML, das auf die leere Zeile folgt, wird gelesen
        # aktuelle Zeile wird gelesen
        ligne = file.readline()
        while ligne:
            # Eintrag in die Protokolldatei
            html.write(str(ligne))
            # nächste Zeile
            ligne = file.readline()
            # Die Schleife endet, wenn der Server die Verbindung schließt
    finally:
        # Der Client schließt die Verbindung
        if sock:
            sock.close()
        # Schließen der HTML-Datei
        if html:
            html.close()


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

# Die Anwendung wird konfiguriert
import config
config = config.configure()

# Abrufen der URL aus der Konfigurationsdatei
for url in config['urls']:
    print("-------------------------")
    print(url['site'])
    print("-------------------------")
    try:
        # URL von der Website [site] auslesen
        get_url(url)
    except BaseException as erreur:
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
# Ende
print("Terminé...")

Kommentare zum Code:

  • Zeilen 108–109: Das Wörterbuch [config] des Moduls [config.py] wird abgerufen;
  • Zeilen 111–122: Dieses Wörterbuch wird verarbeitet;
  • Zeile 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 wird der Client-Server-Datenaustausch in der Konsole protokolliert (Tracking=True);
  • alles erfolgt in einer [try / finally]-Funktion (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: Aufbau einer Verbindung zum Webserver. Die Funktion [socket.create_connection] akzeptiert drei Parameter:
    • [param1]: ist der Name des Internet-Rechners, den man erreichen möchte;
    • [param2]: ist die Portnummer des Dienstes, mit dem eine Verbindung hergestellt werden soll;
    • [param3]: [socket.create_connection] gibt einen Socket zurück, und [param3], sofern vorhanden, bezeichnet das Timeout des erstellten Sockets. Das Timeout ist die maximale Wartezeit des Sockets, während er auf eine Antwort vom Remote-Rechner wartet;
  • Zeilen 27–28: Erstellung der Datei [site.html], in der das empfangene Dokument HTML 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 Textzeile hat folgende Bedeutung: „Ich möchte (GET) die Seite [URL] der Website, mit der ich verbunden bin. Ich arbeite mit dem Protokoll HTTP, Version 1.1“;
  • Zeile 43: Der Befehl [sock.send(bytearray(commande, 'utf-8'))] sendet ein Byte-Array (bytearray). Dieses Array wird durch die Umwandlung der Zeichenkette [commande] in eine Folge von Bytes erhalten, die in UTF-8 kodiert sind;
  • Zeilen 44–52: Es werden die übrigen Zeilen des Protokolls HTTP [Host, User-Agent, Accept, Accept-Language…] gesendet. Ihre Reihenfolge spielt keine Rolle;
  • Zeilen 53–55: Es wird der Header HTTP [Connection: close] gesendet, um den Server aufzufordern, die Verbindung zu schließen, sobald er das angeforderte Dokument gesendet hat. Standardmäßig tut er dies nicht. Man muss ihn daher ausdrücklich darum bitten. Der Vorteil besteht darin, dass dieses Schließen auf der Client-Seite erkannt wird und der Client so weiß, dass er das gesamte angeforderte Dokument erhalten hat;
  • Zeilen 56–57: Es wird eine leere Zeile an den Server gesendet, um anzuzeigen, dass der Client das Senden seiner Header HTTP abgeschlossen hat und nun auf das angeforderte Dokument wartet;
  • Zeilen 68–86: Der Server sendet zunächst eine Reihe von HTTP-Headern, die verschiedene Informationen zum angeforderten Dokument enthalten. Diese Header enden mit einer leeren Zeile;
  • Zeilen 69–73: Um die Antwort des Servers zeilenweise lesen zu können, wird die Methode [sock.makefile(encoding=encoding)] verwendet. Der optionale Parameter [encoding] gibt die erwartete Textkodierung an. Nach diesem Vorgang kann der vom Server gesendete Zeilenstrom wie eine herkömmliche Textdatei gelesen werden;
  • Zeile 78: Mit der Methode [readline] wird eine vom Server gesendete Zeile gelesen. Dabei werden Leerzeichen (Leerzeichen, Zeilenendezeichen) am Zeilenanfang und -ende entfernt;
  • Zeilen 81–83: Ist die Zeile nicht leer und wurde eine Nachverfolgung angefordert, wird die empfangene Zeile auf der Konsole angezeigt;
  • Zeilen 84–86: Wenn die leere Zeile empfangen wurde, die das Ende der vom Server gesendeten HTTP-Header markiert, wird die Schleife aus Zeile 76 beendet;
  • Zeilen 90–95: Die Textzeilen der Serverantwort können zeilenweise mit einer Schleife while 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 Client-Seite wird dies als Dateiende erkannt, und die Schleife in den Zeilen 90–95 wird verlassen;
  • Zeilen 96–102: Unabhängig davon, ob ein Fehler vorliegt oder nicht, werden alle vom Code genutzten Ressourcen freigegeben;

Ergebnisse:

Die Konsole zeigt folgende Protokolleinträge 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/Programmierkurse-Tutorials
<-- 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/Programmierkurse-Tutorials/
<-- Set-Cookie: SERVERID68971=2620178|XwH/h|XwH/h; path=/
<-- X-IPLB-Instance: 17095
Terminé...

Process finished with exit code 0

Kommentare

  • Zeile 12: URL [http://localhost/] wurde gefunden (Code 200);
  • Zeile 29: Die Seite „URL [http://sergetahe.com/]“ wurde nicht gefunden (Code 302). Der Code 302 bedeutet, dass sich die angeforderte Seite geändert hat und nun „URL“ lautet. Die neue URL wird durch die Kopfzeile HTTP [Location] in Zeile 36 angegeben;
  • Zeile 49: Die an den Server [http://tahe.developpez.com] gesendete Anfrage ist fehlerhaft (Code 400);
  • Zeile 65: Die Seite URL [http://www.sergetahe.com/] wurde nicht gefunden (Code 301). Der Code 301 bedeutet, dass sich die angeforderte Seite dauerhaft unter URL geändert hat. Die neue URL wird durch die Kopfzeile HTTP [Location] in Zeile 71 angegeben;

Im Allgemeinen sind die Codes 3xx, 4xx und 5xx eines Servers HTTP Fehlercodes.

Die Ausführung hat folgende 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 empfangene Dokument [output/sergetahe_com.html] lautet wie folgt:

Image

Die meisten HTTP-Server senden ihre Antworten auf die an sie gerichteten Anfragen in Teilen. Jedem gesendeten Teil geht eine Zeile voraus, die die Anzahl der Bytes des folgenden Teils angibt. Dies ermöglicht es dem Client, genau diese Anzahl an Bytes zu lesen, um den Teil zu erhalten. Hier bedeutet die 0, dass der folgende Teil null Bytes umfasst. Zur Erinnerung: Der Server hatte angegeben, dass sich das Dokument [http://sergetahe.com/] gegenüber URL geändert habe. Er hat also 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 Dokument mit der Bezeichnung HTML gesendet, obwohl die Anfrage fehlerhaft war (Zeile 49 der Ergebnisse). Das Dokument HTML ermöglicht es dem Server, die Fehlerursache anzugeben. Diese ist in den Zeilen 6 und 7 angegeben:
    • Zeile 7: Unser Client hat das Protokoll HTTP verwendet;
    • Zeile 8: Der Server arbeitet mit dem Protokoll HTTPS (S = sicher) und akzeptiert das Protokoll HTTP 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, ein Dokument HTML zu senden, in dem dieser Fehler detailliert beschrieben wird (Zeilen 1–7).

21.4.4. Beispiel 4

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

Image

Öffnen wir ein Terminal PyCharm [1]:

Image

  • in [1], Zugriff auf die Terminals von PyCharm;
  • in [2-3] die bereits aktiven Terminals;
  • in [4] den Ordner, in dem Sie sich gerade befinden. Im Folgenden spielt dies keine Rolle;

Im Terminal geben wir 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 geliefert hat, zeigt, dass sich der Befehl [curl] im Verzeichnis PATH des Terminals befindet. Unter Windows ist PATH die Gesamtheit der Ordner, die durchsucht werden, wenn der Benutzer einen ausführbaren Befehl eingibt, in diesem Fall [curl]. Der Wert von PATH lässt sich 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 Ordner von PATH, getrennt durch Semikolons. In dieser Liste erscheint kein Ordner, der mit Laragon in Verbindung steht. Bei näherer Untersuchung stellt man fest, dass sich im Ordner [c:\windows\system32] ein [curl] befindet. Das ist derjenige, der zuvor geantwortet hat.

Wenn man das mit Laragon mitgelieferte Tool [curl] verwenden möchte, kann man wie folgt vorgehen:

Image

Image

  • in [2], das Laragon-Terminal;
  • in [3]: Mit dieser Schaltfläche können neue Terminals erstellt werden, die jeweils in einem Reiter des obigen Fensters installiert werden;
  • in [4] wird das PATH des Laragon-Terminals angefordert;
  • man erhält etwas ganz anderes als das, was in einem Terminal mit der Bezeichnung PyCharm angezeigt wurde. Dieses PATH enthält zahlreiche Ordner, die bei der Installation von Laragon erstellt wurden. Der Ordner, der das Tool [curl] enthält, ist einer davon:

Image

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

Der Befehl [curl --help] zeigt alle Konfigurationsoptionen von [curl] an. Es gibt mehrere Dutzend davon. Wir werden nur sehr wenige davon verwenden. Um ein URL anzufordern, geben Sie einfach den Befehl [curl URL] ein. Dieser Befehl zeigt das angeforderte Dokument auf der Konsole an. Wenn wir zusätzlich den Datenaustausch HTTP zwischen Client und Server anzeigen möchten, geben wir [curl --verbose URL] ein. Um schließlich das angeforderte Dokument HTML in einer Datei zu speichern, geben wir [curl --verbose --output fichier 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                                          
  • In Zeile 3 wechseln wir in den Ordner „[c:\temp]“. Falls dieser Ordner nicht existiert, können Sie ihn erstellen oder einen anderen auswählen;
  • Zeile 6: Wir erstellen einen Ordner mit dem Namen [curl];
  • Zeile 9: Man wechselt in diesen Ordner;
  • Zeile 12: Der Inhalt des Ordners wird aufgelistet. Er ist leer (Zeile 20);

Stellen Sie sicher, dass der Apache-Server von Laragon gestartet ist, und rufen Sie mit [curl] die Dateien URL und [http://localhost/] mit dem Befehl [curl –verbose –output localhost.html http://localhost/] ab. Man erhält 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, damit „localhost“ unverändert bleibt
  • Zeilen 10–13: Von [curl] an den Server [localhost] gesendete Zeilen. Man erkennt das Protokoll HTTP;
  • 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.

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

Die Konsolenausgaben lauten wie folgt:


C:\Temp\curl
λ curl --verbose --output tahe.developpez.com.html https://tahe.developpez.com:443/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 87.98.130.52...
* TCP_NODELAY set
  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 zum Host tahe.developpez.com, unverändert belassen
  • Zeilen 10–39: Der Austausch zwischen Client und Server zur Sicherung der Verbindung: Diese wird verschlüsselt;
  • Zeilen 41–44: Die vom Client HTTP an den Server gesendeten Header;
  • Zeile 52: Das angeforderte Dokument wurde gefunden;
  • Zeile 57: Das Dokument wird in Teilen gesendet;

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

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

Die Konsolenergebnisse lauten dann wie folgt:


C:\Temp\curl
λ curl --verbose --output sergetahe.com.html --location http://sergetahe.com/Programmierkurse-Tutorials
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 87.98.154.146...
* TCP_NODELAY set
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation HTTP/1.1
> Host: sergetahe.com
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Date: 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/Programmierkurse-Tutorials/
< 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 auf dem Host sergetahe.com unverändert belassen
* Issue another request to this URL: 'http://sergetahe.com/Programmierkurse-und-Tutorials/'
* Found bundle for host sergetahe.com: 0x14385f8 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) mit Host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> 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/Programmierkurse-und-Tutorials/
< 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 zum Host sergetahe.com, unverändert belassen
* Issue another request to this URL: 'https://sergetahe.com/Programmierkurse-und-Tutorials/'
*   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/Programmierkurse-und-Tutorials/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/Programmierkurse-und-Tutorials/>; 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, um sergetahe.com unverändert zu belassen
  • Zeile 2: Die Option [--location] wird verwendet, um anzugeben, dass den vom Server gesendeten Weiterleitungen gefolgt werden soll;
  • Zeile 13: Der Server gibt an, dass sich das angeforderte Dokument geändert hat: URL;
  • Zeile 18: Er gibt die neue URL des angeforderten Dokuments an;
  • Zeile 31: [curl] sendet eine neue Anfrage, diesmal an die neue URL;
  • Zeile 36: Der Server antwortet erneut, dass sich die URL geändert hat;
  • Zeile 41: Die neue URL ist bis auf ein Detail genau dieselbe wie die umgeleitete: Das Protokoll hat sich geändert. Es lautet nun HTTPS (Zeile 41), während es zuvor http war (Zeile 31);
  • Zeile 49: Eine neue Anfrage wird an die neue URL gesendet. Diese ist verschlüsselt. Daher wird ein ganzer Dialog zur Einrichtung der Sicherheit aufgebaut, Zeilen 53–91;
  • Zeile 92: Die neue URL wird angefordert, diesmal mit dem Protokoll HTTP/2;
  • 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 die Funktionen des Tools [curl] in einem Python-Programm genutzt werden können. Wir installieren dieses Modul:

Image

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

Image

Die Datei „[http/02/config]“ lautet wie folgt:


def configure():
    # Liste der abzufragenden URL
    urls = [
        # Site: Server, mit dem eine Verbindung hergestellt werden soll
        # Timeout: maximale Wartezeit auf eine Antwort des Servers
        # Ziel: abzufragende URL
        # Kodierung: Kodierung der Serverantwort
        {
            "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"
        }
    ]
    # Die Konfiguration wird zurückgegeben
    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: die erwartete Kodierung des Dokuments;
  • timeout: maximale Wartezeit auf die Antwort des Servers in Millisekunden. Nach Ablauf dieser Zeit trennt der Client die Verbindung;
  • url: URL des angeforderten Dokuments;

Der Code des Skripts [http/02/main.py] lautet wie folgt:


# Importe
import codecs
from io import BytesIO

import pycurl


# -----------------------------------------------------------------------
def get_url(url: dict, suivi=True):
    # liest die URL „URL“ und speichert sie in der Datei „output/url['site'].html“
    # Wenn [suivi=True], dann erfolgt eine Konsolenprotokollierung des Client-Server-Austauschs
    # url[timeout] ist das Timeout für Client-Aufrufe;
    # Die URL [encoding] gibt die Kodierung des angeforderten Dokuments an

    # Hier werden die Konfigurationsdaten abgerufen
    server = url['site']
    timeout = url['timeout']
    target = url['target']
    encoding = url['encoding']
    # Nachverfolgung
    print(f"Client : début de la communication avec le serveur [{server}]")

    # Ausnahmen werden weitergeleitet
    html = None
    curl = None
    try:
        # Initialisierung einer Sitzung cURL
        curl = pycurl.Curl()
        # Binärstrom
        flux = BytesIO()
        # curl-Optionen
        options = {
            # URL
            curl.URL: target,
            # WRITEDATA: Hier werden die empfangenen Daten gespeichert
            curl.WRITEDATA: flux,
            # Ausführlicher Modus
            curl.VERBOSE: suivi,
            # Neue Verbindung – kein Cache
            curl.FRESH_CONNECT: True,
            # Zeitlimit für die Anfrage (in Sekunden)
            curl.TIMEOUT: timeout,
            curl.CONNECTTIMEOUT: timeout,
            # Gültigkeit der Zertifikate nicht prüfen SSL
            curl.SSL_VERIFYPEER: False,
            # Weiterleitungen verfolgen
            curl.FOLLOWLOCATION: True
        }
        # curl-Einstellungen
        for option, value in options.items():
            curl.setopt(option, value)
        # Ausführung der so konfigurierten Anfrage CURL
        curl.perform()
        # Erstellung der Datei server.html – die störenden Zeichen werden durch einen Dateinamen ersetzt
        server2 = server.replace("/", "_")
        server2 = server2.replace(".", "_")
        html_filename = f'{server2}.html'
        html = codecs.open(f"output/{html_filename}", "w", encoding)
        # Speichern des empfangenen Dokuments in der Datei HTML
        html.write(flux.getvalue().decode(encoding))
    finally:
        # Freigabe der Ressourcen
        if curl:
            curl.close()
        if html:
            html.close()


# -------------------Hauptprogramm
# Konfiguration der Anwendung
import config
config = config.configure()

# Abruf der URL aus der Konfigurationsdatei
for url in config['urls']:
    print("-------------------------")
    print(url['site'])
    print("-------------------------")
    try:
        # URL von der Website [site] abrufen
        get_url(url)
    # außer BaseException als Fehler:
    #     print(f"Der folgende Fehler ist aufgetreten: {Fehler}")
    finally:
        pass
# Ende
print("Terminé...")

Kommentare

  • Zeile 5: Das Modul [pycurl] wird importiert;
  • Zeile 3: Die Klasse [BytesIO] wird importiert, mit der wir die vom Server empfangenen Daten in einem Binärstrom speichern können;
  • Zeilen 70–72: Die Konfiguration der Anwendung wird abgerufen;
  • Zeilen 75–85: Wir durchlaufen die Liste der in der Konfiguration gefundenen URL;
  • Zeile 81: Für jedes der URL wird die Funktion [get_url] aufgerufen, die die URL URL mit einem Timeout von [‘target’] herunterlädt;
  • Zeile 9: Die Funktion [get_url] erhält die Konfiguration der abzufragenden Funktion URL;
  • Zeilen 16–19: Die Konfiguration von URL wird in separaten Variablen gespeichert;
  • Zeilen 26, 61: Alle Operationen werden innerhalb eines „try/finally“-Blocks ausgeführt. Ausnahmen werden nicht abgefangen, sondern an den aufrufenden Code weitergeleitet, der sie dann abfängt;
  • Zeile 28: Es wird eine [curl]-Sitzung vorbereitet. [pycurl.Curl()] gibt eine Ressource [curl] zurück, die die Transaktion mit einem Server durchführt;
  • Zeile 30: Instanziierung des Binärstroms, der die empfangenen Daten speichern wird;
  • Zeilen 32–48: Das Dictionary [options] konfiguriert die Verbindung [curl] zum Server. Ihre Funktion ist in den Kommentaren angegeben;
  • Zeilen 49–51: Die Verbindungsoptionen werden an die Ressource [curl] übermittelt;
  • Zeile 53: Verbindung zu URL mit den definierten Optionen angefordert. Aufgrund der Option [curl.WRITEDATA: flux] (Zeile 36) speichert die Funktion [curl.perform()] die empfangenen Daten in [flux];
  • Zeilen 54–60: Es wird die Datei HTML erstellt, in der das empfangene Dokument HTML gespeichert wird;
  • Zeile 60: Der Binärstrom [flux.getvalue()] wird als Zeichenkette in der Datei HTML gespeichert. Die Kodierung dieser Zeichenkette wird in der Methode [decode(encoding)] festgelegt. Man muss daher die Kodierung des vom Server gesendeten Dokuments kennen. Bei einer falschen Angabe schlägt die Dekodierung des Binärstroms fehl. Die Kodierung ist in der Konfigurationsdatei von URL festgelegt (z. B. in Zeile 12). Man hätte diese Information dynamisch verarbeiten können, da der Server sie in seinen HTTP-Header übermittelt. Dies wäre vorzuziehen gewesen. Um den Code einfach zu halten, haben wir darauf verzichtet. Um die Art der Kodierung des Dokuments zu ermitteln, genügt es, die gewünschte URL-Datei mit einem Browser aufzurufen und die von diesem im Debug-Modus des Browsers gesendeten HTTP-Header (F12) oder das Dokument selbst zu überprüfen, da auch dieses die Kodierung angibt:

Image

Image

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

Wenn man das Skript [main.py] ausführt, erhält man folgende Konsolenausgaben:


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/Programmierkurse-Tutorials
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0, um sergetahe.com unverändert zu belassen
* Issue another request to this URL: 'http://sergetahe.com/Programmierkurse-Tutorials'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) mit Host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation HTTP/1.1
Host: sergetahe.com
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/Programmierkurse-Tutorials/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 zum Host sergetahe.com, unverändert belassen
* Issue another request to this URL: 'http://sergetahe.com/Programmierkurse-und-Tutorials/'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) mit Host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation/ HTTP/1.1
Host: sergetahe.com
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/Programmierkurse-und-Tutorials/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 zum Host sergetahe.com, unverändert belassen
* Issue another request to this URL: 'https://sergetahe.com/Programmierkurse-und-Tutorials/'
*   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/Programmierkurse-und-Tutorials/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/Programmierkurse-Tutorials/>; rel=shortlink
< vary: Accept-Encoding
< x-iplb-instance: 17080
< set-cookie: SERVERID68971=26218|XwLIp|XwLIp; path=/
< 
* Connection #1 auf dem Host sergetahe.com unverändert belassen
-------------------------
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, um tahe.developpez.com unverändert zu belassen
-------------------------
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 auf dem Host www.polytech-angers.fr bleibt unverändert
* 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) mit dem Host www.polytech-angers.fr
* Connected to www.polytech-angers.fr (193.49.144.41) port 80 (#0)
> GET /fr/index.html HTTP/1.1
Host: www.polytech-angers.fr
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 zum Host www.polytech-angers.fr, der unverändert bleibt
-------------------------
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 an den Host „localhost“ – unverändert
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;
  • Es ergibt sich derselbe Datenaustausch wie mit dem Tool [curl];
    • Zeile 9: URL [http://sergetahe.com/] wird angefordert;
    • Zeile 15: Der Server antwortet, dass die Seite verschoben wurde. Zeile 21: die neue URL;
    • Zeile 32: 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: 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. Sie verwendet das sichere Protokoll [HTTPS];
    • Zeilen 71–75: Das sichere Protokoll wird mit dem Server hergestellt;
    • Zeile 76: 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 Protokoll HTTP kennengelernt und ein Skript [http/02/main.py] geschrieben, mit dem eine URL aus dem Internet heruntergeladen werden kann.

21.5. Das Protokoll SMTP (Simple Mail Transfer Protocol)

21.5.1. Einleitung

Image

In diesem Kapitel:

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

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

Für unsere Tests SMTP benötigen wir eine E-Mail-Adresse, an die wir schreiben 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 erstellte Konto E-Mails empfangen kann.

21.5.3. Einrichtung eines SMTP-Servers

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

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

Image

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

Image

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

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

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

Image

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

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

Image

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

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

  • Beenden Sie den Server [hMailServer];
  • Öffnen Sie die Datei [<hmailserver>/bin/hmailserver.ini], wobei <hmailserver> der Installationsordner des Servers ist:

Image

  • Entfernen Sie in der Datei [100] das Passwort aus der Zeile [AdministratorPassword]. Dadurch hat der Administrator kein Passwort mehr. Geben Sie einfach [Entrée] ein, wenn Sie dazu aufgefordert werden;

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

Setzen wir die Konfiguration des Servers fort:

Image

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

Image

  • Unter [3] können Sie für die Tests, die wir durchführen werden, so gut wie alles eingeben. In der Praxis müsste hier der Name einer existierenden Domain stehen;

Image

Wir werden nun ein Benutzerkonto anlegen:

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

Image

  • in [13-14] wird der Benutzer angelegt;

Image

  • in [27] der Port des Dienstes SMTP;
  • in [28]: Dieser Dienst erfordert keine Authentifizierung;
  • in [30] geben Sie die Begrüßungsnachricht ein, die der Server SMTP an seine Clients sendet;

Image

Das Gleiche gilt für den Server POP3:

Image

Das Gleiche wiederholen wir für den Server IMAP:

Image

Wir geben die Standarddomain des Servers [hMailServer] an (es kann mehrere geben) :

Image

  • Geben Sie in [37] an, dass die Standarddomäne des Servers SMTP 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 anschließend 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: Es wird eine Verbindung zum Port 25 des Rechners [localhost] hergestellt. Dort läuft ein ungesicherter Server SMTP des Servers [hMailServer];
  • Zeile 4: Wir erhalten die Begrüßungsnachricht, die wir im vorherigen Schritt 30 konfiguriert haben;

Der Server SMTP ist also ordnungsgemäß eingerichtet. Geben Sie den Befehl [quit] ein, um die Verbindung mit dem Server SMTP (Port 25) zu beenden.

Nun machen wir dasselbe mit Port 587, dem Standardport des sicheren E-Mail-Relay-Dienstes SMTP:


(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 des Servers SMTP, der auf Port 587 läuft;

Nun machen wir dasselbe mit Port 110, dem Standardport des E-Mail-Relay-Dienstes POP3:


(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 Begrüßungsnachricht vom Server POP3 erhalten;

Nun machen wir dasselbe mit Port 143, dem Standardport des E-Mail-Relay-Dienstes IMAP:


(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 Begrüßungsnachricht vom Server IMAP erhalten;

21.5.4. Einrichtung eines E-Mail-Readers

Um die E-Mail zu lesen, die wir versenden werden, benötigen wir ein E-Mail-Programm. Für diejenigen, die noch keines haben, zeigen wir hier die Installation und Konfiguration des Programms [Thunderbird]:

  • in [1]: Laden Sie [thunderbird] herunter und installieren Sie es anschließend;

Image

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

Image

Image

Image

  • auf [7-11]: Der Server POP3, über den wir die E-Mails vom Mailserver [hMailServer] abrufen können, befindet sich unter der Adresse [localhost] und läuft auf Port 110;
  • bei [12-16]: Der Server SMTP, über den wir E-Mails im Namen der Benutzer des Mail-Servers [hMailServer] versenden können, befindet sich unter der Adresse [localhost] und läuft auf Port 25;
  • [18]: Die Konfiguration kann hier getestet werden;

Image

Image

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

Um das erstellte Konto zu testen, gehen wir mit Thunderbird wie folgt vor:

  • eine E-Mail an den Benutzer [guest@localhost.com] senden (Protokoll SMTP);
  • die von diesem Benutzer empfangene E-Mail lesen (Protokoll POP3);

Image

  • in [3]: der Absender;
  • in [4]: der Empfänger;
  • in [5]: den Betreff der E-Mail;
  • in [6]: den Inhalt der E-Mail;
  • in [7]: zum Versenden der E-Mail;

Image

  • in [8-9]: Die E-Mail des Benutzers [guest@localhost] wird abgerufen;
  • in [10-15]: die empfangene Nachricht;

Wir werden auch eine E-Mail an den Benutzer [pymailparlexemple@gmail.com] senden. Erstellen wir ihm ein Konto in Thunderbird, damit er die E-Mail lesen kann, die er erhalten wird:

Image

Image

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

Image

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

Image

  • in [14-17]: die Eigenschaften des Servers IMAP;
  • in [18-21]: die Eigenschaften des Servers SMTP;
  • in [22]: Die Konfiguration wird abgeschlossen;

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]: Die E-Mails der verschiedenen Konten werden abgerufen;

Image

  • in [33-36]: die vom Benutzer empfangenen E-Mails [pymailparlexemple@gmail.com]

Ebenso erstellen wir:

  • ein neues Gmail-Konto [pymail2parlexemple@gmail.com];
  • ein neues Thunderbird-Konto [pymail2parlexemple@gmail.com], um die E-Mails des gleichnamigen Benutzers abzurufen:

Image

Image

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

21.5.5. Das Protokoll SMTP

Image

Wir werden das Protokoll SMTP untersuchen, indem wir die Protokolle des Servers [hMailServer] prüfen. Dazu aktivieren wir sie mit dem Outl [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] werden die Protokolle angefordert;
  • in [8] wird die Protokolldatei mit einem beliebigen Texteditor geöffnet;

Image

Im folgenden Beispiel ist der Client [Thunderbird] und der Server [hMailServer]. Stellen Sie in Thunderbird sicher, dass der Benutzer [guest@localhost.com] sich selbst eine Nachricht sendet:

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 Client SMTP (dem E-Mail-Client Thunderbird) und dem Server SMTP (hMailServer) stattgefunden hat. Die Zeilen [SENT] geben an, was der Server SMTP an seinen Client gesendet hat. Die Zeilen [RECEIVED] geben an, was der Server SMTP von seinem Client empfangen hat.

  • Zeile 1: Unmittelbar nach der Verbindung des Clients mit dem Server SMTP sendet dieser die Begrüßungsnachricht an seinen Client;
  • Zeile 2: Der Client sendet den Befehl [EHLO], um sich zu identifizieren. Hier gibt er seine Adresse IP [127.0.0.1] an, die den Rechner [localhost] bezeichnet, d. h. den Rechner, auf dem der Client SMTP läuft;
  • Zeile 3: Der Server sendet eine Reihe von Antworten: [250]. [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. Auf diese Weise weiß der Client SMTP, dass die Antwort des Servers SMTP beendet ist und er einen Befehl senden kann. Die Befehlsfolge [250] diente dazu, dem Client SMTP eine Reihe von Befehlen anzuzeigen, die er verwenden konnte;
  • Zeile 4: Der Client SMTP sendet den Befehl [MAIL FROM : adresse_mail_expéditeur], der angibt, wer die Nachricht sendet;
  • Zeile 5: Der Server SMTP antwortet mit [250 OK] und zeigt damit an, dass er den Befehl verstanden hat;
  • Zeile 6: Der Client SMTP sendet den Befehl [RCPT TO : adresse_mail_destinataire], um die Empfängeradresse anzugeben;
  • Zeile 7: Erneut bestätigt der Server SMTP, dass er den Befehl verstanden hat;
  • Zeile 8: Der Server SMTP sendet den Befehl [DATA]. Das bedeutet, dass er den Inhalt der Nachricht senden wird;
  • Zeile 9: Der Server SMTP signalisiert mit der Antwort [354 OK], dass er bereit ist, die Nachricht zu empfangen. Der Text [send .] besagt, dass der Client SMTP seine Nachricht mit einer Zeile beenden muss, die nur einen einzigen Punkt enthält;
  • Was man anschließend nicht sieht, ist, dass der Client SMTP seine Nachricht sendet. Die Protokolle zeigen dies nicht an;
  • Zeile 10: Der Client SMTP hat den Punkt gesendet, der das Ende der Nachricht kennzeichnet. Der Server SMTP antwortet ihm, dass er die Nachricht in die Warteschlange gestellt hat (queued);
  • Der Client SMTP sendet ihm den Befehl [QUIT], um anzugeben, dass er die Verbindung schließen wird;
  • Zeile 12: Der Server antwortet ihm;

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

Image

Betrachten wir ein neues Beispiel:

  • Client A ist der generische Client TCP ([RawTcpClient]);
  • Server B ist der Mailserver [hMailServer];
  • Client A fordert Server B auf, eine E-Mail zuzustellen, die der Benutzer [guest@localhost.com] an sich selbst gesendet hat;
  • 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 zum Port 25 des lokalen Rechners her, auf dem der Dienst SMTP von [hMailServer] läuft. Das Argument [--quit bye] gibt an, dass der Benutzer das Programm durch Eingabe des Befehls [bye] beendet. Ohne dieses Argument lautet der Befehl zum Beenden des Programms [quit]. Nun ist [quit] aber auch ein Befehl des Protokolls SMTP. Wir müssen diese Mehrdeutigkeit also vermeiden;
  • Zeile [2]: Der Client ist ordnungsgemäß verbunden;
  • Zeile [3]: Der Client wartet auf Befehle, die über die Tastatur eingegeben werden;
  • Zeile [4]: Der Server sendet ihm 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 nom-de-la-machine-client]. Der Server antwortet ihm mit einer Reihe von Nachrichten der Form [250-xx] (6). Der Code [250] zeigt an, dass der vom Client gesendete Befehl erfolgreich war;
  • in [10] gibt der Client den Absender der Nachricht an, hier [guest@localhost.com];
  • in [11] die Antwort des Servers;
  • in [12] wird der Empfänger der Nachricht angegeben, hier der Benutzer [guest@localhost.com];
  • in [13], die Antwort des Servers;
  • in [14] weist der Befehl [DATA] den Server darauf hin, dass der Client den Inhalt der Nachricht senden wird;
  • 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 Zeilen [Subject:, From:, To:] (16–18) enthalten, um jeweils den Betreff der Nachricht, den Absender und den Empfänger anzugeben;
  • in [19] müssen auf die vorangegangenen Kopfzeilen eine Leerzeile folgen;
  • in [20-21] der Text der Nachricht;
  • in [22] die Zeile, die nur einen einzigen Punkt enthält und das Ende der Nachricht kennzeichnet;
  • in [23]: Sobald der Server die Zeile mit dem einzelnen Punkt empfangen hat, stellt er die Nachricht in die Warteschlange;
  • in [24] teilt der Client dem Server mit, dass er fertig ist;
  • in [25] ist zu sehen, dass der Server die Verbindung zum Client geschlossen hat;

Nun überprüfen wir mit 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 ist es unserem Kunden [RawTcpClient] gelungen, eine Nachricht über den Server SMTP [localhost] zu versenden. Nun wenden wir dieselbe Methode an, 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 Gmail-Server SMTP, der auf Port 587 läuft;
  • Zeile 15: Wir kommen nicht weiter, da der Server SMTP von uns verlangt, eine sichere Verbindung herzustellen, was wir nicht wissen, wie man macht. Im Gegensatz zum vorherigen Beispiel verlangt der Server [smtp.gmail.com] (Zeile 1) eine Authentifizierung. Er akzeptiert als Clients nur Benutzer, die in der Domäne [gmail.com] registriert sind. Diese Authentifizierung ist sicher und findet innerhalb einer verschlüsselten Verbindung statt.

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

21.5.6. [smtp/01]-Skripte: 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 {
        # Beschreibung: Beschreibung der gesendeten E-Mail
        # smtp-server: Server SMTP
        # smtp-port: Port des Servers SMTP
        # from: Absender
        # An: Empfänger
        # Betreff: Betreff der E-Mail
        # Nachricht: Nachrichtentext der E-Mail
        "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",
                # Wir senden UTF-8
                "content-type": 'text/plain; charset="utf-8"',
                # Wir testen die Zeichen mit Akzenten
                "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",
                # Wir senden UTF-8
                "Content-type": 'text/plain; charset="utf-8"',
                # Wir testen Zeichen mit Akzenten
                "message": "aglaë séléné\nva au marché\nacheter des fleurs"
            }
        ]
    }
  • Zeilen 10–35: eine Liste der zu versendenden E-Mails. Für jede davon werden folgende Informationen angegeben:
    • [description]: ein Text, der die E-Mail beschreibt;
    • [smtp-server]: der zu verwendende Server SMTP;
    • [smtp-port]: dessen Dienstport;
    • [from]: der Absender der E-Mail;
    • [to]: der Empfänger der E-Mail;
    • [subject]: der Betreff der E-Mail;
    • [content-type]: die Kodierung der E-Mail;
    • [message]: der Text der E-Mail;

Der Code [01/main] des Klienten SMTP lautet wie folgt:


# Importe
import socket


# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: bool):
    # sendet eine Nachricht an den SMTP-Server „smtpserver“ im Namen des Absenders
    # an den Empfänger. Wenn verbose=True, wird der Client-Server-Austausch protokolliert

    # Systemfehler werden gemeldet
    connexion = None
    try:
        # Name des lokalen Rechners (erforderlich für das Protokoll SMTP)
        client = socket.gethostbyaddr(socket.gethostbyname("localhost"))[0]
        # Aufbau einer Verbindung auf Port 25 von smtpServer
        connexion = socket.create_connection((mail["smtp-server"], 25))

        # Die Verbindung stellt einen bidirektionalen Kommunikationsfluss dar
        # zwischen dem Client (diesem Programm) und dem angerufenen SMTP-Server
        # Dieser Kanal wird für den Austausch von Befehlen und Informationen genutzt

        # Nach dem Verbindungsaufbau sendet der Server eine Begrüßungsnachricht, die gelesen wird
        send_command(connexion, "", verbose, True)
        # Befehl „ehlo“:
        send_command(connexion, f"EHLO {client}", verbose, True)
        # Befehl „mail from“:
        send_command(connexion, f"MAIL FROM: <{mail['from']}>", verbose, True)
        # Befehl „rcpt to“:
        send_command(connexion, f"RCPT TO: <{mail['to']}>", verbose, True)
        # Befehl „data“
        send_command(connexion, "DATA", verbose, True)
        # Vorbereitung der zu versendenden Nachricht
        # sie muss folgende Zeilen enthalten
        # Von: Absender
        # An: Empfänger
        # Leere Zeile
        # Nachricht
        # .
        data = f"{mail['message']}"
        # Nachricht senden
        send_command(connexion, data, verbose, False)
        # Senden.
        send_command(connexion, "\r\n.\r\n", verbose, False)
        # Befehl „quit“
        send_command(connexion, "QUIT", verbose, True)
        # Ende
    finally:
        # Verbindung schließen
        if connexion:
            connexion.close()


# --------------------------------------------------------------------------
def send_command(connexion: socket, commande: str, verbose: bool, with_rclf: bool):
    # Befehl im Verbindungskanal senden
    # Ausführlicher Modus, wenn verbose=True
    # wenn with_rclf=True, fügt die Sequenz rclf zum Befehl hinzu

    # Daten
    rclf = "\r\n" if with_rclf else ""
    # Befehl senden, wenn der Befehl nicht leer ist
    if commande:
        # Systemfehler werden weitergeleitet
        #
        # Befehl senden
        connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
        # eventuelles Echo
        if verbose:
            affiche(commande, 1)
        # Antwort mit weniger als 1000 Zeichen lesen
        reponse = str(connexion.recv(1000), 'utf-8')
        # Eventuelle Rückmeldung
        if verbose:
            affiche(reponse, 2)
        # Abruf des Fehlercodes
        codeErreur = int(reponse[0:3])
        # Vom Server zurückgegebenen Fehler?
        if codeErreur >= 500:
            # Es wird eine Ausnahme mit dem Fehler ausgelöst
            raise BaseException(reponse[4:])
        # Rückgabe ohne Fehler


# --------------------------------------------------------------------------
def affiche(echange: str, sens: int):
    # Austausch auf dem Bildschirm anzeigen?
    # wenn sens=1, wird „-->Austausch“ angezeigt
    # wenn „sens“ = 2, wird „<-- Austausch“ ohne die letzten beiden RCLF-Zeichen angezeigt
    if sens == 1:
        print(f"--> [{echange}]")
        return
    elif sens == 2:
        l = len(echange)
        print(f"<-- [{echange[0:l - 2]}]")
        return


# Ende ----------------------------------------------------------------

# Client SMTP (SendMail Transfer Protocol) zum Senden einer Nachricht
# Die Informationen werden aus einer Konfigurationsdatei entnommen, die für jeden Server folgende Angaben enthält

# Beschreibung: Beschreibung der gesendeten E-Mail
# smtp-server: Server SMTP
# smtp-port: Port des Servers SMTP
# from: Absender
# An: Empfänger
# Betreff: Betreff der E-Mail
# Nachricht: Inhalt der E-Mail


# Kommunikationsprotokoll SMTP Client-Server
# -> Der Client verbindet sich über Port 25 mit dem SMTP-Server
# <- Der Server sendet ihm eine Willkommensnachricht
# -> Der Client sendet den Befehl EHLO: Name seines Rechners
# <- Der Server antwortet mit OK oder nicht
# -> Der Client sendet den Befehl „mail from: <Absender>“
# <- Server antwortet mit OK oder nicht
# -> Der Client sendet den Befehl „rcpt to: <Empfänger>“
# <- Server antwortet mit OK oder nicht
# -> Der Client sendet den Befehl „data“
# <- Server antwortet mit OK oder nicht
# -> Der Client sendet alle Zeilen seiner Nachricht und schließt mit einer Zeile ab, die nur das Zeichen enthält.
# <- Server antwortet mit OK oder nicht
# -> Der Client sendet den Befehl „quit“
# <- Der Server antwortet mit „OK“ oder nicht

# Die Antworten des Servers haben die Form „xxx Text“, wobei „xxx“ eine dreistellige Zahl ist. Jede Zahl „xxx“ >= 500
# deutet auf einen Fehler hin. Die Antwort kann mehrere Zeilen umfassen, die alle mit „xxx-“ beginnen – mit Ausnahme der letzten
# in der Form xxx (Leerzeichen)

# Die ausgetauschten Textzeilen müssen mit den Zeichen RC(#13) und LF(#10) enden

# Konfiguration der Anwendung
import config
config = config.configure()

# E-Mails werden einzeln verarbeitet
for mail in config['mails']:
    try:
        # Protokolle
        print("----------------------------------")
        print(f"Envoi du message [{mail['description']}]")
        # Vorbereitung der zu versendenden Nachricht
        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']}"
        # Versenden der Nachricht im ausführlichen Modus
        sendmail(mail, True)
        # Ende
        print("Message envoyé...")
    except BaseException as erreur:
        # Der Fehler wird angezeigt
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
    # nächste E-Mail

Anmerkungen

  • Zeilen 134–136: Die Anwendung wird konfiguriert;
  • Zeilen 139–151: Alle in der Konfiguration gefundenen E-Mails werden sortiert;
  • Zeilen 141–143: Es wird angezeigt, was als Nächstes geschieht;
  • Zeilen 144–149: Die zu versendende Nachricht wird definiert. Der Nachricht [message] werden die Kopfzeilen [From, To, Subject, Content-type] vorangestellt;
  • Zeile 151: Der Versand der E-Mail erfolgt über die Funktion [sendmail], die zwei Parameter akzeptiert:
    • [mail]: das Dictionary, das die für den Versand der E-Mail erforderlichen Informationen enthält;
    • [verbose]: Ein boolescher Wert, der angibt, ob der Client-Server-Datenaustausch in der Konsole protokolliert werden soll oder nicht;
  • Zeilen 154–156: Alle Ausnahmen, die aus der Funktion [sendmail] stammen, werden abgefangen. Sie werden angezeigt;
  • Zeile 6: [mail] ist das Dictionary, das die zu versendende E-Mail beschreibt;
  • Zeile 14: Im Protokoll SMTP muss der Client seinen Namen senden. Hier wird der Name des lokalen Rechners abgerufen, der als Client dienen soll;
  • Zeile 16: Verbindung zum Server SMTP, an den die Nachricht gesendet wird;
  • Zeilen 22–23: Wenn die Verbindung zum Server SMTP hergestellt wurde, sendet dieser eine Begrüßungsnachricht, die hier ausgelesen wird;
  • Die Funktion [sendmail] sendet anschließend die verschiedenen Befehle, die ein SMTP-Client senden muss:
    • Zeilen 24–25: der Befehl EHLO;
    • Zeilen 26–27: der Befehl MAIL FROM:;
    • Zeilen 28–29: den 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 Abschlusszeichens;
    • Zeilen 44–457: Der Befehl QUIT, der den Dialog des Clients mit dem Server SMTP beendet;
  • die Ausführung von [sendmail] erfolgt innerhalb eines [try / finally], der alle Ausnahmen an den aufrufenden Code weiterleitet. Es ist bekannt, dass dieser alle Ausnahmen abfängt, um sie anzuzeigen;
  • Zeilen 48–50: Freigabe der Ressourcen;
  • Zeile 54: Die Funktion [send_command] ist dafür zuständig, die Befehle des Clients an den Server SMTP zu senden. Sie akzeptiert vier Parameter:
    • [connexion]: die Verbindung zwischen Client und Server;
    • [commande]: der zu sendende Befehl;
    • [verbose]: Wenn TRUE, dann wird der Austausch zwischen Client und Server 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 Protokolls SMTP erforderlich, aber [send_command] dient ebenfalls zum Senden der Nachricht. Hier 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 in Form einer Byte-Kette UTF-8 an den Server gesendet;
  • Zeilen 70–71: Einlesen aller Zeilen der Antwort. Es wird davon ausgegangen, dass sie weniger als 1000 Zeichen umfasst. Die Antwort kann mehrere Zeilen umfassen. Jede Zeile hat die Form XXX-YYY, wobei XXX ein numerischer Code ist, mit Ausnahme der letzten Zeile der Antwort, die die Form XXX YYY hat (ohne das Zeichen „-“);
  • Zeile 76: Auslesen des Fehlercodes 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 liefert folgende Konsolenergebnisse:


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

Die Ergebnisse in Thunderbird lauten wie folgt:

Image

Image

Der bisherige Client weist mindestens zwei Mängel auf:

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

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

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


def configure() -> dict:
    return {
        # Beschreibung: Beschreibung der gesendeten E-Mail
        # smtp-server: Server SMTP
        # SMTP-Port: Port des Servers SMTP
        # from: Absender
        # An: Empfänger
        # Betreff: Betreff der E-Mail
        # Nachricht: E-Mail-Nachricht
        "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",
                # Wir testen Zeichen mit Akzenten
                "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",
                # Wir testen Zeichen mit Akzenten
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP mit Authentifizierung
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlh@1QZ3TG",
            }
        ]
    }

Es sind dieselben Felder wie in der Datei „[smtp/01/config]“ enthalten, mit zwei zusätzlichen Feldern, wenn der Server „SMTP“ eine Authentifizierung anfordert:

  • Zeile 31, [user]: der Name des Benutzers, der die Verbindung authentifiziert;
  • Zeile 32, [password]: sein Passwort;

Diese beiden Felder sind nur vorhanden, wenn der kontaktierte Server SMTP eine Authentifizierung verlangt. Diese erfolgt dann über eine gesicherte Verbindung.

Der Code des Skripts [smtp/02/main.py] lautet wie folgt:


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


# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: True):
    # sendet eine Nachricht an den SMTP-Server „smtpserver“ im Namen des Absenders
    # an den Empfänger. Wenn „verbose=True“, wird der Client-Server-Austausch protokolliert

    # Es wird die Bibliothek „smtplib“ verwendet
    # Ausnahmen werden weitergeleitet
    #
    # der Server SMTP
    server = smtplib.SMTP(mail["smtp-server"])
    # Verbose-Modus
    server.set_debuglevel(verbose)
    # Sichere Verbindung?
    if "user" in mail:
        # Sichere Verbindung
        server.starttls()
        # EHLO Befehl + Authentifizierung
        server.login(mail["user"], mail["password"])

   # Erstellung einer Multipart-Nachricht – diese Multipart-Nachricht wird gesendet
    msg = MIMEText(mail["message"])
    msg['from'] = mail["from"]
    msg['to'] = mail["to"]
    msg['date'] = formatdate(localtime=True)
    msg['subject'] = mail["subject"]
    # Die Nachricht wird gesendet
    server.send_message(msg)
    # Beenden
    server.quit()


# Hauptprogramm ----------------------------------------------------------------

# Die Informationen werden aus einer Konfigurationsdatei entnommen, die für jeden Server folgende Angaben enthält

# Beschreibung: Beschreibung der gesendeten E-Mail
# smtp-server: Server SMTP
# smtp-port: Port des Servers SMTP
# from: Absender
# An: Empfänger
# Betreff: Betreff der E-Mail
# content-type: Kodierung der E-Mail
# Nachricht: Inhalt der E-Mail


# Anwendungskonfiguration
import config
config = config.configure()

# E-Mails werden nacheinander verarbeitet
for mail in config['mails']:
    try:
        # Protokolle
        print("----------------------------------")
        print(f"Envoi du message [{mail['description']}]")
        # Versenden der Nachricht im ausführlichen Modus
        sendmail(mail, True)
        # Ende
        print("Message envoyé...")
    except BaseException as erreur:
        # Anzeige des Fehlers
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
    # nächste E-Mail

Kommentare

  • Zeilen 8–35: Es wird ausschließlich die Funktion [sendmail] verwendet. Diese wird fortan das Modul [smtplib] (Zeile 2) nutzen;
  • Zeile 16: Verbindung zum Server SMTP;
  • Zeile 18: Bei [verbose=True] wird der Client-Server-Datenaustausch auf der Konsole angezeigt;
  • Zeilen 20–24: Eine eventuelle Authentifizierung wird durchgeführt, falls der Server SMTP dies verlangt;
  • Zeile 22: Die Authentifizierung erfolgt über eine sichere Verbindung;
  • Zeile 24: Authentifizierung;
  • Zeilen 26–33: Senden der Nachricht. Der Dialog mit dem Skript [smtp/01/main] wird nun ausgeführt. Falls eine Authentifizierung stattgefunden hat, erfolgt dies über eine sichere Verbindung;
  • Zeile 35: Der Client-Server-Dialog wird beendet;

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

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

Machen Sie dasselbe mit dem zweiten Gmail-Konto [pymail2parlexemple@gmail.com].

Ergebnisse

Wenn man das Skript [smtp/02/main] ausführt, erhält man die folgenden Konsolenergebnisse:


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] leitet den Dialog ein, um eine verschlüsselte Verbindung mit dem Server SMTP herzustellen, was im Skript [smtp/main/01] nicht gelungen war;
  • ansonsten finden sich die bekannten Befehle des Protokolls SMTP;

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

Image

21.5.8. Skripte [smtp/03]: Verwaltung von Dateianhängen

Wir ergänzen das Skript [smtp/02/main], damit die versendete E-Mail Dateianhänge enthalten kann.

Image

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


import os


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

    return {
        # Beschreibung: Beschreibung der gesendeten E-Mail
        # smtp-server: Server SMTP
        # SMTP-Port: Port des Servers SMTP
        # from: Absender
        # An: Empfänger
        # Betreff: Betreff der E-Mail
        # Nachricht: E-Mail-Nachricht
        "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",
                # Wir testen Zeichen mit Akzenten
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP mit Authentifizierung
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # Hier müssen absolute Pfade für die angehängten Dateien angegeben werden
                "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 Liste [attachments] (Zeilen 30–32), die die Liste der an die zu versendende Nachricht anzuhängenden Dateien angibt.

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


# Importe
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):
    # sendet die E-Mail „mail[message]“ an den SMTP-Server „mail[smtp-server]“ im Namen von „mail[from]“
    # für mail[to]. Wenn verbose=True, wird der Client-Server-Austausch protokolliert

    # wird die Bibliothek smtplib verwendet
    # Ausnahmen werden weitergeleitet
    #
    # der Server SMTP
    server = smtplib.SMTP(mail["smtp-server"])
    # Verbose-Modus
    server.set_debuglevel(verbose)
    # Sichere Verbindung?
    if "user" in mail:
        server.starttls()
        server.login(mail["user"], mail["password"])

    # Erstellung einer Multipart-Nachricht – dies ist die Nachricht, die gesendet wird
    # Quelle: 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"]
    # Die Textnachricht wird im Format MIMEText angehängt
    msg.attach(MIMEText(mail["message"]))
    # die Anhänge werden durchlaufen
    for path in mail["attachments"]:
        # Der Pfad muss ein absoluter Pfad sein
        # Der Typ der angehängten Datei wird ermittelt
        ctype, encoding = mimetypes.guess_type(path)
        # Wenn keine Erkennung möglich war
        if ctype is None or encoding is not None:
            # Es konnte keine Erkennung vorgenommen werden oder die Datei ist verschlüsselt (komprimiert), daher
            # wird ein generischer „Bag-of-Bits“-Typ verwendet.
            ctype = 'application/octet-stream'
        # wird der Typ in Haupttyp und Untertyp zerlegt
        maintype, subtype = ctype.split('/', 1)
        # die verschiedenen Fälle werden behandelt
        if maintype == 'text':
            with open(path) as fp:
                # Hinweis: Wir sollten die Berechnung des Zeichensatzes übernehmen
                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)
        # Fälle des Typs „message“ / RFC 822
        elif maintype == 'message':
            with open(path, 'rb') as fp:
                part = MIMEMessage(email.message_from_bytes(fp.read()))
        else:
            # andere Fälle
            with open(path, 'rb') as fp:
                part = MIMEBase(maintype, subtype)
                part.set_payload(fp.read())
            # Die Nutzdaten mit Base64 kodieren
            encoders.encode_base64(part)
        # Den Parameter „filename“ festlegen
        basename = os.path.basename(path)
        part.add_header('Content-Disposition', 'attachment', filename=basename)
        # Die Datei wird an die zu versendende Nachricht angehängt
        msg.attach(part)
    # Alle Anhänge wurden hinzugefügt – die Nachricht wird als Zeichenkette gesendet
    server.send_message(msg)


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

..

Kommentare

  • Zeilen 18–32: Die Funktion [sendmail] bleibt unverändert gegenüber der Situation, als noch keine Anhänge vorhanden waren;
  • Zeile 35: Der folgende Code stammt aus einer offiziellen Python-Dokumentation;
  • Zeile 36: Die zu versendende Nachricht wird aus mehreren Teilen bestehen: Text und angehängte Dateien. Man bezeichnet dies als eine [Multipart]-Nachricht;
  • Zeilen 37–40: Die Nachricht [Multipart] enthält die üblichen Felder einer E-Mail;
  • Zeile 42: Die verschiedenen Teile der Nachricht [Multipart] [msg] werden mittels der Methode [msg.attach] (Zeile 81) an die Nachricht angehängt. Die angehängten Teile können beliebiger Art sein. Diese sind durch einen Typ MIME gekennzeichnet. Der Typ MIME eines gewöhnlichen Textes ist der Typ [MIMEText];
  • Zeilen 44–81: An die Nachricht [msg Multipart] werden alle Anhänge der zu versendenden Nachricht angehängt (Zeile 81);
  • Zeile 44: [path] steht für den absoluten Pfad der anzuhängenden Datei;
  • Zeile 47: Um den Typ MIME zu ermitteln, der für den Anhang verwendet werden soll, wird die Dateiendung (.docx, .php…) der anzuhängenden Datei herangezogen. Die Methode [mimetypes.guess_type] übernimmt diese Aufgabe. Sie liefert zwei Informationen:
    • [ctype]: den Typ MIME der Datei;
    • [encoding]: eine Angabe zur Kodierung;
  • Zeilen 49–52: Falls der Dateityp MIME nicht bestimmt werden kann, wird angegeben, dass es sich um eine Binärdatei handelt (Zeile 52);
  • Zeile 54: Der Dateityp MIME setzt sich aus einem Haupttyp und einem Nebentyp zusammen, zum Beispiel [application/pdf]. Diese beiden Elemente werden voneinander getrennt;
  • Zeilen 56–76: Je nach Wert des Haupttyps MIME werden verschiedene Fälle behandelt. Im Fall von [application/pdf] einer Datei vom Typ 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, dass die Datei ein Bild enthält. In diesem Fall wird ein Element vom Typ [MIMEImage] mit dem Inhalt [fp.read] erstellt;
    • Zeilen 63–65: Der Fall, dass 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, dass es sich bei der Datei um eine E-Mail handelt. In diesem Fall wird ein Element vom Typ [MIMEMessage] (Zeile 69) mit dem Inhalt [email.message_from_bytes(fp.read())] erstellt. Im Gegensatz zu den vorherigen Fällen, in denen der Inhalt des Elements MIME der Binärinhalt der zugehörigen Datei war, ist hier der Inhalt des Elements MIMEMessage vom Typ [email.message.Message];
    • Zeilen 70–76: die übrigen Fälle. Dazu gehören beispielsweise die Word-Dateien und PDF aus 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] führt zwei Schritte aus:
    • Sie öffnet die Datei und weist ihr den Deskriptor [file] zu;
    • sie stellt sicher, dass beim Verlassen von [with] – unabhängig davon, ob ein Fehler auftritt oder nicht – der Deskriptor [file] geschlossen wird. Es handelt sich also um eine Alternative zur Struktur [try file=open(…)/ finally];
  • Zeile 73: Es wird ein neues Element [part] erstellt, das in die Multipart-Nachricht eingebettet werden soll. Hier wird die Klasse [MIMEBase] verwendet, und dem Konstruktor werden die in Zeile 54 festgelegten Elemente [maintype, subtype] übergeben;
  • Zeile 74: Das in die Multipart-Nachricht einzubindende Element muss einen Inhalt haben. Dieser kann mit der Methode [set_payload] initialisiert werden;
  • Zeilen 75–76: Die angehängten Dateien müssen einer 7-Bit-Kodierung unterzogen werden. Tatsächlich unterstützten einige Server SMTP in der Vergangenheit nur 7-Bit-kodierte Zeichen. Hier wird die als „Base64“ bezeichnete Kodierung verwendet;
  • Zeile 77: Ab dieser Zeile gilt die Verarbeitung für alle Typen MIME, die wir in den Zeilen 56–76 [MIMEMessage, MIMEImage, MIMEAudio, MIMEBase, MIMEText] erstellt haben;
  • Zeile 79: Das Element, das der Multipart-Nachricht hinzugefügt werden soll, verfügt über einen Header, der es beschreibt. Hier wird angegeben, dass es sich bei dem hinzugefügten Element um eine angehängte Datei handelt. Der Name dieser Datei ist der dritte Parameter, der an die Methode [add_header] übergeben wird. Der Name dieser Datei wird häufig von E-Mail-Programmen verwendet, um die angehängte Datei unter diesem Namen im Dateisystem des Programms zu speichern. Bislang haben wir mit dem absoluten Pfadnamen der angehängten Datei gearbeitet. Hier übergeben wir lediglich ihren Namen ohne den Pfad (Zeile 78);
  • Zeile 81: Der Binärinhalt der Datei wird in die Nachricht [msg Multipart] eingebettet;
  • Zeile 83: Sobald alle Teile der Nachricht an die Nachricht „[msg Multipart]“ angehängt wurden, wird diese versendet;

Ergebnisse

Wenn man das Skript [smtp/03/main] mit der bereits vorgestellten Datei [smtp/02/config] ausführt, erhält das Konto [pymail2parlexemple@gmail.com] Folgendes:

Image

Die angehängten Dateien sind in [4, 9-11] zu sehen.

Sehen wir uns nun ein Beispiel mit einer E-Mail-Anhang an. Wir speichern die oben genannte E-Mail unter dem Namen [3]:

Image

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

Nun ändern wir die Datei „[smtp/03/config]“ wie folgt:


import os


def configure() -> dict:
    # Konfiguration der Anwendung
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        # Beschreibung: Beschreibung der gesendeten E-Mail
        # smtp-server: Server SMTP
        # SMTP-Port: Port des Servers SMTP
        # from: Absender
        # An: Empfänger
        # Betreff: Betreff der E-Mail
        # Nachricht: E-Mail-Nachricht
        "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",
                # Wir testen Zeichen mit Akzenten
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP mit Authentifizierung
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # Hier müssen absolute Pfade für die angehängten Dateien angegeben werden
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                    f"{script_dir}/attachments/mail attaché 1.eml",
                ]
            }
        ]
    }
  • In 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 Text der Nachricht;
  • in [3]: der Text der angehängten E-Mail;
  • in [4]: Thunderbird hat 5 Anhänge gefunden:
    • [fichier attaché.docx];
    • [fichier attaché.pdf];
    • [mail attaché 1.eml]. Dieser Anhang ist selbst eine E-Mail, die zwei Anhänge enthält:
      • [fichier attaché.docx];
      • [fichier attaché.pdf];

21.6. Das Protokoll POP3

21.6.1. Einleitung

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

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

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

Image

  • [Serveur B] ist je nach Fall:
    • ein lokaler POP3-Server, der vom Mailserver [hMailServer] implementiert wird;
    • der Server [pop.gmail.com], bei dem es sich um den Server POP3 des E-Mail-Managers [gmail.com] handelt;
  • [Client A] wird ein Client von POP3 in verschiedenen Formen sein:
    • der Client [RawTcpClient] zur Erkundung des Protokolls POP3;
    • ein Python-Skript, das das Protokoll POP3 des Clients [RawTcpClient] nachbildet;
    • ein Python-Skript, das Python-Module nutzt, um angehängte Dateien zu verwalten sowie eine verschlüsselte und authentifizierte Verbindung herzustellen, wenn der Server POP3 dies verlangt;

21.6.2. Einführung in das Protokoll POP3

Wie bereits beim Protokoll SMTP werden wir nun das Protokoll POP3 anhand der Protokolle des Mail-Servers [hMailServer] untersuchen. Dazu muss dieser Server gestartet werden.

Mit Thunderbird werden wir:

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

Image

Image

In [3-6] oben ist die Nachricht zu sehen, die der Benutzer [guest@localhost.com] erhalten hat.

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

Image

Die Protokolle von POP3 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 Server POP3 sendet eine Begrüßungsnachricht an den Client (Thunderbird), der sich gerade angemeldet hat;
  • Zeile 2: Der Client sendet den Befehl [CAPA] (capabilities), um die Liste der Befehle abzufragen, die er verwenden kann;
  • Zeile 3: Der Server antwortet ihm, dass er die Befehle [USER, UIDL, TOP] verwenden kann. Der Server POP beginnt seine Antworten mit [+OK] oder [-ERR], um anzuzeigen, 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 das Postfach des Benutzers [guest] einsehen möchte;
  • Zeile 5: Der Server antwortet mit [+OK] und fragt nach dem Passwort von [guest];
  • Zeile 6: Der Client sendet den Befehl [PASS password], um das Passwort des Benutzers [guest] zu übermitteln. Hier wird das Passwort im Klartext übertragen, da der Server POP3 keine sichere Verbindung vorgeschrieben hat. Wir werden sehen, dass dies beim Gmail-Server POP3 anders sein wird;
  • Zeile 7: Der Server hat die Kombination aus Benutzername und Passwort validiert. Er gibt an, dass er das Postfach des Benutzers [guest] sperrt;
  • Zeile 8: Der Client sendet ihm den Befehl [STAT], der Informationen zum Postfach anfordert;
  • Zeile 9: Der Server antwortet, dass eine Nachricht mit einer Größe von 612 Byte 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 ihm die Liste der Nachrichten in folgender Form:
    • eine zusammenfassende Zeile mit der Anzahl der Nachrichten und ihrer Gesamtgröße;
    • eine Zeile pro Nachricht, in der die Nummer der Nachricht und ihre Größe angegeben sind;
  • Zeile 13: Der Client sendet den Befehl [UIDL], der die Liste der Nachrichten mit ihren Identifikationsnummern anfordert. Tatsächlich wird jede Nachricht innerhalb des E-Mail-Dienstes durch eine eindeutige Nummer gekennzeichnet;
  • Zeile 14: Die Antwort des Servers. Daraus geht hervor, dass die Nachricht Nr. 1 in der Liste die Kennung 42 hat;
  • Zeile 15: Der Client sendet den Befehl [RETR 1], mit dem er die Übertragung der Nachricht Nr. 1 aus der Liste anfordert;
  • Zeile 16: Der Server POP3 führt dies aus;
  • Zeile 17: Der Client sendet den Befehl [QUIT], um anzugeben, dass er die Verbindung zum Server POP3 trennen wird;
  • Zeile 18: Der Server wird ebenfalls die Verbindung zum Client beenden, sendet ihm zuvor jedoch eine Abschiedsmeldung;

Wir werden nun Teile des obigen Dialogs nachstellen, indem wir den Client [RawTcpClient] verwenden, der in einem Fenster mit der Nummer PyCharm ausgeführt wird:

Image

Der Dialog sieht wie folgt aus:


(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: Es wird eine Verbindung zum Port 110 des Rechners [localhost] hergestellt. Dort läuft der Dienst POP3 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 Servers POP3;
  • Zeile 5: Wir geben an, dass wir auf das Postfach des Benutzers zugreifen möchten: [guest];
  • Zeile 7: Das Passwort des Benutzers [guest] wird im Klartext gesendet;
  • Zeile 9: Die Liste der Nachrichten im Postfach wird angefordert;
  • Zeile 13: Die Nachricht Nr. 1 wird angefordert;
  • Zeilen 14–33: Der Server POP3 sendet die Nachricht Nr. 1;
  • Zeile 34: Die Sitzung wird beendet;

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

  • Der Befehl [USER] dient dazu, den Benutzer festzulegen, dessen E-Mail-Postfach man lesen möchte;
  • Der Befehl [PASS] dient dazu, das Passwort festzulegen;
  • Der Befehl [LIST] fordert die Liste der im Postfach des Benutzers vorhandenen Nachrichten an;
  • Der Befehl [RETR] fordert die Anzeige der Nachricht an, deren Nummer übergeben wird;
  • Der Befehl [DELE] fordert das Löschen der Nachricht an, deren Nummer übergeben wird;
  • Der Befehl [QUIT] teilt dem Server mit, dass der Vorgang abgeschlossen ist;

Die Antwort des Servers kann verschiedene Formen annehmen:

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

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

Image

Da das Protokoll POP3 dieselbe Struktur wie das Protokoll SMTP hat, ist das Skript [pop3/01/main.py] eine Portierung des Skripts [smtp/01/main.py]. Es wird die folgende Konfigurationsdatei [pop3/01/config.py] haben:


def configure() -> dict:
    # die Postfächer, aus denen E-Mails abgerufen werden
    mailboxes = [
        # Server: Server POP3
        # Port: Port des Servers POP3
        # Benutzer: Benutzer, dessen Nachrichten man lesen möchte
        # Passwort: sein Passwort
        # maxmails: die maximale Anzahl der herunterzuladenden E-Mails
        # timeout: maximale Wartezeit auf eine Antwort des Servers
        # encoding: Kodierung der empfangenen E-Mails
        # delete: Wenn „True“, werden die E-Mails aus dem Postfach gelöscht
        # sobald sie lokal heruntergeladen wurden

        {
            "server": "localhost",
            "port": "110",
            "user": "guest",
            "password": "guest",
            "maxmails": 10,
            "timeout": 1.0,
            "encoding": "utf-8",
            "delete": False
        }
    ]
    # wird die Konfiguration zurückgesetzt
    return {
        "mailboxes": mailboxes
    }
  • Zeilen 3–24: Liste der abzufragenden 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 Server POP3 ist der lokale Server [hMailServer];
  • Zeilen 17–18: Es soll das Postfach des Benutzers [guest@localhost] ausgelesen werden;
  • Zeile 19: Es werden höchstens 10 E-Mails gelesen;
  • Zeile 20: Der Client wartet höchstens 1 Sekunde auf eine Antwort des Servers;
  • Zeile 21: Die Kodierungsart der gelesenen Nachrichten;
  • Zeile 22: Die heruntergeladenen Nachrichten werden nicht gelöscht;

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


# Importe
import re
import socket


# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
    # liest das durch das Wörterbuch beschriebene Postfach [mailbox]
    # Wenn „verbose=True“, protokolliert es den Client-Server-Datenaustausch



# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
    # sendet Befehl an den Verbindungskanal
    # Ausführlicher Modus, wenn „verbose=True“
    # wenn with_rclf=True, fügt die Sequenz „rclf“ zum Datenaustausch hinzu
    # gibt die erste Zeile der Antwort zurück



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


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

# Client POP3 (Post Office Protocol) zum Abrufen von Nachrichten aus einem Postfach
# Kommunikationsprotokoll POP3 Client-Server
# -> Der Client verbindet sich über Port 110 mit dem SMTP-Server
# <- Der Server sendet ihm eine Begrüßungsnachricht
# -> Der Client sendet den Befehl USER „Benutzer“
# <- Der Server antwortet mit „OK“ oder nicht
# -> Der Client sendet den Befehl PASS mot_de_passe
# <- Server antwortet mit OK oder nicht
# -> Der Client sendet den Befehl LIST
# <- Server antwortet mit OK oder nicht
# -> Der Client sendet den Befehl RETR, jeweils eine Nummer für jede E-Mail
# <- Server antwortet mit OK oder nicht. Bei OK wird der Inhalt der angeforderten E-Mail gesendet
# -> Der Server sendet alle Zeilen der E-Mail und schließt mit einer Zeile ab, die das
# einzige Zeichen.
# -> Der Client sendet den Befehl DELE-Nr., um eine E-Mail zu löschen
# <- Server antwortet mit OK oder nicht
# # -> Der Client sendet den Befehl QUIT, um den Dialog mit dem Server zu beenden
# <- Server antwortet mit OK oder nicht
# Die Antworten des Servers haben das Format +OK Text oder -ERR Text
# Die Antwort kann mehrere Zeilen umfassen. In diesem Fall besteht die letzte Zeile aus einem einzigen Punkt
# Die ausgetauschten Textzeilen müssen mit den Zeichen RC(#13) und LF(#10) enden
# 

# Die Konfiguration der Anwendung wird abgerufen
import config
config = config.configure()

# Die E-Mail-Postfächer werden nacheinander verarbeitet
for mailbox in config['mailboxes']:
    try:
        # Konsolenausgabe
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
        # Auslesen des Postfachs im ausführlichen Modus
        readmails(mailbox, True)
        # Ende
        print("Lecture terminée...")
    except BaseException as erreur:
        # Der Fehler wird angezeigt
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass

Kommentare

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

  • Zeile 64: Die Funktion [readmails] ist dafür zuständig, die E-Mails aus einem Postfach auszulesen. Die Informationen für die Verbindung zu diesem Postfach befinden sich im Wörterbuch [mailbox]. Der zweite Parameter [True] ist der Parameter [Verbose], der hier eine Überwachung des Client-Server-Datenaustauschs anfordert;

Die Funktion [readmails] hat folgende Aufgabe:


# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
    # Liest die E-Mails aus dem im Wörterbuch beschriebenen Postfach [mailbox]
    # Wenn „verbose=True“, wird der Client-Server-Datenaustausch protokolliert

    # Die Parameter des Postfachs werden isoliert
    # Es wird angenommen, dass das Wörterbuch [mailbox] gültig ist
    server = mailbox['server']
    port = int(mailbox['port'])
    user = mailbox['user']
    password = mailbox['password']
    maxmails = mailbox['maxmails']
    delete = mailbox['delete']
    timeout = mailbox['timeout']

    # Systemfehler werden weitergeleitet
    connexion = None
    try:
        # Aufbau einer Verbindung über den Port [port] von [server] mit einem Timeout von einer Sekunde
        connexion = socket.create_connection((server, port), timeout=timeout)

        # Die Verbindung stellt einen bidirektionalen Kommunikationsfluss dar
        # zwischen dem Client (diesem Programm) und dem kontaktierten POP3-Server
        # Dieser Kanal wird für den Austausch von Befehlen und Informationen genutzt

        # Willkommensnachricht lesen
        send_command(mailbox, connexion, "", verbose, True)
        # Befehl USER
        send_command(mailbox, connexion, f"USER {user}", verbose, True)
        # Befehl PASS
        send_command(mailbox, connexion, f"PASS {password}", verbose, True)
        # Befehl LIST
        première_ligne = send_command(mailbox, connexion, "LIST", verbose, True)
        # Analyse der ersten Zeile, um die Anzahl der Nachrichten zu ermitteln
        match = re.match(r"^\+OK (\d+)", première_ligne)
        nbmessages = int(match.groups()[0])
        # Schleife über die Nachrichten
        imessage = 0
        while imessage < nbmessages and imessage < maxmails:
            # Befehl RETR
            send_command(mailbox, connexion, f"RETR {imessage + 1}", verbose, True)
            # Befehl DELE
            if delete:
                send_command(mailbox, connexion, f"DELE {imessage + 1}", verbose, True)
            # nächste Nachricht
            imessage += 1
        # Befehl QUIT
        send_command(mailbox, connexion, "QUIT", verbose, True)
        # Ende
    finally:
        # Verbindung wird geschlossen
        if connexion:
            connexion.close()

Kommentare

  • Zeilen 8–14: Die Konfigurationsdaten des abzurufenden Postfachs werden abgerufen;
  • Zeilen 19–20: Aufbau einer Verbindung zum Server POP3;
  • Zeilen 26–27: Die vom Server gesendete Begrüßungsnachricht wird gelesen;
  • Zeilen 28–29: Der Befehl [USER] wird gesendet, um den Benutzer zu identifizieren, dessen E-Mails abgerufen werden sollen;
  • Zeilen 30–31: Der Befehl [PASS] wird gesendet, um das Passwort dieses Benutzers anzugeben;
  • Zeilen 32–33: Der Befehl [LIST] wird gesendet, um zu erfahren, wie viele E-Mails sich im Postfach dieses Benutzers befinden. Die Funktion [sendCommand] gibt die erste Zeile der Antwort des Servers zurück. Darin 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: Es wird eine Schleife über alle Nachrichten durchlaufen. Für jede davon werden zwei Befehle gesendet:
    • RETR i: zum Abrufen der Nachricht Nr. i (Zeilen 40–41);
    • DELE i: um sie zu löschen, falls die Konfiguration vorsieht, dass gelesene Nachrichten vom Server gelöscht werden (Zeilen 43–44);
  • Zeilen 47–48: Der Befehl [QUIT] wird gesendet, um dem Server mitzuteilen, dass der Vorgang abgeschlossen ist;

Die Funktion [send_command] lautet wie folgt:


# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
    # Befehl im Verbindungskanal senden
    # Ausführlicher Modus, wenn verbose=True
    # wenn with_rclf=True, fügt die Sequenz rclf zum Austausch hinzu
    # gibt die erste Zeile der Antwort zurück

    # Zeichen für Zeilenende
    if with_rclf:
        rclf = "\r\n"
    else:
        rclf = ""
    # Befehl senden, wenn nicht leer
    if commande:
        connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
        # mögliches Echo
        if verbose:
            affiche(commande, 1)
    # Auslesen des Sockets, als wäre es eine Textdatei
    encoding = f"{mailbox['encoding']}" if mailbox['encoding'] else None
    file = connexion.makefile(encoding=encoding)
    # Diese Datei wird zeilenweise verarbeitet
    # Lesen der ersten Zeile
    première_ligne = réponse = file.readline().strip()
    # Ausführlicher Modus?
    if verbose:
        affiche(première_ligne, 2)
    # Fehlercode abrufen
    code_erreur = réponse[0]
    if code_erreur == "-":
        # Es ist ein Fehler aufgetreten
        raise BaseException(réponse[5:])
    # Sonderfall bei mehrzeiligen Antworten: LIST, RETR
    cmd = commande.lower()[0:4]
    if cmd == "list" or cmd == "retr":
        # Letzte Zeile der Antwort?
        dernière_ligne = False
        while not dernière_ligne:
            # nächste Zeile lesen
            ligne_suivante = file.readline().strip()
            # Ausführlicher Modus?
            if verbose:
                affiche(ligne_suivante, 2)
            # letzte Zeile?
            dernière_ligne = ligne_suivante == "."
    # Fertig – die erste Zeile wird zurückgegeben
    return première_ligne

Kommentare

  • Zeilen 13–18: Der Befehl [command] wird nur dann an den Server POP3 gesendet, wenn er nicht leer ist. Dies ist erforderlich, um die Begrüßungsnachricht des Servers POP3 zu lesen, die dieser sendet, obwohl der Client noch keine Befehle gesendet hat;
  • Zeilen 19–21: Der Socket wird so gelesen, als wäre er eine Textdatei. Dies ermöglicht es uns, die Methode [readline] (Zeile 24) zu verwenden und die Nachricht so zeilenweise zu lesen. Wir verwenden den Schlüssel [encoding] aus dem Wörterbuch [mailbox], um die Kodierung der Zeilen anzugeben, die gelesen werden sollen;
  • Zeile 24: Wir lesen die erste Zeile der Antwort;
  • Zeilen 28–32: Der Fall eines möglichen Fehlers wird behandelt. Diese gehören zum Typ [-ERR invalid password, -ERR mailbox unknown, -ERR unable to lock mailbox…];
  • Zeile 32: Es wird eine Ausnahme mit der Fehlermeldung ausgelöst;
  • Zeile 35: Nur die Befehle vom Typ [list, retr] können mehrzeilige Antworten haben;
  • Zeilen 36–45: Im Falle einer mehrzeiligen Antwort werden alle empfangenen Zeilen (Zeilen 42–43) angezeigt, bis die letzte Zeile empfangen wurde (Zeile 45);
  • Zeile 46: Die erste gelesene Zeile wird zurückgegeben, da sie im Fall des Befehls [LIST] die Anzahl der im Postfach vorhandenen Nachrichten enthält;

Ergebnisse

Nehmen wir das vorherige Beispiel. Mit Thunderbird hatten wir die folgende Nachricht an den Benutzer [guest@localhost] gesendet (der Server hMailServer muss dabei gestartet sein):

Image

Bei der Ausführung erhält man 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.

Wir haben hier einen einfachen POP3-Client, dem bestimmte Funktionen fehlen:

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

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

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

Wir werden einen Client POP3 schreiben, der die Verwaltung von Anhängen sowie die Kommunikation mit sicheren Servern ermöglicht. Außerdem werden wir die Nachrichten und ihre Anhänge in Dateien speichern.

Wir werden zwei Python-Module verwenden:

  • [poplib]: Dieses Modul übernimmt das Protokoll POP3;
  • [email]: Dieses Modul umfasst zahlreiche Untermodule, mit denen wir die empfangenen Nachrichten analysieren können. Jede Nachricht ist eine strukturierte Zeichenkette, in der Folgendes zu finden ist:
    • die Kopfzeilen der Nachricht [From, To, Subject, Return-Path…];
    • die Nachricht im Textformat und gegebenenfalls im HTML-Format (HTML);
    • die angehängten Dateien;

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] lautet wie folgt:


import os


def configure() -> dict:
    # App-Konfiguration
    config = {
        # Liste der zu verwaltenden Postfächer
        "mailboxes": [
            # Server: Server POP3
            # Port: Port des Servers POP3
            # Benutzer: Benutzer, dessen Nachrichten gelesen werden sollen
            # Passwort: sein Passwort
            # maxmails: die maximale Anzahl der herunterzuladenden E-Mails
            # timeout: maximale Wartezeit auf eine Antwort des Servers
            # delete: auf „true“ gesetzt, wenn die heruntergeladenen Nachrichten vom Server gelöscht werden sollen
            # ssl: auf „true“ gesetzt, wenn die E-Mails über eine sichere Verbindung abgerufen werden
            # output: Der Ordner, in dem die heruntergeladenen Nachrichten gespeichert werden

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

    # Absolute Pfade der Ordner, die in den Syspath aufgenommen werden sollen
    absolute_dependencies = [
        # Lokaler Ordner
        f"{script_dir}/../../shared",
   ]

    # Konfiguration des Syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # Die Konfiguration wird übernommen
    return config

Die Datei definiert die Liste der abzufragenden Postfächer und legt den Python-Pfad der Anwendung fest.

Hier gibt es nur ein einziges Postfach:

  • Zeilen 22–23: der Benutzer, dessen E-Mails abgerufen werden sollen;
  • Zeilen 20–21: Name und Port des Servers POP3, auf dem die E-Mails dieses Benutzers gespeichert sind;
  • Zeile 24: die maximale Anzahl der abzurufenden E-Mails. Wenn Sie dieses Skript in Ihrem eigenen Postfach ausprobieren, möchten Sie wahrscheinlich nicht die Hunderte von E-Mails abrufen, die sich dort befinden;
  • Zeile 25: Ein boolescher Wert, der angibt, ob eine E-Mail nach dem Lesen gelöscht werden soll (delete=True);
  • Zeile 26: Das Attribut [ssl] mit dem Wert „True“ bedeutet, dass der in den Zeilen 20–21 definierte Server POP3 eine verschlüsselte Verbindung verwendet;
  • Zeile 27: Die maximale Wartezeit für Antworten des Servers, angegeben in Sekunden;
  • Zeile 28: Der Ordner, in dem die gelesenen E-Mails abgelegt werden sollen. Er wird erstellt, falls er noch nicht existiert. Hier wird ein relativer Pfad angegeben. Bei der Ausführung bezieht er sich auf den Ordner, von dem aus Sie das Skript starten. Bei [Pycharm] ist dies der Ordner des Skripts [pop3/02];

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


# Importe
import email
import os
import poplib
import shutil


# Lesen eines E-Mail-Postfachs
def readmails(mailbox: dict, verbose: bool):
    # liest das im Wörterbuch beschriebene Postfach [mailbox]
    # Wenn „verbose=True“, protokolliert den Client-Server-Datenaustausch


# main ----------------------------------------------------------------
#   POP3-Client (Post Office Protocol) zum Lesen von E-Mails

# Die Konfiguration der Anwendung wird abgerufen
import config
config = config.configure()

# die Postfächer werden nacheinander verarbeitet
for mailbox in config['mailboxes']:
    try:
        # Konsolenausgabe
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
        # Auslesen des Postfachs im ausführlichen Modus
        readmails(mailbox, True)
        # Ende
        print("Lecture terminée...")
    except BaseException as erreur:
        # Der Fehler wird angezeigt
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
  • Zeilen 17–36: Der Teil „[main]“ des Skripts entspricht dem des Skripts „[pop3/01]“;

Die Funktion [readmails] lautet wie folgt:


# Ein Postfach wird gelesen
def readmails(mailbox: dict, verbose: bool):
    # Liest die im Wörterbuch beschriebene Mailbox [mailbox]
    # Wenn „verbose=True“, wird der Client-Server-Datenaustausch protokolliert

    # Import von mail_parser
    from mail_parser import save_message

    # Die Parameter des E-Mail-Postfachs werden isoliert
    # Es wird angenommen, dass das Wörterbuch [mailbox] gültig ist
    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']

    # Systemfehler werden gemeldet
    pop3 = None
    try:
        # Die Speicherordner werden erstellt, falls sie noch nicht vorhanden sind
        if not os.path.isdir(output):
            os.mkdir(output)
        # Benutzer
        dir2 = f"{output}/{user}"
        # Der Ordner „[dir2]“ wird gelöscht, falls er vorhanden ist, und anschließend neu erstellt
        if os.path.isdir(dir2):
            # Löschen
            shutil.rmtree(dir2)
        # Erstellung
        os.mkdir(dir2)
        # Öffnen einer Verbindung am Port [port] von [server]
        if ssl:
            pop3 = poplib.POP3_SSL(server, port, timeout=timeout)
        else:
            pop3 = poplib.POP3(server, port, timeout=timeout)

        # Die Verbindung stellt einen bidirektionalen Kommunikationsfluss dar
        # zwischen dem Client (diesem Programm) und dem kontaktierten POP3-Server
        # Dieser Kanal wird für den Austausch von Befehlen und Informationen genutzt

        # Ausführlicher Modus
        pop3.set_debuglevel(2 if verbose else 0)
        # Willkommensnachricht lesen
        pop3.getwelcome(    )
        # Befehl USER
        réponse = pop3.user(user)
        # Befehl PASS
        réponse = pop3.pass_(password)
        # Befehl LIST
        liste = pop3.list()
        # Die E-Mails befinden sich in der Liste [1]
        imail = 0
        nb_mails = len(liste[1])
        fini = imail == maxmails or imail == nb_mails
        éléments = liste[1]
        while not fini:
            # aktuelles Element
            élément = éléments[imail]
            # Das Element ist eine Byte-Liste, die in eine Zeichenkette dekodiert wird
            desc = élément.decode()
            # Es handelt sich um eine durch Leerzeichen getrennte Zeichenkette
            # Das erste Element ist die Nachrichten-ID
            num = desc.split()[0]
            # Die Nachricht wird abgerufen
            message = pop3.retr(int(num))
            # Die Zeilen der Nachricht befinden sich in „message“ [1]
            str_message = ""
            for ligne in message[1]:
                # Eine Zeile ist eine Folge von Bytes, die in einen String dekodiert wird
                str_message += f"{ligne.decode()}\r\n"
            # Ordner der Nachricht
            dir3 = f"{dir2}/message_{num}"
            # Wenn der Ordner nicht existiert, wird er angelegt
            if not os.path.isdir(dir3):
                os.mkdir(dir3)
            # Betreff email.message.Message
            save_message(dir3, email.message_from_string(str_message), 0)
            # eine weitere E-Mail
            imail += 1
            # Wurde das Maximum erreicht?
            fini = imail == maxmails or imail == nb_mails

        # Bestellung QUIT
        pop3.quit()
    finally:
        # Verbindung wird geschlossen
        if pop3:
            pop3.close()

Kommentare

  • Zeilen 6–7: Die in Zeile 80 verwendete Funktion [mail_parser.save_message] wird importiert;
  • Der Funktionscode ist in einem „try“ (Zeile 22) und einem „finally“ (Zeile 88) gekapselt. So werden alle Ausnahmen an den Hauptcode weitergeleitet, der sie abfängt und anzeigt;
  • Zeilen 11–18: Die Konfigurationsinformationen des Postfachs werden abgerufen;
  • Zeilen 23–33: Alle Nachrichten werden im Ordner „[output/user]“ gespeichert, wobei „[output]“ und „[user]“ in der Konfiguration definiert sind. Daher werden nacheinander die Ordner „[output]“ und anschließend „[output/user]“ erstellt. Um letzteren zu erstellen, wird er zunächst in Zeile 31 gelöscht. [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 wird das Modul [os] verwendet, das ebenfalls importiert werden muss;
  • Zeilen 34–38: Es wird eine Verbindung zum Server POP3 hergestellt. Ist der Server gesichert, wird die Klasse [poplib.POP3_SSL] verwendet, andernfalls die Klasse [poplib.POP3]. Das in Zeile 35 verwendete Attribut [ssl] stammt aus der Konfiguration des Postfachs;
  • Zeile 45: Hier wird eine Protokollierungsstufe festgelegt:
    • 0: keine Protokolle;
    • 1: Die vom Client POP3 gesendeten Befehle werden protokolliert;
    • 2: detaillierte Protokolle. Man sieht auch, was der Client POP3 empfängt;
  • Zeile 47: Nach der Verbindung sendet der Server POP3 eine Begrüßungsnachricht. Diese wird angezeigt;
  • Zeilen 48–49: Befehl USER des Protokolls POP3;
  • Zeilen 50–51: Befehl PASS des Protokolls POP3;
  • Zeilen 52–53: Befehl LIST des Protokolls POP3. Die Antwort ist ein Tupel (response, ['mesg_num octets'…], Bytes), zum Beispiel liste=(b'+OK 3 Nachrichten (3859 Bytes)', [b'1 584', b'2 550', b'3 2725'], 22). Man sieht, dass die ersten beiden Elemente des Tupels Bytes sind (Präfix b). liste[1] ist ein Array, in dem jedes Element eine Bytefolge ist, die zwei Informationen enthält: die Nummer der Nachricht und ihre Größe in Bytes;
  • Zeile 56: Aus dem Vorstehenden lässt sich ableiten, dass die Anzahl der Nachrichten im Postfach über [taxpayers[slice(10,12)]] ermittelt werden kann;
  • Zeilen 59–84: Es wird eine Schleife über jede einzelne Nachricht durchlaufen. Die Schleife endet, wenn alle Nachrichten gelesen wurden oder die in der Konfiguration festgelegte maximale Anzahl an E-Mails erreicht ist;
  • Zeile 61: Aktuelles Element des Arrays „liste[1]“, also etwa „b'1 584'“, eine Folge von Bytes;
  • Zeile 63: Die Bytefolge wird in eine Zeichenkette umgewandelt. Nun haben wir die Zeichenkette '1 584';
  • Zeile 66: Die Nachrichtennummer wird abgerufen, hier die Zeichenkette „1“;
  • Zeile 68: Der Befehl POP3 RETR num wird gesendet. Man erhält eine Antwort der Art:

[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 mit drei Elementen;
    • message[1] ist ein Array aus Zeilen. Jede Zeile ist eine Folge von Bytes (Präfix „b“). Die vollständige Nachricht besteht aus dieser Reihe von Zeilen;
    • [Return-Path, Received, To, Subject, Message-ID, Content-Type, Content-Transfer-Encoding, Content-Language] sind die Kopfzeilen der Nachricht. Jede enthält Informationen zur empfangenen Nachricht. Anhand dieser Informationen lässt sich der Nachrichtentext (vorletztes Element des Arrays „message[1]“) abrufen;
  • Zeilen 71–73: Es wird die Zeichenkette [strMessage] erstellt, die aus allen Zeilen der Nachricht besteht. Nun liegt die Nachricht in Form einer Zeichenkette vor. Diese Nachricht kann weitere Nachrichten sowie Anhänge enthalten. Denn Anhänge liegen ebenfalls in Form einer Zeichenkette vor. Ein wichtiger Punkt ist also, dass eine E-Mail ursprünglich eine Zeichenkette ist und dass genau diese Zeichenkette analysiert werden muss, um die Anhänge, eventuell 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 angehängten Dateien im Ordner „[dir3]“;
  • Zeilen 79–80: Die Analyse der Nachricht wird an eine Funktion namens [save_message] delegiert:
    • Der erste Parameter ist [dir3], der Ordner, in dem der Inhalt der Nachricht abgelegt werden soll;
    • Der zweite Parameter ist vom Typ [email.message.Message]. Dieses Objekt verfügt über Methoden zum Abrufen der verschiedenen Teile der Nachricht (Haupttext, Anhänge) sowie aller Kopfzeilen. Um dieses Objekt nutzen zu können, muss das Modul [email] importiert werden. Mit der Funktion [email.message_from_string] lässt sich aus der Zeichenkette der Nachricht ein Objekt vom Typ [email.message.Message] 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:


# Importe
import codecs
import email.contentmanager
import email.header
import email.iterators
import email.message
import os


# Speichern einer Nachricht vom Typ email.message.Message
# Diese Funktion kann rekursiv aufgerufen werden
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # Ausgabe: Ordner zum Speichern der Nachrichten
    # email_message: die zu speichernde Nachricht
    # irfc822: Laufende Nummer der angehängten E-Mails
    #
    # Teil der Nachricht
    part = email_message
    # Die Header [From, To, Subject] befinden sich in einem der Multipart-Teile
    # oder in einem Teil [text/*], wenn kein Teil [multipart] vorhanden ist
    keys = part.keys()
    # „From“ muss Teil der Kopfzeilen sein, andernfalls enthält der Teil nicht die gesuchten Kopfzeilen
    if "From" in keys:
        # Es werden bestimmte Kopfzeilen abgerufen
        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'))}"]
        # Speichern der Kopfzeilen in einer Textdatei
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # In Datei schreiben
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # Art des Datensatzes [part]
    main_type = part.get_content_maintype()

Kommentare

  • Zeile 12: Die Funktion erhält höchstens drei Parameter:
  • [output]: der Ordner, in dem die Nachricht gespeichert werden soll (2. Parameter);
  • [email_message]: eine Nachricht vom Typ [email.message.Message]. Dieser Typ ist ein strukturierter Typ. Er enthält den Text der E-Mail sowie alle angehängten Dateien und bietet Methoden zum Abrufen der verschiedenen Elemente;
  • [irfc822]: Dieser Parameter wird verwendet, um die in [email_message] gekapselten E-Mails zu nummerieren;
  • Zeile 18: Das Objekt [email_message] wird in [part] eingefügt. Der Typ [email.message.Message] enthält Teile vom Typ [part] (Nachrichtentext, Anhänge, eingebettete E-Mails), die ebenfalls den Typ [email.message.Message] haben. Jeder Teil vom Typ [part] kann Unterteile haben. Somit ist der Typ [email.message.Message] ein Baum aus Elementen vom Typ [email.message.Message]:
    • [part.ismultipart()] entspricht [True], wenn der Teil [part] Unterteile enthält. Diese sind dann über [part.get_payload()] verfügbar;
    • wenn [part.ismultipart()] gleich [False] ist, bedeutet dies, dass man ein Blatt des Baums der ursprünglichen Nachricht erreicht hat: Dabei kann es sich handeln um:
      • um den Nachrichtentext in Form von normalem Text;
      • um den Nachrichtentext in Form eines HTML-Textes;
      • eines Anhangs (mit Ausnahme einer gekapselten Nachricht, bei der [part.ismultipart()] gleich [True] ist);
  • Aufgrund der Baumstruktur des Parameters [email.message.Message] wird die Funktion [save_message] rekursiv aufgerufen. Die Rekursion endet, sobald die Blätter des Baums erreicht sind, d. h. ein Teil [part], für den [part.ismultipart()] gleich [False] ist;
  • Zeile 21: Wir fordern die Schlüssel (oder Kopfzeilen) der aktuell analysierten Nachricht an (die aufgrund der Rekursion ein Teil der ursprünglichen Nachricht sein kann);
  • Zeilen 23–35: Wir möchten die Kopfzeilen speichern:
    • [From]: der Absender der Nachricht;
    • [To]: der Empfänger der Nachricht;
    • [Subject]: der Betreff der Nachricht;
    • [Return-Path]: der Empfänger, an den man antworten muss, wenn man antworten möchte. Diese Information ist nämlich nicht immer in [From] enthalten;
    • [User-Agent]: der Client POP3, der mit dem Server POP3 kommuniziert;
    • [Date]: Versanddatum der E-Mail;
  • Zeile 23: Nur einer der Teile einer Nachricht enthält diese Kopfzeilen. Bei den anderen Teilen wird der Code der Zeilen 23–35 ignoriert;
  • Zeilen 25–30: Es wird eine Liste mit den sechs Kopfzeilen erstellt;
  • Zeile 25: Analysieren wir den ersten Header:
    • [part.get(key)] ermöglicht es, die zum Schlüssel [key] gehörende Kopfzeile zu erhalten;
    • Dieser Header kann kodiert sein. Wenn es sich nicht um eine UTF-8-Kodierung handelt, wird der Header dekodiert, um ihn anschließend mithilfe der Funktion [decode_header] in UTF-8 neu zu kodieren;
    • 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]):


# Dekodierung der Header
def decode_header(header: object) -> str:
    # Der Header wird dekodiert
    header = email.header.decode_header(f"{header}")
    # Das Ergebnis ist ein Array – hier enthält es nur ein Element vom Typ (Header, Encoding)
    # Wenn encoding == None, dann ist header eine Zeichenkette
    # ansonsten ist es eine Liste von Bytes, die mit „encoding“ kodiert sind
    header, encoding = header[0]
    if not encoding:
        # Wenn keine Kodierung vorhanden ist
        return header
    else:
        # Wenn eine Kodierung vorhanden ist, wird dekodiert
        return header.decode(encoding)

Kommentare

  • Zeile 4: Der Header wird dekodiert:
    • Das Modul [email.header] muss importiert werden;
    • man erhält eine Liste von Tupeln [(header1,encoding1) , (header2, encoding2)…];
    • für die Kopfzeilen [From, To, Subject, Return-Path, Date] enthält die Liste nur ein Element;
    • Zeile 8: Man ruft den einzigen Header und dessen Kodierung ab:
      • Wenn [encoding==None], dann ist [header] der Header in Form einer Zeichenkette;
      • ansonsten ist [header] eine Bytefolge, die den kodierten Header darstellt;
  • Zeilen 10–11: Wenn keine Kodierung vorliegt, wird der Header ausgegeben;
  • Zeilen 12–14: Wenn eine Kodierung vorliegt, wird die abgerufene Bytefolge in eine Zeichenkette dekodiert und diese zurückgegeben;

Kommen wir zurück zur Funktion [save_message]:


# Speichern einer Nachricht vom Typ email.message.Message
# Diese Funktion kann rekursiv aufgerufen werden
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # Ausgabe: Ordner zum Speichern der Nachrichten
    # email_message: die zu speichernde Nachricht
    # irfc822: Laufende Nummer der angehängten E-Mails
    #
    # Teil der Nachricht
    part = email_message
    # Die Header [From, To, Subject] befinden sich in einem der Multipart-Teile
    # oder in einem Teil [text/*], wenn kein Teil [multipart] vorhanden ist
    keys = part.keys()
    # „From“ muss Teil der Kopfzeilen sein, andernfalls verfügt der Teil nicht über die gesuchten Kopfzeilen
    if "From" in keys:
        # Es werden bestimmte Kopfzeilen abgerufen
        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'))}"]
        # Speichern der Kopfzeilen in einer Textdatei
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # In Datei schreiben
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # Typ des Teils [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # wenn die Nachricht vom Typ „text/plain“ ist
    if type_of_part == "text/plain":
        # Textnachricht
        save_textmessage(output, part, 0)

    # ob die Nachricht vom Typ „text/html“ ist
    elif type_of_part == "text/html":
        # Nachricht HTML
        save_textmessage(output, part, 1)

    #, wenn die Nachricht aus mehreren Teilen besteht
    elif part.is_multipart():
        
    else:
        
    # die übrigen Teile werden ignoriert (kein „text/plain“, kein „text/html“, kein Anhang)
    # Der aktuelle Wert von irfc822 wird zurückgegeben (Nummerierung der im Ordner „output“ abgelegten E-Mail-Anhänge)
    return irfc822

Kommentare

  • Zeilen 1–26: Die Kopfzeilen der ursprünglichen Nachricht wurden verarbeitet;
  • Zeilen 28–31: Die Teile einer Nachricht vom Typ [email.message.Message] haben einen Haupttyp und einen Untertyp. Diese werden abgerufen;
  • Zeilen 32–35: Wenn der verarbeitete Teil den Typ [text/plain] hat, ist ein Blatt des Baums der ursprünglichen Nachricht erreicht worden. 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 hat den Wert 0 zum Speichern eines normalen Textes, 1 für einen Text vom Typ HTML;
  • Zeilen 37–40: Wenn der Teil den Typ [text/html] hat, dann ist ebenfalls ein Blatt des Baums der ursprünglichen Nachricht erreicht worden. Dies ist der Text, den der Absender in seiner Nachricht verfasst hat, diesmal im Format HTML. Nicht alle E-Mail-Programme unterstützen dieses Format;

Die Funktion [save_textmessage] hat folgende Aufgabe:


# Speichern einer Textnachricht
def save_textmessage(output: str, part: email.message.Message, type_of_text: int):
    # Kopfzeilen
    headers = []
    # Zeichensatz der Nachricht
    charset = part.get_content_charset()
    if charset is not None:
        charset = part.get_content_charset().lower()
        headers.append(f"Charset: {charset}")
    # Kodierungsmodus des Inhalts
    content_transfer_encoding = part.get("Content-Transfer-Encoding")
    if content_transfer_encoding is not None:
        headers.append(f"Transfer-Content-Encoding: {content_transfer_encoding}")
    # Der 8-Bit-Modus hat Probleme verursacht
    if content_transfer_encoding == "8bit":
        # Die Nachricht wird aus der E-Mail abgerufen
        msg = part.get_payload()
    else:
        # Die E-Mail-Nachricht wird abgerufen
        msg = email.contentmanager.raw_data_manager.get_content(part)
    # je nach Textart
    filename = None
    if type_of_text == 0:
        # Speichern der Kopfzeilen
        with codecs.open(f"{output}/headers.txt", "a", "utf-8") as file:
            # In Datei schreiben
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")
        # Textdatei für den Inhalt
        filename = f"{output}/mail.txt"
    elif type_of_text == 1:
        # HTML-Datei für den Inhalt
        filename = f"{output}/mail.html"
    # Speichern der Nachricht
    with codecs.open(filename, "w", "utf-8") as file:
        # In Datei schreiben
        file.write(msg)

Anmerkungen

  • 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…). Dies ist die Kodierung, die vom E-Mail-Programm verwendet wurde, das die Nachricht gesendet hat. Sie ist durch den Header [Content-Type] der empfangenen Nachricht bekannt;
    • eine zweite Kodierung, der der ursprüngliche Text möglicherweise unterzogen wurde, um versendet zu werden. Sie ist anhand des Headers [Transfer-Content-Encoding] der empfangenen Nachricht erkennbar;
  • 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 der Datei [headers.txt] hinzugefügt, in der bestimmte Kopfzeilen der Nachricht gespeichert sind;
  • Zeile 20: [email.contentmanager.raw_data_manager.get_content] ermöglicht es, die Nachricht mit ihrer ursprünglichen Kodierung 1 abzurufen. Die Kodierung 2 wurde entfernt. Das Objekt [email.contentmanager.raw_data_manager] verwaltet jedoch nur zwei Arten von [Transfer-Content-Encoding]:
    • [quoted-printable];
    • [base64];

Die anderen ignoriert es. Thunderbird verwendet beispielsweise die als „8bit“ bezeichnete Kodierung [Transfer-Content-Encoding]. Diese Kodierung wird ignoriert, und Nachrichten mit Akzentzeichen werden verfälscht. Die Nachricht kann dann mit der Methode [part.get_payload()] (Zeilen 15–17) abgerufen werden;

  • Zeile 21: An dieser Stelle liegt die Nachricht ohne ihre Übertragungskodierung vor, also so, wie sie vom Absender verfasst wurde;
  • Zeilen 22–37: Hier geht es um den Fall, dass eine Textnachricht gespeichert werden muss;
    • Zeilen 24–28: Die beiden in den Zeilen 9 und 13 erstellten Kopfzeilen werden in der Datei „[headers.txt]“ gespeichert. Diese Datei existiert bereits und enthält Kopfzeilen. Daher wird der Modus „a“ (Zeile 25) verwendet, um diese Datei zu öffnen. „a“ steht für „append“ (anhängen), und die neuen Kopfzeilen 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 Nachricht „HTML“ gespeichert werden soll;
    • Zeilen 34–37: Der UTF-8-Text wird in einer Datei gespeichert;

Kehren wir zur Funktion [save_message] zurück:


# Speichern einer Nachricht vom Typ email.message.Message
# Diese Funktion kann rekursiv aufgerufen werden
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # Ausgabe: Ordner zum Speichern der Nachrichten
    # email_message: die zu speichernde Nachricht
    # irfc822: Laufende Nummer der angehängten E-Mails
    #
    # Teil der Nachricht
    part = email_message
    # Die Header [From, To, Subject] befinden sich in einem der Multipart-Teile
    # oder in einem Teil [text/*], wenn kein Teil [multipart] vorhanden ist
    keys = part.keys()
    # „From“ muss Teil der Kopfzeilen sein, andernfalls verfügt der Teil nicht über die gesuchten Kopfzeilen
    if "From" in keys:
        # Es werden bestimmte Kopfzeilen abgerufen
        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'))}"]
        # Speichern der Kopfzeilen in einer Textdatei
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # In Datei schreiben
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # Typ des Teils [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # wenn die Nachricht vom Typ „text/plain“ ist
    if type_of_part == "text/plain":
        # Textnachricht
        save_textmessage(output, part, 0)

    # wenn die Nachricht vom Typ „text/html“ ist
    elif type_of_part == "text/html":
        # Nachricht HTML
        save_textmessage(output, part, 1)

    #, wenn die Nachricht aus mehreren Teilen besteht
    elif part.is_multipart():
        # Sonderfall einer E-Mail mit Anhang
        if type_of_part == "message/rfc822":
            # Erstellung eines neuen Ordners „output2“ für die angehängte E-Mail
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # Speichern der Unterteile der IRFC822-Nachricht in „output2“
            for subpart in part.get_payload():
                # im neuen Ordner „irfc822“ beginnt die Nummerierung wieder bei 0
                save_message(output2, subpart, 0)

        else:
            # Es handelt sich nicht um eine E-Mail-Anhang
            # Speichern der Unterteile im aktuellen Ordner „output“
            # irfc822 muss dann für jeden Unterteil von message/rfc822 erhöht werden
            for subpart in part.get_payload():
                # save_message liefert den letzten Wert von irfc822
                # um 1 erhöht, wenn subpart="message/rfc822" ist, andernfalls nicht erhöht
                irfc822 = save_message(output, subpart, irfc822)
    else:
        # andere Fälle (nicht text/plain, nicht text/html, nicht multipart)
        # Anhang?
        disposition = part.get('Content-Disposition')
        if disposition and disposition.startswith('attachment'):
            save_attachment(output, part)
    # die übrigen Teile werden ignoriert (nicht „text/plain“, nicht „text/html“, kein Anhang)
    # der aktuelle Wert von irfc822 wird zurückgegeben (Nummerierung der im Ordner „output“ abgelegten E-Mails mit Anhängen)
    return irfc822

Kommentare

  • Zeilen 33–40: Wir haben zwei mögliche Fälle einer Nachricht an einem Ende des ursprünglichen Nachrichtenbaums behandelt (keine Unterteile). Es bleiben noch zwei Fälle zu behandeln:
    • Zeilen 43–62: der 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 hier wieder an einem Extrem 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 durch den Schlüssel [Content-Disposition] identifiziert;
  • Zeile 67: Wenn dieser Schlüssel existiert und mit der Zeichenfolge [attachment] beginnt, handelt es sich um einen an die Nachricht angehängten Anhang;
  • Zeile 68: Der Anhang wird im Ordner „[output]“ gespeichert;

Die Funktion [save_attachment] hat folgende Aufgabe:


# Speichern eines Anhangs
def save_attachment(output: str, part: email.message.Message):
    # Name der angehängten Datei
    filename = os.path.basename(part.get_filename())

    # Der Dateiname kann kodiert sein
    # zum Beispiel =?utf-8?Q?Kurse-Tutorials-Serge-Tah=C3=A9-1568x268=2Ep
    filename = decode_header(filename)
    # Die angehängte Datei wird gespeichert
    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] ermittelt. Es wird nur der Dateiname und nicht der Pfad gespeichert;
  • Zeile 8: Dateinamen sind in der Regel verschlüsselt, und zwar auf dieselbe Weise wie die Kopfzeilen der Nachricht. Daher wird die Funktion [decode_header] verwendet, um sie zu entschlüsseln;
  • Zeile 11: Der Inhalt der angehängten Datei ist vorerst eine Zeichenkette, die durch die Kodierung (häufig Base64) des ursprünglichen Dateiinhalts in Text entstanden ist. Um diesen ursprünglichen Inhalt zu erhalten, wird die Funktion [part.get_payload(decode=True)] verwendet. Der Parameter [decode=True] gibt an, dass der Inhalt des angehängten Dokuments dekodiert werden soll. Man erhält dann eine Bytefolge;
  • Zeile 10: Diese Bytefolge wird in der Datei [output/filename] gespeichert. Der Modus „wb“ beim Öffnen der Datei steht für „write binary“;

Kehren wir zum Code der Funktion [save_message] zurück:


def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # Ausgabe: Ordner zum Speichern der Nachrichten
    # email_message: die zu speichernde Nachricht
    # irfc822: Laufende Nummer der angehängten E-Mails
    #
    # Teil der Nachricht
    part = email_message
    # Die Header [From, To, Subject] befinden sich in einem der Multipart-Teile
    # oder in einem Teil [text/*], wenn kein Teil [multipart] vorhanden ist
    keys = part.keys()
    # „From“ muss Teil der Kopfzeilen sein, andernfalls verfügt der Teil nicht über die gesuchten Kopfzeilen
    if "From" in keys:
        # Es werden bestimmte Kopfzeilen abgerufen
        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'))}"]
        # Speichern der Kopfzeilen in einer Textdatei
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # In Datei schreiben
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # Typ des Teils [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # wenn die Nachricht vom Typ „text/plain“ ist
    if type_of_part == "text/plain":
        # Textnachricht
        save_textmessage(output, part, 0)

    # ob die Nachricht vom Typ „text/html“ ist
    elif type_of_part == "text/html":
        # Nachricht HTML
        save_textmessage(output, part, 1)

    #, wenn die Nachricht aus mehreren Teilen besteht
    elif part.is_multipart():
        # Sonderfall einer E-Mail mit Anhang
        if type_of_part == "message/rfc822":
            # Erstellung eines neuen Ordners „output2“ für die angehängte E-Mail
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # Speichern der Unterteile der IRFC822-Nachricht in „output2“
            for subpart in part.get_payload():
                # im neuen Ordner „irfc822“ beginnt die Nummerierung wieder bei 0
                save_message(output2, subpart, 0)

        else:
            # Es handelt sich nicht um eine E-Mail-Anhang
            # Speichern der Unterabschnitte im aktuellen Ordner „output“
            # irfc822 muss dann für jeden Unterabschnitt „message/rfc822“ erhöht werden
            for subpart in part.get_payload():
                # save_message gibt den letzten Wert von irfc822 zurück
                # um 1 erhöht, wenn subpart="message/rfc822" ist, andernfalls nicht erhöht
                irfc822 = save_message(output, subpart, irfc822)
    else:
        # andere Fälle (nicht text/plain, nicht text/html, nicht multipart)
        # Anhang?
        disposition = part.get('Content-Disposition')
        if disposition and disposition.startswith('attachment'):
            save_attachment(output, part)
    # die übrigen Teile werden ignoriert (nicht „text/plain“, nicht „text/html“, kein Anhang)
    # der aktuelle Wert von irfc822 wird zurückgegeben (Nummerierung der im Ordner „output“ abgelegten E-Mails mit Anhängen)
    return irfc822

Kommentare

  • Wir haben die Fälle der Endpunkte des ursprünglichen Nachrichtenbaums behandelt: die Teile [text/plain, text/html et Content-Disposition=attachment;…]. Nun müssen wir noch den Fall behandeln, bei dem der analysierte Teil ein Container für Teile ist, d. h., er enthält Unterteile [part.is_multipart()==True], Zeile 41. Um zu den Endknoten des Nachrichtenbaums zu gelangen, müssen daher diese Unterteile analysiert werden;
  • Zeile 43: Der Fall, in dem der analysierte Teil den Typ [message/rfc822] hat, wird gesondert behandelt. Dies ist der Typ einer E-Mail. Es handelt sich also um den Fall, in dem eine E-Mail eine andere E-Mail als Anhang enthält;

Der Code lautet wie folgt:


    # wenn die Nachricht aus mehreren Teilen besteht
    elif part.is_multipart():
        # Sonderfall der angehängten E-Mail
        if type_of_part == "message/rfc822":
            # Erstellung eines neuen Ordners „output2“ für die angehängte E-Mail
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # Speichern der Unterteile der IRFC822-Nachricht in „output2“
            for subpart in part.get_payload():
                # im neuen Ordner „irfc822“ beginnt die Nummerierung wieder bei 0
                save_message(output2, subpart, 0)

        else:
            # Es handelt sich nicht um eine E-Mail-Anhang
            # Speichern der Unterabschnitte im aktuellen Ordner „output“
            # irfc822 muss dann für jeden Unterabschnitt „message/rfc822“ erhöht werden
            for subpart in part.get_payload():
                # save_message gibt den letzten Wert von irfc822 zurück
                # um 1 erhöht, wenn subpart="message/rfc822" ist, andernfalls nicht erhöht
                irfc822 = save_message(output, subpart, irfc822)

    return irfc822
  • Der Unterschied zwischen einem Teil vom Typ [message/rfc822] und den anderen Multipart-Teilen besteht darin, dass sich der Speicherordner ändert;
    • Zeilen 6–8: Für den Teil „[message/rfc822]“ wird der Speicherordner zu dem in Zeile 7 genannten „[output/rfc822_x]“, wobei „x“ die Nummer der angehängten E-Mail ist, 1 für die erste, 2 für die zweite usw.;
    • Zeile 21: Bei den anderen Multipart-Teilen bleibt der Speicherordner weiterhin der Ordner „[output]“ der ursprünglichen Nachricht. Der Ordner wird nicht gewechselt;
  • Zeilen 10–12: Jeder Teil wird durch einen rekursiven Aufruf von [save_message] gespeichert. Der dritte Parameter ist der Nummerierungsindex der in [subpart] gekapselten E-Mails. Zu Beginn beträgt dieser Index 0;
  • Zeile 21: Gleiche Erklärung wie für Zeile 12, jedoch ändert sich der Wert des dritten Parameters von [irfc822]. Wenn in der Schleife der Zeilen 18–21 mehrere E-Mails gekapselt sind, müssen diese in Ordnern mit dem Namen […/rfc822-1…/rfc822_2…] abgelegt werden. Daher muss der dritte Parameter der Funktion [save_message] nacheinander die Werte 1, 2, 3… annehmen. Zu diesem Zweck übernimmt [save_message] den Wert von [irfc822] (Zeile 21).

Nehmen wir ein Beispiel und gehen wir davon aus, dass die Liste der Unterteile in Zeile 18 „[subpart1, subpart2, subpart3, subpart4, subpart5]“ lautet und dass „[subpart1, subpart3, subpart5]“ E-Mail-Anhänge sind, [subpart2] ein „text/plain“-Teil und [subpart4] ein Anhang ist und dass in der Nachricht [irfc822=0] noch keine E-Mail mit Anhang aufgetreten ist. In diesem Fall:

  • (Fortsetzung)
    • [subpart1] wird in Zeile 21 gespeichert: Die Funktion [saveMessage] wird mit irfc822=0 ausgeführt;
    • [subpart1] ist eine E-Mail mit Anhang, daher wird irfc822 auf 1 gesetzt (Zeile 6 des Codes). Ein Ordner [output/irfc822_1] wird angelegt. Der von [saveMessage(ouput,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 eine E-Mail mit Anhang, daher wechselt irfc822 auf 2 (Zeile 6 des Codes). Ein Ordner [output/irfc822_2] wird angelegt. Der von [save_message(ouput,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 auf 2 gesetzt. 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 eine E-Mail mit Anhang, daher wechselt irfc822 auf 3 (Zeile 6 des Codes). Ein Ordner [output/irfc822_3] wird angelegt. Der von [save_message(ouput,subpart1,2)] zurückgegebene Wert ist daher 3 (Zeile 21);

Ausführungsbeispiele

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

  • [Gmail]: [https://mail.google.com/];
  • [Outlook]: [https://outlook.live.com/owa/];
  • [em Client]: [https://www.emclient.com/];
  • [Mozilla Thunderbird]: [https://www.thunderbird.net/fr/];

Alle E-Mails haben den Betreff „[hélène va au marché]“ und den Text „[acheter des légumes]“. Wir möchten testen, wie die Zeichen mit Akzenten wiedergegeben werden.

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


import os


def configure() -> dict:
    # App-Konfiguration
    config = {
        # Liste der zu verwaltenden Postfächer
        "mailboxes": [
            # Server: Server POP3
            # Port: Port des Servers POP3
            # Benutzer: Benutzer, dessen Nachrichten gelesen werden sollen
            # Passwort: sein Passwort
            # maxmails: die maximale Anzahl der herunterzuladenden E-Mails
            # timeout: maximale Wartezeit auf eine Antwort des Servers
            # delete: auf „true“ gesetzt, wenn die heruntergeladenen Nachrichten vom Server gelöscht werden sollen
            # ssl: auf „true“ gesetzt, wenn die E-Mails über eine sichere Verbindung abgerufen werden
            # output: Der Ordner, in dem die heruntergeladenen Nachrichten gespeichert werden

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

    # Absolute Pfade der Ordner, die in den Syspath aufgenommen werden sollen
    absolute_dependencies = [
        # Lokaler Ordner
        f"{script_dir}/../../shared",
    ]

    # Konfiguration des Syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # Die Konfiguration wird übermittelt
    return config

Das Ergebnis sieht wie folgt aus:

Image

Nachricht 1 ist die von Thunderbird gesendete:

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 vom em-Client gesendete Nachricht:

Image

Image

Es ist zu beachten, dass [em Client] die Texte in UTF-8 ([4]) kodiert und sie in [quoted-printable] und [5] überträgt. Außerdem hat er eine Kopie der Nachricht in den Formaten HTML und [7-8] versendet. Alle hier getesteten E-Mail-Programme sind dazu in der Lage. Dies ist eine Konfigurationseinstellung.

Nachricht 3 ist die von Gmail gesendete Nachricht:

Image

Es ist zu beachten, dass Gmail die Texte in UTF-8 ([3]) kodiert und sie in [quoted-printable] und [4] weiterleitet. In [6], die Version HTML der Nachricht.

Nachricht 4 ist die von Outlook gesendete Nachricht:

Image

Es ist zu beachten, dass Outlook die Texte in ISO-8859-1 ([3]) kodiert und sie in [quoted-printable] und [4] überträgt.

Die vorstehenden Beispiele zeigen zwei Dinge:

  • Unser Client [pop3/02] war funktionsfähig;
  • E-Mail-Programme haben unterschiedliche Vorgehensweisen beim Versenden einer E-Mail;

Sehen wir uns nun die angehängten Dateien an. 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:
    # Konfiguration der Anwendung
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        # Beschreibung: Beschreibung der gesendeten E-Mail
        # smtp-server: Server SMTP
        # SMTP-Port: Port des Servers SMTP
        # from: Absender
        # An: Empfänger
        # Betreff: Betreff der E-Mail
        # Nachricht: E-Mail-Nachricht
        "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",
                # Wir testen die Zeichen mit Akzenten
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP mit Authentifizierung
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # Hier müssen absolute Pfade für die angehängten Dateien angegeben werden
                "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 hinzu:
  • eine Word-Datei;
  • eine PDF-Datei;
  • eine E-Mail mit denselben beiden angehängten Dateien;

Sobald die E-Mail versendet 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 Protokoll IMAP wiederverwenden.

21.7. Das Protokoll IMAP

21.7.1. Einleitung

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

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

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

Image

  • [Serveur B] ist je nach Fall:
    • ein lokaler IMAP-Server, der vom Mailserver [hMailServer] implementiert wird;
    • der Server [imap.gmail.com:993], bei dem es sich um den Server IMAP des E-Mail-Managers [Gmail] handelt;
  • [Client A] wird ein Python-Skript sein, das Python-Module nutzt, um Anhänge zu verwalten sowie eine verschlüsselte und authentifizierte Verbindung herzustellen, wenn der Server IMAP dies verlangt;

Das Protokoll IMAP geht über das Protokoll POP3 hinaus:

  • Die E-Mails werden auf dem Server IMAP gespeichert und können in Ordnern organisiert werden;
  • Der Client IMAP kann Befehle zum Anlegen, Ändern und Löschen dieser Ordner senden;

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

Image

  • ist Thunderbird der Client A;
  • [imap.gmail.com] ist der 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 [dossier1];

Image

  • Bei [7-8] verschieben wir (mit der Maus) alle Dateien aus dem Ordner [Courrier entrant] in den Ordner [dossier1];

Nun melden wir uns auf der Gmail-Website an und geben uns als Benutzer [pymail2parlexemple@gmail.com] aus:

Image

  • Bei [2-3] ist der Posteingang leer;
  • Bei [1] ist der Ordner [dossier1], der erstellt wurde;

Image

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

Wir haben es hier mit folgender Architektur zu tun:

Image

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

Die Ordnerstruktur des Nutzers wird vom Server IMAP verwaltet. Anschließend synchronisieren sich alle Clients IMAP mit diesem Server, um dem Nutzer die Ordner seines Kontos anzuzeigen. In diesem Fall hat Thunderbird mehrere Befehle gesendet, um:

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

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

Image

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


import os


def configure() -> dict:
    # Konfiguration der App
    config = {
        # Liste der zu verwaltenden Postfächer
        "mailboxes": [
            # Server: Server IMAP
            # Port: Port des Servers IMAP
            # Benutzer: Benutzer, dessen Nachrichten gelesen werden sollen
            # Passwort: sein Passwort
            # maxmails: die maximale Anzahl der herunterzuladenden E-Mails
            # timeout: maximale Wartezeit auf eine Antwort des Servers
            # delete: auf „true“ gesetzt, wenn die heruntergeladenen Nachrichten vom Server gelöscht werden sollen
            # ssl: auf „true“ gesetzt, wenn die E-Mails über eine sichere Verbindung abgerufen werden
            # output: Der Ordner, in dem die heruntergeladenen Nachrichten gespeichert werden

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

    # Absolute Pfade der Ordner, die in den Syspath aufgenommen werden sollen
    absolute_dependencies = [
        # Lokaler Ordner
        f"{script_dir}/../shared",
    ]

    # Konfiguration des Syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # Die Konfiguration wird übernommen
    return config

Anmerkungen

  • Zeilen 8–29: Der Schlüssel [mailboxes] ist der Liste der abzufragenden Postfächer zugeordnet;
  • Zeile 20: der Server IMAP;
  • Zeile 21: sein Dienstport;
  • Zeilen 22–23: der Benutzer, dessen E-Mails gelesen werden sollen;
  • Zeile 24: die maximale Anzahl der E-Mails, die abgerufen werden sollen;
  • Zeile 25: Gibt an, ob eine sichere Verbindung zum Server IMAP hergestellt werden soll (True) oder nicht (False);
  • Zeile 26: die maximale Wartezeit auf eine Antwort des Servers;
  • Zeile 27: Ordner zum Speichern der gelesenen E-Mails;

Das Skript [imap/main] lautet wie folgt:


# Importe
import email
import imaplib
import os
import shutil


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

def readmails(mailbox: dict):
    


# main ----------------------------------------------------------------
#  Client IMAP zum Lesen von E-Mails

# Die Konfiguration der Anwendung wird abgerufen
import config
config = config.configure()

# die Postfächer werden nacheinander verarbeitet
for mailbox in config['mailboxes']:
    try:
        # Konsolenausgabe
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']} / {mailbox['server']}:{mailbox['port']}")
        # Lesen des Postfachs
        readmails(mailbox)
        # Ende
        print("Lecture terminée...")
    # außer BaseException als Fehler:
    #     # Der Fehler wird angezeigt
    #     print(f"Der folgende Fehler ist aufgetreten: {Fehler}")
    finally:
        pass

Kommentare

  • Zeilen 14–36: Hier finden wir den bereits aus dem Skript |pop3/02/main| bekannten Ablauf wieder;

Die Funktion [readmails] lautet wie folgt:


def readmails(mailbox: dict):
    # Ausnahmen werden weitergeleitet
    #
    # Modul für den E-Mail-Parser
    from mail_parser import save_message

    # Konfigurationsinformationen abrufen
    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']
    #
    # Los geht's
    imap_resource = None
    try:
        # Die Speicherordner werden erstellt, falls sie noch nicht vorhanden sind
        if not os.path.isdir(output):
            os.mkdir(output)
        # Benutzer
        dir2 = f"{output}/{user}"
        # Der Ordner „[dir2]“ wird gelöscht, falls er vorhanden ist, und anschließend neu erstellt
        if os.path.isdir(dir2):
            # Löschen
            shutil.rmtree(dir2)
        # Erstellung
        os.mkdir(dir2)
        # Verbindung zum Server IMAP
        if ssl:
            imap_resource = imaplib.IMAP4_SSL(server, port)
        else:
            imap_resource = imaplib.IMAP4(server, port)
        # Zeitüberschreitung bei der Client-Kommunikation
        sock = imap_resource.socket()
        sock.settimeout(timeout)
        # Authentifizierung
        imap_resource.login(user, password)
        # Der Ordner INBOX (Eingehende E-Mails) wird ausgewählt
        imap_resource.select('INBOX')
        # Alle Nachrichten dieses Ordners werden abgerufen: Kriterium ALL
        # keine spezielle Kodierung: None
        typ1, data1 = imap_resource.search(None, 'ALL')
        # print(f"typ={typ1}, data={data1}")

        # data1[0] ist ein Byte-Array, das die Nummern aller Nachrichten enthält, getrennt durch ein Leerzeichen
        nums = data1[0].split()
        imail = 0
        fini = imail >= maxmails or imail >= len(nums)
        # Die E-Mails werden nacheinander gelesen
        while not fini:
            # „num“ ist eine Nachrichtennummer im Binärformat
            num = nums[imail]
            # print(f"Nachricht Nr. {num}")

            # Die Nachricht mit der Nummer „num“ wird abgerufen
            typ2, data2 = imap_resource.fetch(num, '(RFC822)')
            # print(f"type={typ2}, data={data2}")

            # „data“ ist eine Liste, die Tupel enthält, hier nur eines
            # data[0] ist das Tupel, dataQZXW2HTMLBWzBdZQXQZXW2HTMLBWzFdZQX ist das zweite Element des Tupels
            # dataQZXW2HTMLBWzBdZQXQZXW2HTMLBWzFdZQX enthält eine Bytefolge, die alle Zeilen der Nachricht darstellt
            # Unter „Nachricht“ ist der Text der Nachricht sowie alle angehängten Dateien zu verstehen

            # Die Nachricht wird als Typ email.message.Message abgerufen
            message = email.message_from_bytes(data2[0][1])
            # Ordner der Nachricht
            dir3 = f"{dir2}/message_{int(num)}"
            # Wenn der Ordner nicht existiert, wird er angelegt
            if not os.path.isdir(dir3):
                os.mkdir(dir3)
            # wird gespeichert
            save_message(dir3, message)
            # nächste Nachricht
            imail += 1
            fini = imail >= maxmails or imail >= len(nums)
    finally:
        if imap_resource:
            # Die Verbindung zur Mailbox wird geschlossen
            imap_resource.close()
            # Man meldet sich vom Server IMAP ab
            imap_resource.logout()

Kommentare

  • Zeilen 7–15: Die Konfigurationselemente werden abgerufen;
  • Zeilen 19, 79: Der Code wird durch eine try/finally-Anweisung gesteuert. Ausnahmen werden also nicht abgefangen (Fehlen der except-Klausel) und werden an den aufrufenden Code weitergeleitet, der sie abbricht und anzeigt;
  • Zeilen 23–30: Der Ordner zum Speichern der E-Mails wird angelegt;
  • Zeilen 31–35: Es wird eine Verbindung zum Server IMAP hergestellt. Die verwendete Klasse hängt davon ab, ob es sich um einen gesicherten Server IMAP (IMAP4_SSL) oder einen ungesicherten Server (IMAP4) handelt;
  • Zeilen 36–38: Das Timeout für die Client-Server-Kommunikation wird festgelegt;
  • Zeilen 39–40: Man authentifiziert sich beim Server IMAP;
  • Zeilen 41–42: Wir haben gesehen, dass das Postfach eines Benutzers IMAP in Ordner unterteilt sein kann. Der Ordner [INBOX] ist der Ordner für eingehende E-Mails. Um den Ordner [dossier1] auszuwählen, würde man [imapResource.select('dossier1')] eingeben;
  • Zeilen 43–45: Es wird die Liste aller in [INBOX] gefundenen Nachrichten angefordert:
    • Der erste Parameter von [imapResource.search] ist eine Kodierungsart. [None] bedeutet „kein Filter für die Kodierung“;
    • Der zweite Parameter ist ein Kriterium. Es gibt verschiedene Möglichkeiten, dieses auszudrücken. Das Kriterium [ALL] bedeutet, dass alle Nachrichten des Ordners angefordert werden sollen;

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


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

[data] ist eine Liste, die die Nummern der gefundenen Nachrichten enthält. Diese sind binär kodiert. Oben wurden zwei Nachrichten im Ordner [INBOX] gefunden;

  • Zeile 49: Hier werden die Nummern der Nachrichten abgerufen. Oben erhalten wir die Liste [b'1' b'2'], eine Liste von binär codierten Nummern;
  • Zeilen 53–78: In einer Schleife werden die Nachrichten aus dem Ordner „[INBOX]“ gelesen;
  • Zeilen 54–55: Nachrichtennummer;
  • Zeilen 58–59: Die Nachricht mit der Nummer [num] wird vom Server IMAP angefordert;
    • der erste Parameter ist die Nummer der gewünschten Nachricht;
    • der zweite Parameter ist eine Zeichenfolge „(part1)(part2)…“, wobei [parti] der Name eines Teils der Nachricht ist. Ich habe diesen Punkt nicht näher untersucht. Der Name (RFC822) bezeichnet die gesamte E-Mail;

Man erhält etwas in folgender Form:


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 einzige 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ärzeichenfolge, die die gesamte angeforderte Nachricht darstellt. Oben sind Elemente zu erkennen, die bereits bei der Untersuchung des Moduls [mail_parser] vorgestellt wurden.

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

  • Zeile 68: Die Funktion [response.text[39:]] erstellt aus den Zeilen der Nachricht 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] des Moduls [mail_parser] aus Zeile 5 auf. Diese Funktion wurde im Abschnitt |pop3/02/main| beschrieben;
  • Zeilen 76–78: Wir kehren zur 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: Die Verbindung zum abgefragten Ordner wird geschlossen;
    • Zeile 84: Man trennt die Verbindung zum Server IMAP;

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