21. Internetfuncties
We gaan nu in op de internetfuncties van Python waarmee we kunnen programmeren met TCP / IP (Transfer Control Protocol / Internet Protocol).

21.1. De basisprincipes van internetprogrammering
21.1.1. Algemeen
Laten we eens kijken naar de communicatie tussen twee op afstand gelegen computers A en B:

Wanneer een AppA-toepassing op computer A wil communiceren met een AppB-toepassing op computer B via het internet, moet deze verschillende zaken weten:
- het adres IP (Internet Protocol) of de naam van computer B;
- het poortnummer waarmee de applicatie AppB werkt. Computer B kan namelijk talrijke applicaties ondersteunen die via het internet werken. Wanneer deze computer informatie uit het netwerk ontvangt, moet hij weten voor welke applicatie deze informatie bestemd is. De applicaties op machine B hebben toegang tot het netwerk via poorten, ook wel communicatiepoorten genoemd. Deze informatie is opgenomen in het pakket dat door machine B wordt ontvangen, zodat het aan de juiste applicatie kan worden afgeleverd;
- de communicatieprotocollen die door machine B worden begrepen. In onze studie zullen we uitsluitend de protocollen TCP-IP gebruiken;
- het dialoogprotocol dat door de applicatie AppB wordt geaccepteerd. Machine A en machine B gaan namelijk met elkaar „praten”. Wat ze gaan zeggen, wordt ingekapseld in de protocollen TCP-IP. Wanneer echter aan het einde van de keten de applicatie AppB de door de applicatie AppA verzonden informatie ontvangt, moet zij deze kunnen interpreteren. Dit is vergelijkbaar met de situatie waarin twee personen, A en B, via de telefoon communiceren: hun dialoog wordt via de telefoon overgebracht. De spraak wordt door telefoon A in de vorm van signalen gecodeerd, via telefoonlijnen overgebracht en komt bij telefoon B aan om daar te worden gedecodeerd. Persoon B hoort dan spraak. Hier komt het begrip dialoogprotocol om de hoek kijken: als A Frans spreekt en B deze taal niet begrijpt, kunnen A en B geen zinvolle dialoog voeren;
Daarom moeten de twee communicerende applicaties het eens zijn over het type dialoog dat ze gaan hanteren. De dialoog met een dienst ftp is bijvoorbeeld niet dezelfde als die met een dienst pop: deze twee diensten accepteren niet dezelfde opdrachten. Ze hebben een verschillend dialoogprotocol;
21.1.2. De kenmerken van het protocol TCP
We zullen hier alleen netwerkcommunicatie behandelen die gebruikmaakt van het transportprotocol TCP, waarvan hier de belangrijkste kenmerken volgen:
- het proces dat gegevens wil verzenden, brengt eerst een verbinding tot stand met het proces dat de te verzenden informatie moet ontvangen. Deze verbinding wordt tot stand gebracht tussen een poort van de verzendende machine en een poort van de ontvangende machine. Tussen de twee poorten ontstaat zo een virtueel pad dat uitsluitend is gereserveerd voor de twee processen die de verbinding tot stand hebben gebracht;
- alle pakketten die door het bronproces worden verzonden, volgen dit virtuele pad en komen aan in de volgorde waarin ze zijn verzonden;
- De verzonden informatie heeft een continu karakter. Het verzendproces verstuurt informatie in zijn eigen tempo. Deze informatie wordt niet noodzakelijkerwijs onmiddellijk verzonden: het protocol TCP wacht tot er voldoende informatie is verzameld om deze te verzenden. De informatie wordt opgeslagen in een structuur die het segment TCP wordt genoemd. Zodra dit segment gevuld is, wordt het doorgestuurd naar de laag IP, waar het wordt ingekapseld in een pakket IP;
- elk segment dat via het protocol TCP wordt verzonden, is genummerd. Het ontvangende protocol TCP controleert of het de segmenten in de juiste volgorde ontvangt. Voor elk correct ontvangen segment stuurt het een ontvangstbevestiging naar de afzender;
- wanneer de afzender deze ontvangt, meldt hij dit aan het verzendende proces. Dit proces weet dan dat een segment goed is aangekomen;
- als het protocol TCP, dat een segment heeft verzonden, na een bepaalde tijd geen ontvangstbevestiging ontvangt, verzendt het het betreffende segment opnieuw, waardoor de kwaliteit van de dienst voor het doorsturen van informatie wordt gewaarborgd;
- de virtuele verbinding die tot stand is gebracht tussen de twee processen die met elkaar communiceren, is full-duplex: dit betekent dat de informatie in beide richtingen kan worden verzonden. Zo kan het bestemmingsproces ontvangstbevestigingen versturen terwijl het bronproces doorgaat met het verzenden van informatie. Hierdoor kan bijvoorbeeld het bronprotocol TCP meerdere segmenten verzenden zonder op een ontvangstbevestiging te wachten. Als het na verloop van tijd vaststelt dat het geen ontvangstbevestiging heeft ontvangen voor een bepaald segment met nummer n, zal het de verzending van de segmenten vanaf dat punt hervatten;
21.1.3. De client-serverrelatie
Vaak is de communicatie via internet asymmetrisch: machine A initieert een verbinding om een dienst aan te vragen bij machine B; hij geeft aan dat hij een verbinding wil openen met de dienst SB1 van machine B. Deze accepteert of weigert. Als machine B de verbinding accepteert, kan machine A haar verzoeken naar de dienst SB1 sturen. Deze verzoeken moeten voldoen aan het communicatieprotocol dat door de dienst SB1 wordt ondersteund. Zo ontstaat een vraag-antwoorddialoog tussen machine A, die we de clientmachine noemen, en machine B, die we de servermachine noemen. Een van beide partners zal de verbinding verbreken.
21.1.4. Architectuur van een client
De architectuur van een netwerkprogramma dat gebruikmaakt van de diensten van een servertoepassing ziet er als volgt uit:
21.1.5. Architectuur van een server
De architectuur van een programma dat diensten aanbiedt, ziet er als volgt uit:
Het serverprogramma behandelt het eerste verbindingsverzoek van een klant anders dan zijn latere verzoeken om een dienst te verkrijgen. Het programma levert de dienst zelf niet. Als het dat wel zou doen, zou het gedurende de looptijd van de dienst niet meer luisteren naar verbindingsverzoeken en zouden klanten dan niet bediend worden. Het gaat als volgt te werk: zodra een verbindingsverzoek wordt ontvangen op de luisterpoort en vervolgens wordt geaccepteerd, maakt de server een taak aan die verantwoordelijk is voor het leveren van de door de klant gevraagde dienst. Deze dienst wordt geleverd op een andere poort van de server, de zogenaamde servicepoort. Zo kunnen meerdere klanten tegelijkertijd worden bediend.
Een servicetaken heeft de volgende structuur:
21.2. Ontdek de communicatieprotocollen van het internet
21.2.1. Inleiding
Wanneer een client verbinding heeft gemaakt met een server, ontstaat er een dialoog tussen beide. De aard van deze dialoog vormt het zogenaamde communicatieprotocol van de server. Tot de meest gangbare protocollen op het internet behoren de volgende:
- HTTP: HyperText Transfer Protocol – het protocol voor de communicatie met een webserver (HTTP-server);
- SMTP: Simple Mail Transfer Protocol – het protocol voor communicatie met een e-mailverzendingsserver (server SMTP);
- POP: Post Office Protocol – het protocol voor communicatie met een e-mailopslagserver (server POP). Dit dient om ontvangen e-mails op te halen en niet om ze te verzenden;
- IMAP: Internet Message Access Protocol – het protocol voor communicatie met een e-mailopslagserver (server IMAP). Dit protocol heeft het oudere protocol POP geleidelijk vervangen;
- FTP: File Transfer Protocol – het protocol voor communicatie met een server voor bestandsopslag (server FTP);
Al deze protocollen hebben als bijzonderheid dat het tekstregelprotocollen zijn: de client en de server wisselen tekstregels uit. Als men een client heeft die in staat is om:
- een verbinding tot stand te brengen met een TCP-server;
- de tekstregels die de server hem stuurt op de console weer te geven;
- de tekstregels die een gebruiker via het toetsenbord invoert naar de server te verzenden;
Dan is het mogelijk om te communiceren met een server TCP die een tekstregelprotocol gebruikt, mits men de regels van dit protocol kent.
21.2.2. Hulpprogramma's TCP

In de bij dit document behorende codes zijn twee communicatiehulpprogramma's TCP te vinden:
- [RawTcpClient] maakt het mogelijk verbinding te maken met poort P van een server S;
- met [RawTcpServer] kunt u een server aanmaken die op een poort P op clients wacht;
Dit zijn twee C#-programma’s waarvan de broncodes aan u worden verstrekt. U kunt ze dus aanpassen.
De server TCP [RawTcpServer]wordt aangeroepen met de syntaxis [RawTcpServeur port] om een service TCP te starten op poort [port] van de lokale machine (de computer waarop u werkt):
- de server kan meerdere clients tegelijkertijd bedienen;
- de server voert de commando’s uit die de gebruiker via het toetsenbord invoert. Dit zijn de volgende:
- list: geeft een overzicht van de clients die momenteel verbonden zijn met de server. Deze worden weergegeven in de vorm [id=x-nom=y]. Het veld [id] dient om de clients te identificeren;
- send x [texte]: verstuurt tekst naar client nr. x (id=x). De vierkante haakjes [] worden niet meegestuurd. Ze zijn nodig in het commando. Ze dienen om de naar de client verzonden tekst visueel af te bakenen;
- close x: verbreekt de verbinding met klant nr. x;
- quit: sluit alle verbindingen en stopt de dienst;
- de regels die door de klant naar de server worden verzonden, worden op de console weergegeven;
- alle communicatie wordt gelogd in een tekstbestand met de naam [machine-port.txt], waarbij
- [machine] de naam is van de machine waarop de code wordt uitgevoerd;
- [port] de servicepoort is die de verzoeken van de client beantwoordt;
De client TCP [RawTcpClient] wordt aangeroepen met de syntaxis [RawTcpClient serveur port] om verbinding te maken met poort [port] van de server [serveur]:
- de regels die de gebruiker via het toetsenbord invoert, worden naar de server verzonden;
- de door de server verzonden regels worden weergegeven op de console;
- alle communicatie wordt vastgelegd in een tekstbestand met de naam [serveur-port.txt];
Laten we eens een voorbeeld bekijken. We openen twee terminalvensters PyCharm en gaan in elk daarvan naar de map met hulpprogramma’s:

In een van de vensters starten we de server [RawTcpServer] op poort 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 :
- regel 1: we bevinden ons in de map met hulpprogramma's;
- regel 1: we starten de server TCP op poort 100;
- regels 2-4: de server wacht op een client TCP en geeft een lijst weer met commando’s die de gebruiker via het toetsenbord kan invoeren;
- regel 5: de server wacht op een commando dat de gebruiker via het toetsenbord invoert;
In het andere opdrachtvenster starten we de 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) :
- regel 1: we bevinden ons in de map met hulpprogramma’s;
- regel 1: we starten de client TCP; we geven hem de opdracht om verbinding te maken met poort 100 van de lokale machine (die waarop de code van [RawTcpClient] draait);
- regel 2: de client is erin geslaagd verbinding te maken met de server. We geven de gegevens van de client weer: deze bevindt zich op de machine [DESKTOP-30FF5FB] (in dit voorbeeld de lokale machine) en gebruikt poort [51173] om met de server te communiceren;
- regel 3: de client wacht op een commando dat de gebruiker via het toetsenbord invoert;
Laten we teruggaan naar het servervenster. De inhoud is veranderd:
(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...
- regel 5: er is een client gedetecteerd. De server heeft hem nummer 1 toegewezen. De server heeft de externe client (computer en poort) correct geïdentificeerd;
- regel 6: de server wacht weer op een nieuwe client;
Laten we teruggaan naar het venster van de client en een commando naar de server sturen:
(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
- regel 4: het commando dat naar de server is verzonden;
Laten we teruggaan naar het servervenster. De inhoud is veranderd:
(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]
- regel 7, tussen vierkante haken, het bericht dat door de server is ontvangen;
Laten we een antwoord naar de klant sturen:
(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 :
- regel 8, het antwoord dat naar de klant wordt gestuurd 1. Alleen de tekst tussen de haakjes wordt verzonden, niet de haakjes zelf;
Laten we teruggaan naar het venster van de klant:
(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]
- regel 5, het antwoord dat de klant heeft ontvangen. De ontvangen tekst is die tussen de vierkante haakjes;
Laten we teruggaan naar het servervenster om andere commando's te bekijken:
(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
- regel 9, we vragen om de lijst met clients;
- regel 10, het antwoord;
- regel 11: we verbreken de verbinding met klant nr. 1;
- regel 12: de bevestiging van de server;
- regel 13: we stoppen de server;
- regel 14: de bevestiging van de server;
Laten we teruggaan naar het venster van de client:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 100
Client [DESKTOP-30FF5FB:51173] connecté au serveur [localhost-100]
Tapez vos commandes (quit pour arrêter) :
hello from client
<-- [hello from server]
Perte de la connexion avec le serveur...
- regel 6, de client heeft het einde van de dienst gedetecteerd;
Er zijn twee logbestanden aangemaakt, één voor de server en één voor de client:

- in [1], de logbestanden van de server: de bestandsnaam is de naam van de client in de vorm [machine-port]. Hierdoor kunnen er verschillende logbestanden zijn voor verschillende clients;
- in [2], de logbestanden van de client: de bestandsnaam is de naam van de server in de vorm [machine-port];
De serverlogs zijn als volgt:
<-- [hello from client]
--> [hello from server]
De logbestanden van de client zijn als volgt:
--> [hello from client]
<-- [hello from server]
21.3. De naam of het adres IP van een computer op het internet opvragen

Computers op het internet worden geïdentificeerd aan de hand van een adres (IP, IPv4 of IPv6) en meestal ook aan de hand van een naam. Maar uiteindelijk wordt alleen het adres IP gebruikt door de communicatieprotocollen van het internet. Men moet dus het adres IP kennen van een computer die bij zijn naam wordt aangeduid.
Het script [ip-01.py] is als volgt:
# imports
import socket
# ------------------------------------------------
def get_ip_and_name(nom_machine: str):
# nom_machine: naam van de machine waarvan het adres wordt gevraagd IP
try:
# nom_machine-->adres IP
ip = socket.gethostbyname(nom_machine)
print(f"ip[{nom_machine}]={ip}")
except socket.error as erreur:
# er wordt een foutmelding weergegeven
print(f"ip[{nom_machine}]={erreur}")
return
try:
# adres IP --> nom_machine
names = socket.gethostbyaddr(ip)
print(f"names[{ip}]={names}")
except socket.error as erreur:
# er wordt een foutmelding weergegeven
print(f"names[{ip}]={erreur}")
return
# ---------------------------------------- hoofdprogramma
# de internetmachines
hosts = ["istia.univ-angers.fr", "www.univ-angers.fr", "sergetahe.com", "localhost", "xx"]
# adressen IP van de machines van HOTES
for host in hosts:
print("-------------------------------------")
get_ip_and_name(host)
# einde
print("Terminé...")
Opmerkingen
- regel 2: de module [socket] biedt de functies die nodig zijn voor het beheer van internetsockets. [socket] betekent stopcontact, netwerkaansluiting;
- regel 6: met de functie [get_ip_and_name] kan op basis van de internetnaam van een machine het volgende worden verkregen:
- het adres IP van de machine;
- de naam van de machine, afgeleid van het voorgaande adres IP;
- regel 10: met de functie [socket.gethostbyname] kan het adres IP van een machine worden verkregen op basis van een van deze namen (een internetmachine kan een hoofdnaam en aliassen hebben);
- regel 12: de functies voor sockets genereren de uitzondering [socket.error] zodra er een fout optreedt;
- regel 19: met de functie [socket.gethostbyaddr] kun je de naam van een machine opvragen op basis van het adres IP. We zullen zien dat je een andere naam kunt krijgen dan de naam die in regel 6 is doorgegeven;
- regel 30: een lijst met computernamen. De laatste naam is onjuist. De naam [localhost] verwijst naar de computer waarop u werkt en waarop het script wordt uitgevoerd;
- regel 33-35: we geven de IP van deze machines weer;
Resultaten:
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. Het protocol HTTP (HyperText Transfer Protocol)
21.4.1. Voorbeeld 1

Wanneer een browser een URL weergeeft, fungeert deze als client van een webserver of, anders gezegd, van een HTTP-server. De browser neemt het initiatief en begint met het verzenden van een aantal opdrachten naar de server. Voor dit eerste voorbeeld:
- is de server het hulpprogramma [RawTcpServer];
- de client is een browser;
We starten eerst de server op poort 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 :
Vervolgens vragen we met een browser de URL [http://localhost:100] op, dat wil zeggen dat we aangeven dat de opgevraagde server HTTP op poort 100 van de lokale machine draait:

Laten we teruggaan naar het servervenster:
(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...
- regel 5: de client die verbinding heeft gemaakt;
- regels 9-22: de reeks tekstregels die hij heeft verzonden:
- regel 9: deze regel heeft de indeling [GET URL HTTP/1.1]. Hierin wordt URL opgevraagd en wordt de server gevraagd het protocol HTTP 1.1 te gebruiken;
- regel 10: deze regel heeft de indeling [Host: serveur:port]. Hoofdletters en kleine letters in het commando [Host] maken geen verschil. Ter herinnering: de client vraagt hier informatie op bij een lokale server die op poort 100 draait;
- regel 14: het commando [User-Agent] geeft de identiteit van de client aan;
- regel 15: het commando [Accept] geeft aan welke documenttypes door de client worden geaccepteerd;
- regel 21: het commando [Accept-Language] geeft aan in welke taal de opgevraagde documenten gewenst zijn, indien ze in meerdere talen beschikbaar zijn;
- regel 11: het commando [Connection] geeft de gewenste verbindingsmodus aan: [keep-alive] geeft aan dat de verbinding in stand moet worden gehouden totdat de uitwisseling is voltooid;
- regel 22: de client sluit zijn opdrachten af met een lege regel;
We beëindigen de verbinding door de server af te sluiten:
client 1 : []
server : Client 3-DESKTOP-30FF5FB-51441 connecté...
server : Attente d'un client...
quit
server : fin du service
21.4.2. Voorbeeld 2
Nu we weten welke commando’s een browser verstuurt om een URL op te vragen, gaan we deze URL opvragen met onze client TCP [RawTcpClient]. De Apache-server van Laragon (paragraaf |Installatie van Laragon|) zal onze webserver zijn.
Laten we Laragon starten en vervolgens de Apache-webserver:


Laten we nu met een browser de pagina URL [http://localhost:80] opvragen. Hier specificeren we alleen de server [localhost:80] en geen URL document. In dit geval wordt de URL / opgevraagd, d.w.z. de root van de webserver:

- in [1], het gevraagde URL. We hadden aanvankelijk [http://localhost:80] ingetikt en de browser (hier Firefox) heeft dit eenvoudigweg omgezet in [localhost], omdat het protocol [http] impliciet is wanneer er geen protocol wordt vermeld en de poort [80] impliciet is wanneer de poort niet wordt gespecificeerd;
- in [2], de hoofdpagina / van de opgevraagde webserver;
Laten we nu de tekst bekijken die door de browser is ontvangen:

- klik met de rechtermuisknop op de ontvangen pagina en kies de optie [2]. We krijgen de volgende broncode:
<!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>
Laten we nu de URL [http://localhost:80] opvragen met onze client TCP:
(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) :
- regel 1: we maken verbinding met poort 80 van de server localhost. Daar draait de webserver van Laragon;
We voeren nu de commando’s in die we in de vorige paragraaf hebben ontdekt:
(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...
- regel 4, het commando [GET]. We vragen de rootmap / van de webserver op;
- regel 5, het commando [Host];
- dit zijn de enige twee onmisbare commando’s. Voor de overige commando’s gebruikt de webserver standaardwaarden;
- regel 6, de lege regel waarmee de opdrachten van de client moeten worden afgesloten;
- onder regel 6 volgt het antwoord van de webserver;
- regels 7-12: de headers HTTP van het antwoord van de server;
- regel 13: de lege regel die het einde van de HTTP-headers aangeeft;
- regels 14-82: het document HTML dat in regel 4 is opgevraagd;
We laden het logbestand [localhost-80.txt]:

--> [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>]
- regels 11-79: het ontvangen document HTML. In het vorige voorbeeld had Firefox hetzelfde ontvangen;
We hebben nu de basis om een client TCP te programmeren die een URL zou opvragen.
21.4.3. Voorbeeld 3

Het script [http/01/main.py] is een HTTP-client die is geconfigureerd door het bestand [config.py]. De inhoud daarvan is als volgt:
def configure():
# URLs om op te vragen
urls = [
# site: naam van de site waarmee verbinding moet worden gemaakt
# poort: poort van de webservice
# GET: URL aangevraagd
# headers: HTTP-headers die in het verzoek moeten worden verzonden
# endOfLine: regeleinde in de verzonden headers HTTP
# encoding: codering van het antwoord van de server
# timeout: maximale wachttijd voor een antwoord van de server
{
"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
}
]
# de configuratie wordt doorgegeven
return {
"urls": urls
}
- de inhoud van het bestand is een lijst met URL, waarbij elk element in de lijst een woordenboek is. Dit woordenboek geeft aan hoe verbinding moet worden gemaakt met de website die wordt aangeduid door de sleutel [site];
- regels 4-10: de betekenis van de sleutels van elk woordenboek;
Het script [http/01/main.py] is als volgt:
# imports
import codecs
import socket
# -----------------------------------------------------------------------
def get_url(url: dict, suivi: bool = True):
# leest de urlURL van de website url["GET"] en slaat deze op in het bestand url[site].html
# de communicatie tussen client en server verloopt volgens het protocol HTTP dat is opgegeven in het woordenboek [url]
# uitzonderingen worden doorgegeven
sock = None
html = None
try:
# verbinding met [site] op poort 80 met een time-out
site = url['site']
sock = socket.create_connection((site, int(url['port'])), float(url['timeout']))
# de verbinding vertegenwoordigt een bidirectionele communicatiestroom
# tussen de client (dit programma) en de benaderde webserver
# dit kanaal wordt gebruikt voor de uitwisseling van opdrachten en informatie
# het communicatieprotocol is HTTP
# aanmaken van het bestand site.html – de storende tekens worden vervangen door een bestandsnaam
site2 = site.replace("/", "_")
site2 = site2.replace(".", "_")
html_filename = f'{site2}.html'
html = codecs.open(f"output/{html_filename}", "w", "utf-8")
# de client start de dialoog HTTP met de server
if suivi:
print(f"Client : début de la communication avec le serveur [{site}]")
# afhankelijk van de server moeten de regels van de client eindigen op \n of \r\n
end_of_line = url["endOfLine"]
# de client verstuurt het commando GET om de configuratie URL op te vragen
# syntaxis GET URL HTTP/1.1
commande = f"GET {url['GET']} HTTP/1.1{end_of_line}"
# status?
if suivi:
print(f"--> {commande}", end='')
# het commando wordt naar de server verzonden
sock.send(bytearray(commande, 'utf-8'))
# verzending van de headers HTTP
for verb, value in url['headers'].items():
# het te verzenden commando wordt samengesteld
commande = f"{verb}: {value}{end_of_line}"
# volgt?
if suivi:
print(f"--> {commande}", end='')
# het commando wordt naar de server verzonden
sock.send(bytearray(commande, 'utf-8'))
# de header HTTP [Connection: close] wordt verzonden om de webserver te vragen
# de verbinding te verbreken zodra het gevraagde document is verzonden
sock.send(bytearray(f"Connection: close{end_of_line}", 'utf-8'))
# de headers van het protocol HTTP moeten eindigen met een lege regel
sock.send(bytearray(end_of_line, 'utf-8'))
#
# de server zal nu via het sock-kanaal antwoorden. Hij zal alle
# zijn gegevens verzenden en vervolgens het kanaal sluiten. De client leest dus alles wat binnenkomt via sock
# totdat het kanaal wordt gesloten
#
# eerst worden de door de server verzonden headers HTTP gelezen
# ook deze eindigen met een lege regel
if suivi:
print(f"Réponse du serveur [{site}]")
# de socket wordt gelezen alsof het een tekstbestand is
encoding = f"{url['encoding']}" if url['encoding'] else None
if encoding:
file = sock.makefile(encoding=encoding)
else:
file = sock.makefile()
# dit bestand wordt regel voor regel verwerkt
fini = False
while not fini:
# de huidige regel lezen
ligne = file.readline().strip()
# is er een niet-lege regel?
if ligne:
if suivi:
# de header wordt weergegeven HTTP
print(f"<-- {ligne}")
else:
# dit was de lege regel – de kopteksten HTTP zijn voltooid
fini = True
# het document HTML wordt gelezen, dat volgt op de lege regel
# huidige regel wordt gelezen
ligne = file.readline()
while ligne:
# opname in het logbestand
html.write(str(ligne))
# volgende regel
ligne = file.readline()
# de lus eindigt wanneer de server de verbinding verbreekt
finally:
# de client verbreekt de verbinding
if sock:
sock.close()
# het HTML-bestand wordt gesloten
if html:
html.close()
# -------------------main
# de applicatie wordt geconfigureerd
import config
config = config.configure()
# de URL uit het configuratiebestand ophalen
for url in config['urls']:
print("-------------------------")
print(url['site'])
print("-------------------------")
try:
# URL lezen van de website [site]
get_url(url)
except BaseException as erreur:
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
pass
# einde
print("Terminé...")
Opmerkingen bij de code:
- regels 108-109: het woordenboek [config] van de module [config.py] wordt opgehaald;
- regels 111-122: dit woordenboek wordt verwerkt;
- regel 118, 7: de functie [get_url(url)] vraagt een document op van de website url[site] en slaat dit op in het tekstbestand url[site].HTML. Standaard wordt de communicatie tussen client en server gelogd op de console (tracking=True);
- dit gebeurt allemaal in een [try / finally] (regels 14-96). Er is geen [except]-clausule. Uitzonderingen worden doorgegeven aan de aanroepende code en deze stopt ze en geeft ze weer (regels 119-120);
- regels 16-17: een verbinding met de webserver openen. De functie [socket.create_connection] accepteert drie parameters:
- [param1]: is de naam van de internetcomputer die men wil bereiken;
- [param2]: is het poortnummer van de dienst waarmee verbinding moet worden gemaakt;
- [param3]: [socket.create_connection] retourneert een socket en [param3], indien aanwezig, geeft de time-out van de aangemaakte socket aan. De time-out is de maximale wachttijd van de socket terwijl deze wacht op een antwoord van de externe machine;
- regels 27-28: aanmaken van het bestand [site.html] waarin het ontvangen document HTML wordt opgeslagen;
- regels 34-43: het eerste commando van de client moet het commando [GET URL HTTP/1.1] zijn;
- regel 43: met de functie [sock.send] kan de client gegevens naar de server verzenden. De verzonden tekstregel heeft hier de volgende betekenis: „Ik wil (GET) de pagina [URL] van de website waarmee ik verbonden ben. Ik werk met het protocol HTTP versie 1.1";
- regel 43: de instructie [sock.send(bytearray(commande, 'utf-8'))] verstuurt een bytearray. Deze array wordt verkregen door de tekenreeks [commande] om te zetten in een reeks bytes die gecodeerd zijn in UTF-8;
- regels 44-52: de overige regels van het protocol HTTP [Host, User-Agent, Accept, Accept-Language…] worden verzonden. De volgorde ervan doet er niet toe;
- regels 53-55: de header HTTP [Connection: close] wordt verzonden om de server te vragen de verbinding te verbreken zodra het gevraagde document is verzonden. Standaard doet de server dit niet. Dit moet dus expliciet worden gevraagd. Het voordeel hiervan is dat het verbreken van de verbinding aan de kant van de client wordt gedetecteerd, waardoor de client weet dat hij het volledige gevraagde document heeft ontvangen;
- regels 56-57: er wordt een lege regel naar de server gestuurd om aan te geven dat de client klaar is met het verzenden van zijn headers HTTP en nu wacht op het gevraagde document;
- regels 68-86: de server stuurt eerst een reeks HTTP-headers die diverse informatie over het opgevraagde document geven. Deze headers eindigen met een lege regel;
- regels 69-73: om het antwoord van de server regel voor regel te kunnen lezen, wordt de methode [sock.makefile(encoding=encoding)] gebruikt. De optionele parameter [encoding] specificeert de codering van de verwachte tekst. Na deze bewerking kan de stroom van door de server verzonden regels worden gelezen als een gewoon tekstbestand;
- regel 78: we lezen een door de server verzonden regel met de methode [readline]. We verwijderen de spaties (witruimte, regeleinde) aan het begin en einde van de regel;
- regels 81-83: als de regel niet leeg is en er om follow-up is gevraagd, wordt de ontvangen regel op de console weergegeven;
- regels 84-86: als de lege regel is opgehaald die het einde aangeeft van de door de server verzonden HTTP-headers, wordt de lus van regel 76 gestopt;
- regels 90-95: de tekstregels van het antwoord van de server kunnen regel voor regel worden gelezen met een lus while en worden opgeslagen in het tekstbestand [html]. Wanneer de webserver de volledige pagina heeft verzonden die is opgevraagd, verbreekt hij de verbinding met de client. Aan de kant van de client wordt dit gedetecteerd als het einde van het bestand en wordt de lus van de regels 90-95 verlaten;
- regels 96-102: of er nu een fout is of niet, alle door de code gebruikte bronnen worden vrijgegeven;
Resultaten:
De console geeft de volgende logberichten weer:
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/cursussen-tutorials-programmeren
<-- 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/cursussen-en-tutorials-over-programmeren/
<-- Set-Cookie: SERVERID68971=2620178|XwH/h|XwH/h; path=/
<-- X-IPLB-Instance: 17095
Terminé...
Process finished with exit code 0
Opmerkingen
- regel 12: URL [http://localhost/] is gevonden (code 200);
- regel 29: de URL [http://sergetahe.com/] is niet gevonden (code 302). Code 302 betekent dat de opgevraagde pagina is gewijzigd in URL. De nieuwe URL wordt aangegeven door de header HTTP [Location] in regel 36;
- regel 49: het verzoek dat aan de server [http://tahe.developpez.com] is gedaan, is onjuist (code 400);
- regel 65: de pagina URL [http://www.sergetahe.com/] is niet gevonden (code 301). Code 301 betekent dat de opgevraagde pagina definitief is gewijzigd in URL. De nieuwe URL wordt aangegeven door de header HTTP [Location] in regel 71;
Over het algemeen zijn de 3xx-, 4xx- en 5xx-codes van een HTTP-server foutcodes.
De uitvoering heeft de volgende bestanden opgeleverd:

Het ontvangen bestand [output/localhost.HTML] is als volgt:
<!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>
We hebben inderdaad hetzelfde document ontvangen als met de Firefox-browser.
Het ontvangen document [output/sergetahe_com.html] is als volgt:

De meeste HTTP-servers sturen hun antwoorden op verzoeken in stukjes. Elk verzonden stukje wordt voorafgegaan door een regel die het aantal bytes van het volgende stukje aangeeft. Hierdoor kan de client precies dat aantal bytes lezen om het stukje te ontvangen. Hier geeft de 0 aan dat het volgende stukje nul bytes bevat. Ter herinnering: de server had aangegeven dat het document [http://sergetahe.com/] was gewijzigd in URL. Er is dus geen document verzonden.
Het document [output/tahe_developpez_com.html] is als volgt:
<!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>
- regels 1-12: de server heeft een document HTML verzonden, ondanks het feit dat het verzoek onjuist was (regel 49 van de resultaten). Met het document HTML kan de server de oorzaak van de fout specificeren. Deze wordt aangegeven in de regels 6 en 7:
- regel 7: onze client heeft het protocol HTTP gebruikt;
- regel 8: de server werkt met het protocol HTTPS (S = beveiligd) en accepteert het protocol HTTP niet;
Het document [output/www_sergetahe_com.html] ziet er als volgt uit:
<!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>
Ook hier is er een fout opgetreden (regel 3). De server zorgt er echter voor dat er een document HTML wordt verzonden waarin deze fout wordt beschreven (regels 1-7).
21.4.4. Voorbeeld 4
Uit de voorgaande voorbeelden is gebleken dat onze client HTTP ontoereikend was. We zullen nu een tool presenteren met de naam [curl] waarmee webdocumenten kunnen worden opgehaald en de genoemde problemen worden opgelost: het HTTPS-protocol, documenten die in delen worden verzonden, omleidingen… De tool [curl] is geïnstalleerd met Laragon:

Laten we een terminal openen: PyCharm [1]:

- in [1], toegang tot de terminals van PyCharm;
- in [2-3], de reeds actieve terminals;
- in [4], de map waarin u zich bevindt. Dit maakt in het volgende niet uit;
In de terminal voeren we de volgende opdracht in:
(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
…
Het feit dat het commando [curl –help] resultaten heeft opgeleverd, toont aan dat het commando [curl] zich in de map PATH van de terminal bevindt. In Windows is de PATH de verzameling mappen die wordt doorzocht wanneer de gebruiker een uitvoerbaar commando invoert, in dit geval [curl]. De waarde van de PATH kan worden achterhaald:
(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;
Regel 2: de mappen van PATH, gescheiden door puntkomma’s. In deze lijst komt geen map voor die verband houdt met Laragon. Als we wat verder zoeken, vinden we dat er een [curl] in de map [c:\windows\system32] staat. Dit is het bestand dat eerder een reactie heeft gegeven.
Als je de tool [curl] wilt gebruiken die bij Laragon wordt meegeleverd, kun je als volgt te werk gaan:


- in [2], de Laragon-terminal;
- in [3]: met deze knop kun je nieuwe terminals aanmaken, die elk in een tabblad van het bovenstaande venster worden geïnstalleerd;
- met [4] vraag je de PATH van de Laragon-terminal op;
- het resultaat is heel anders dan wat er in een terminal met de naam PyCharm werd verkregen. Deze PATH bevat talrijke mappen die tijdens de installatie van Laragon zijn aangemaakt. De map met het hulpprogramma [curl] is daar een van:

Gebruik daarna de terminal van uw keuze. Houd er wel rekening mee dat wanneer u een door Laragon geleverde tool wilt gebruiken, de Laragon-terminal de voorkeur verdient.
Met het commando [curl --help] worden alle configuratieopties van [curl] weergegeven. Dat zijn er tientallen. We zullen er maar heel weinig gebruiken. Om een URL op te vragen, volstaat het om de opdracht [curl URL] in te voeren. Deze opdracht geeft het gevraagde document weer op de console. Als we bovendien de uitwisselingen HTTP tussen de client en de server willen zien, typen we [curl --verbose URL]. Tot slot, om het opgevraagde document HTML in een bestand op te slaan, typen we [curl --verbose --output fichier URL].
Om te voorkomen dat het bestandssysteem van onze machine vol raakt, gaan we naar een andere locatie (ik gebruik hier een 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
- regel 3: we gaan naar de map [c:\temp]. Als deze map niet bestaat, kun je deze aanmaken of een andere map kiezen;
- regel 6: we maken een map aan met de naam [curl];
- regel 9: ga naar deze map;
- regel 12: de inhoud ervan wordt weergegeven. Deze is leeg (regel 20);
Zorg ervoor dat de Apache-server van Laragon is gestart en vraag met [curl] de bestanden URL en [http://localhost/] op met het commando [curl –verbose –output localhost.html http://localhost/]. We krijgen de volgende resultaten:
λ 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 naar host localhost, ongewijzigd gelaten
- regels 10-13: regels die door [curl] naar de server [localhost] zijn verzonden. We herkennen het protocol HTTP;
- regels 14-20: regels die door de server als antwoord zijn verzonden;
- regel 14: geeft aan dat het gevraagde document inderdaad is ontvangen;
Het bestand [localhost.html] bevat het gevraagde document. U kunt dit controleren door het bestand in een teksteditor te openen.
Laten we nu URL [https://tahe.developpez.com:443/] opvragen. Om dit URL te verkrijgen, moet de client HTTP de taal HTTPS beheersen. Dit is het geval bij de client [curl].
De console-uitvoer is als volgt:
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 naar host tahe.developpez.com, ongewijzigd gelaten
- regels 10-39: de communicatie tussen client en server om de verbinding te beveiligen: deze wordt versleuteld;
- regels 41-44: de headers HTTP die door de client [curl] naar de server worden verzonden;
- regel 52: het opgevraagde document is gevonden;
- regel 57: het document wordt in delen verzonden;
[curl] verwerkt zowel het beveiligde protocol HTTPS als het feit dat het document in delen wordt verzonden op de juiste manier. Het verzonden document is hier te vinden in het bestand [tahe.developpez.com.html].
Laten we nu URL [http://sergetahe.com/cours-tutoriels-de-programmation] opvragen. We hadden gezien dat er voor dit URL een omleiding was naar het URL [http://sergetahe.com/cours-tutoriels-de-programmation/] (met een / aan het einde).
De console-uitvoer is dan als volgt:
C:\Temp\curl
λ curl --verbose --output sergetahe.com.html --location http://sergetahe.com/cursussen-tutorials-programmeren
% 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/cursussen-en-tutorials-over-programmeren/
< 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 om sergetahe.com intact te laten
* Issue another request to this URL: 'http://sergetahe.com/cursussen-en-tutorials-over-programmeren/'
* Found bundle for host sergetahe.com: 0x14385f8 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) met 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/cursussen-en-tutorials-over-programmeren/
< 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 naar host sergetahe.com, ongewijzigd gelaten
* Issue another request to this URL: 'https://sergetahe.com/cursussen-en-tutorials-over-programmeren/'
* 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/cursussen-en-tutorials-over-programmeren/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/cursussen-en-tutorials-over-programmeren/>; 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 om sergetahe.com intact te laten
- regel 2: we gebruiken de optie [--location] om aan te geven dat we de door de server verzonden omleidingen willen volgen;
- regel 13: de server geeft aan dat het opgevraagde document is gewijzigd in URL;
- regel 18: de server geeft de nieuwe URL van het opgevraagde document aan;
- regel 31: [curl] verstuurt een nieuw verzoek, ditmaal naar de nieuwe URL;
- regel 36: de server antwoordt opnieuw dat de URL is gewijzigd;
- regel 41: de nieuwe URL is precies hetzelfde als degene die werd omgeleid, op één detail na: het protocol is gewijzigd. Het is nu HTTPS (regel 41), terwijl het eerder http was (regel 31);
- regel 49: er wordt een nieuw verzoek verzonden naar de nieuwe URL. Dit verzoek is versleuteld. Er volgt dan ook een hele dialoog om de beveiliging in te stellen, regels 53-91;
- regel 92: de nieuwe URL wordt opgevraagd, ditmaal met het protocol HTTP/2;
- regel 100: het document is gevonden;
Het opgevraagde document is te vinden in het bestand [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. Voorbeeld 5
Python beschikt over een module genaamd [pyccurl] waarmee de mogelijkheden van de tool [curl] in een Python-programma kunnen worden benut. We installeren deze module:

We gaan een nieuw script [http/02/main.py] schrijven:

Het bestand [http/02/config] ziet er als volgt uit:
def configure():
# lijst met URL die moeten worden opgevraagd
urls = [
# site: server waarmee verbinding moet worden gemaakt
# time-out: maximale wachttijd voor een antwoord van de server
# doel: op te vragen URL
# codering: codering van het antwoord van de server
{
"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"
}
]
# de configuratie wordt weergegeven
return {
''urls': URL's
}
Het bestand bevat een lijst met woordenboeken, die elk de volgende structuur hebben:
- site: de naam van een webserver;
- encoding: het coderingstype van het verwachte document;
- timeout: maximale wachttijd voor het antwoord van de server, uitgedrukt in milliseconden. Na het verstrijken van deze tijd verbreekt de client de verbinding;
- url: URL van het opgevraagde document;
De scriptcode [http/02/main.py] is als volgt:
# imports
import codecs
from io import BytesIO
import pycurl
# -----------------------------------------------------------------------
def get_url(url: dict, suivi=True):
# leest de urlURL en slaat deze op in het bestand output/url['site'].html
# als [suivi=True], dan is er een console-logboek van de client/server-uitwisseling
# url[timeout] is de time-out voor clientverzoeken;
# url [encoding] is de codering van het opgevraagde document
# hiermee worden de configuratiegegevens opgehaald
server = url['site']
timeout = url['timeout']
target = url['target']
encoding = url['encoding']
# tracking
print(f"Client : début de la communication avec le serveur [{server}]")
# uitzonderingen worden doorgegeven
html = None
curl = None
try:
# Initialisatie van een sessie cURL
curl = pycurl.Curl()
# binaire stream
flux = BytesIO()
# curl-opties
options = {
# URL
curl.URL: target,
# WRITEDATA: de locatie waar de ontvangen gegevens worden opgeslagen
curl.WRITEDATA: flux,
# uitgebreide modus
curl.VERBOSE: suivi,
# nieuwe verbinding – geen cache
curl.FRESH_CONNECT: True,
# time-out van het verzoek (in seconden)
curl.TIMEOUT: timeout,
curl.CONNECTTIMEOUT: timeout,
# de geldigheid van certificaten niet controleren SSL
curl.SSL_VERIFYPEER: False,
# omleidingen volgen
curl.FOLLOWLOCATION: True
}
# configuratie van curl
for option, value in options.items():
curl.setopt(option, value)
# Het verzoek CURL met deze instellingen uitvoeren
curl.perform()
# het bestand server.html aanmaken – de storende tekens voor de bestandsnaam worden vervangen
server2 = server.replace("/", "_")
server2 = server2.replace(".", "_")
html_filename = f'{server2}.html'
html = codecs.open(f"output/{html_filename}", "w", encoding)
# Het ontvangen document opslaan in het bestand HTML
html.write(flux.getvalue().decode(encoding))
finally:
# vrijgeven van de bronnen
if curl:
curl.close()
if html:
html.close()
# -------------------hoofd
# de applicatie wordt geconfigureerd
import config
config = config.configure()
# de URL uit het configuratiebestand ophalen
for url in config['urls']:
print("-------------------------")
print(url['site'])
print("-------------------------")
try:
# URL van de site [site] lezen
get_url(url)
# behalve BaseException als fout:
# print(f"De volgende fout is opgetreden: {fout}")
finally:
pass
# einde
print("Terminé...")
Opmerkingen
- regel 5: we importeren de module [pycurl];
- regel 3: de klasse [BytesIO] wordt geïmporteerd, waarmee we de van de server ontvangen gegevens in een binaire stream kunnen opslaan;
- regels 70-72: we halen de configuratie van de applicatie op;
- regels 75-85: we doorlopen de lijst met URL-elementen die in de configuratie zijn gevonden;
- regel 81: voor elk van de URL-elementen wordt de functie [get_url] aangeroepen, die de urlURL met een time-out van url[‘target’] zal downloaden;
- regel 9: de functie [get_url] ontvangt de configuratie van de URL die moet worden opgevraagd;
- regels 16-19: de configuratie van de URL wordt opgeslagen in afzonderlijke variabelen;
- regels 26, 61: alle bewerkingen worden uitgevoerd binnen een try/finally-blok. Uitzonderingen worden niet opgevangen, maar doorgegeven aan de aanroepende code, die ze vervolgens wel opvangt;
- regel 28: we bereiden een [curl]-sessie voor. [pycurl.Curl()] retourneert een [curl]-resource die de transactie met een server zal uitvoeren;
- regel 30: instantiëren van de binaire stream waarin de ontvangen gegevens worden opgeslagen;
- regels 32-48: het woordenboek [options] configureert de verbinding [curl] met de server. Hun rol wordt in de opmerkingen aangegeven;
- regels 49-51: de verbindingsopties worden doorgegeven aan de bron [curl];
- regel 53: verbinding met URL aangevraagd met de gedefinieerde opties. Vanwege de optie [curl.WRITEDATA: flux] (regel 36) zal de functie [curl.perform()] de ontvangen gegevens opslaan in [flux];
- regels 54-60: het bestand HTML wordt aangemaakt om het ontvangen document HTML op te slaan;
- regel 60: de binaire stroom [flux.getvalue()] wordt als een tekenreeks opgeslagen in het bestand HTML. De codering van deze tekenreeks wordt gespecificeerd in de methode [decode(encoding)]. Men moet dus de codering kennen van het door de server verzonden document. Als men zich vergist, zal het decoderen van de binaire stroom mislukken. De codering wordt gespecificeerd in het configuratiebestand van URL (bijvoorbeeld regel 12). We hadden deze informatie dynamisch kunnen verwerken, aangezien de server deze in de HTTP-headers verstuurt. Dat zou de voorkeur hebben gehad. Om de code eenvoudig te houden, hebben we dit echter niet gedaan. Om het coderingstype van het document te achterhalen, hoef je alleen maar de gewenste URL op te vragen met een browser en te kijken naar de HTTP-headers die de browser verstuurt in de debugmodus (F12) of naar het document zelf, want daarin staat de codering ook vermeld:


- regel 61-66: de toegewezen bronnen worden vrijgegeven;
Wanneer het script [main.py] wordt uitgevoerd, krijgt men de volgende console-uitvoer:
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/cursussen-programmeerhandleidingen
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
<
* Ignoring the response-body
* Connection #0 om sergetahe.com intact te laten
* Issue another request to this URL: 'http://sergetahe.com/cursussen-en-tutorials-over-programmeren'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) met 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/cursussen-en-tutorials-over-programmeren/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
<
* Ignoring the response-body
* Connection #0 naar host sergetahe.com, ongewijzigd gelaten
* Issue another request to this URL: 'http://sergetahe.com/cursussen-en-tutorials-over-programmeren/'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) met 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/cursussen-en-tutorials-over-programmeren/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
<
* Ignoring the response-body
* Connection #0 naar host sergetahe.com, ongewijzigd gelaten
* Issue another request to this URL: 'https://sergetahe.com/cursussen-en-tutorials-over-programmeren/'
* 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/cursussen-en-tutorials-over-programmeren/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/cursussen-en-tutorials-over-programmeren/>; rel=shortlink
< vary: Accept-Encoding
< x-iplb-instance: 17080
< set-cookie: SERVERID68971=26218|XwLIp|XwLIp; path=/
<
* Connection #1 om sergetahe.com intact te laten
-------------------------
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 om tahe.developpez.com intact te laten
-------------------------
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 om www.polytech-angers.fr te hosten, ongewijzigd gelaten
* 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) met 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 naar host www.polytech-angers.fr, ongewijzigd gebleven
-------------------------
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 naar host localhost is intact gebleven
Terminé...
Process finished with exit code 0
Opmerkingen
- in blauw: de http-commando's die naar de server zijn verzonden;
- in het groen: de gegevens die de client als antwoord ontvangt;
- we krijgen dezelfde uitwisselingen als met de tool [curl];
- regel 9: de URL [http://sergetahe.com/] wordt opgevraagd;
- regel 15: de server antwoordt dat de pagina is verplaatst. Regel 21: de nieuwe URL;
- regel 32: URL [http://sergetahe.com/cours-tutoriels-de-programmation] wordt opgevraagd;
- regel 38: de server antwoordt dat de pagina is verplaatst. Regel 43: de nieuwe URL;
- regel 54: de URL [http://sergetahe.com/cours-tutoriels-de-programmation/] wordt opgevraagd;
- regel 60: de server antwoordt dat de pagina is verplaatst. Regel 65: de nieuwe URL. Deze maakt gebruik van het beveiligde protocol [HTTPS];
- regels 71-75: het beveiligde protocol wordt met de server tot stand gebracht;
- regel 76: de URL [https://sergetahe.com/cours-tutoriels-de-programmation/] wordt opgevraagd;
- regel 82: het opgevraagde document is gevonden;
21.4.6. Conclusie
In dit hoofdstuk hebben we het HTTP-protocol ontdekt en een [http/02/main.py]-script geschreven waarmee een URL van het internet kan worden gedownload.
21.5. Het protocol SMTP (Simple Mail Transfer Protocol)
21.5.1. Inleiding

In dit hoofdstuk:
- [Serveur B] is een lokale SMTP-server die we zullen installeren;
- [Client A] is een SMTP-client in verschillende vormen:
- de client [RawTcpClient] om het protocol SMTP te verkennen;
- een Python-script dat het protocol SMTP van de client [RawTcpClient] nabootst;
- een Python-script dat gebruikmaakt van de module [smtplib] waarmee allerlei soorten e-mails kunnen worden verzonden;
21.5.2. Een [gmail]-adres aanmaken
Om onze SMTP-tests uit te voeren, hebben we een e-mailadres nodig om naar te schrijven. Hiervoor gaan we een Gmail-adres [https://www.google.com/intl/fr/gmail/about/] aanmaken:

Opmerking: Stuur een paar e-mails naar het adres dat u hebt aangemaakt. Ga pas verder als u zeker weet dat het aangemaakte account e-mails kan ontvangen.
21.5.3. Installatie van een SMTP-server
Voor onze tests installeren we de mailserver [hMailServer], die zowel een SMTP-server is waarmee e-mails kunnen worden verzonden, een POP3-server (Post Office Protocol) waarmee e-mails kunnen worden gelezen die op de server zijn opgeslagen, en een IMAP-server (Internet Message Access Protocol) die eveneens het lezen van op de server opgeslagen e-mails mogelijk maakt, maar nog verder gaat. Deze maakt het met name mogelijk om de opslag van e-mails op de server te beheren.
De e-mailserver [hMailServer] is beschikbaar op de URL [https://www.hmailserver.com/] (mei 2019).

Tijdens de installatie wordt u om bepaalde gegevens gevraagd:

- selecteer in [1-2] zowel de e-mailserver als de tools om deze te beheren;
- tijdens de installatie wordt u om het beheerderswachtwoord gevraagd: noteer dit, want u zult het nodig hebben;
[hMailServer] wordt geïnstalleerd als een Windows-service die automatisch wordt gestart bij het opstarten van de computer. Het is beter om handmatig opstarten te kiezen:
- in [3] typt u [services] in het invoerveld van de statusbalk;

- bij [4-8] zet u de service in de modus [manuel] (6) en start u deze op (7);
Zodra de server is opgestart, moet de [hMailServer]-server worden geconfigureerd. De server is geïnstalleerd met een beheerprogramma [hMailServer Administrator]:

- in [2], in het invoerveld van de statusbalk, typ je [hmailserver];
- in [3] de beheerder starten;
- in [4]: de beheerder verbinden met de server [hMailServer];
- in [5]: voer het wachtwoord in dat u bij de installatie van [hMailServer] hebt opgegeven;
Als u het wachtwoord bent vergeten, gaat u als volgt te werk:
- stop de [hMailServer]-server;
- open het bestand [<hmailserver>/bin/hmailserver.ini], waarbij <hmailserver> de installatiemap van de server is:

- verwijder in [100] het wachtwoord uit de regel [AdministratorPassword]. Hierdoor heeft de beheerder geen wachtwoord meer. Voer gewoon [Entrée] in wanneer hierom wordt gevraagd;
ValidLanguages=english,swedish
[Security]
AdministratorPassword=
[Database]
Laten we doorgaan met de configuratie van de server:

- voeg in [1-2] een domein toe (als dit nog niet bestaat);

- in [3] kunt u vrijwel alles invoeren voor de tests die we gaan uitvoeren. In de praktijk zou u de naam van een bestaand domein moeten invoeren;

We gaan een gebruikersaccount aanmaken:
- klik met de rechtermuisknop op [Accounts] (7) en vervolgens op (8) om een nieuwe gebruiker toe te voegen;
- in het tabblad [General] (9) definiëren we een gebruiker [guest] (10) met het wachtwoord [guest] (11). Deze krijgt het e-mailadres [guest@localhost] (10);
- in [12] is de gebruiker [guest] geactiveerd;

- in [13-14] is de gebruiker aangemaakt;

- in [27] de poort van de service SMTP;
- in [28]: voor deze dienst is geen authenticatie vereist;
- in [30]: voer het welkomstbericht in dat de server SMTP naar zijn clients zal sturen;

We doen hetzelfde met de server POP3:

We doen hetzelfde voor de server IMAP:

We geven het standaarddomein van de server [hMailServer] (er kunnen er meerdere zijn) aan :

- geef in [37] aan dat het standaarddomein van de server SMTP hetzelfde is als het domein dat u in [38] hebt aangemaakt;
Nadat u deze configuratie hebt opgeslagen, kunt u deze als volgt testen. Open een PyCharm-terminal in de map met hulpprogramma’s:

Typ vervolgens de volgende opdracht:
(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]
- regel 1: er wordt verbinding gemaakt met poort 25 van de machine [localhost]. Daar draait een onbeveiligde SMTP-server van de server [hMailServer];
- regel 4: we ontvangen het welkomstbericht dat we in de vorige stap 30 hebben geconfigureerd;
De server SMTP is dus inderdaad actief. Typ de opdracht [quit] om de dialoog met de server SMTP 25 te beëindigen.
Laten we nu hetzelfde doen met poort 587, de standaardpoort van de beveiligde e-mailrelaisservice 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]
- regel 4, het antwoord van de server SMTP die op poort 587 actief is;
Laten we nu hetzelfde doen met poort 110, de standaardpoort van de e-mailrelaisservice 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]
- regel 4: we hebben het welkomstbericht ontvangen van de server POP3;
Laten we nu hetzelfde doen met poort 143, de standaardpoort van de e-mailrelaisservice 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]
- regel 4: we hebben het welkomstbericht van de server IMAP ontvangen;
21.5.4. Installatie van een e-mailclient
Om de e-mail te kunnen lezen die we gaan versturen, hebben we een e-mailclient nodig. Voor degenen die er geen hebben, laten we zien hoe je de e-mailclient [Thunderbird] installeert en configureert:
- in [1]: download [thunderbird] en installeer het vervolgens;

- start de mailserver [hMailServer] als deze nog niet actief is;
- in [2-3]: zodra Thunderbird is gestart, gaan we een e-mailaccount aanmaken voor de gebruiker [guest@localhost] op de e-mailserver [hMailServer];



- op [7-11]: de server POP3, waarmee we de e-mail van de mailserver [hMailServer] kunnen lezen, bevindt zich op het adres [localhost] en draait op poort 110;
- in [12-16]: de server SMTP, waarmee we e-mail kunnen versturen namens de gebruikers van de mailserver [hMailServer], bevindt zich op het adres [localhost] en draait op poort 25;
- [18]: we kunnen controleren of deze configuratie klopt;


- in [26]: omdat er geen versleuteling is in SSL, waarschuwt Thunderbird ons dat onze configuratie risico's met zich meebrengt;
- in [28]: het account is aangemaakt;
Om het aangemaakte account te testen, gaan we met Thunderbird:
- een e-mail sturen naar de gebruiker [guest@localhost.com] (protocol SMTP);
- de door deze gebruiker ontvangen e-mail lezen (protocol POP3);

- in [3]: de afzender;
- in [4]: de ontvanger;
- in [5]: het onderwerp van de e-mail;
- in [6]: de inhoud van de e-mail;
- in [7]: om de e-mail te verzenden;

- in [8-9]: de e-mail van gebruiker [guest@localhost] wordt opgehaald;
- in [10-15]: het ontvangen bericht;
We gaan ook een e-mail sturen naar de gebruiker [pymailparlexemple@gmail.com]. Laten we voor hem een account aanmaken in Thunderbird om de e-mail te kunnen lezen die hij zal ontvangen:


- in [4]: vul hier wat u wilt;
- in [5]: het adres is [pymailparlexemple@gmail.com];
- in [6]: voer het wachtwoord in dat u aan deze gebruiker hebt toegewezen toen u hem aanmaakte;
- in [7]: bevestig deze configuratie;

- in [8]: Thunderbird heeft de volgende gegevens uit zijn database opgehaald;
- in [9]: het protocol voor het ophalen van e-mail is niet langer POP3 maar IMAP. Het belangrijkste verschil tussen beide is dat [POP3] de gelezen e-mail terugzet op de lokale computer waarop de e-mailclient draait en deze van de externe server verwijdert, terwijl [IMAP] de e-mail op de externe server bewaart;
- in [10]: identificatie van de server SMTP;
- in [13]: voor meer informatie over de servers IMAP en SMTP schakelt u over naar handmatige configuratie;

- naar [14-17]: de specificaties van de server IMAP;
- naar [18-21]: de specificaties van de server SMTP;
- in [22]: de configuratie wordt voltooid;

- in [23-24]: het nieuwe Thunderbird-account;
- in [26]: een nieuw bericht schrijven;

- in [27]: de afzender is [pymailparlexemple@gmail.com];
- in [28]: de ontvanger is [pymailparlexemple@gmail.com];
- in [29-30]: het bericht;
- in [31]: om het te verzenden;

- in [32]: de e-mail van de verschillende accounts wordt opgehaald;

- in [33-36]: de e-mail die de gebruiker heeft ontvangen [pymailparlexemple@gmail.com]
We maken ook:
- een nieuw Gmail-account [pymail2parlexemple@gmail.com];
- een nieuw Thunderbird-account [pymail2parlexemple@gmail.com] om de berichten op te halen van de gebruiker met dezelfde naam:


We beschikken nu over de tools om de protocollen SMTP, POP3 en IMAP te onderzoeken. We beginnen met het protocol SMTP.
21.5.5. Het protocol SMTP

We gaan het protocol SMTP verkennen door de logbestanden van de server [hMailServer] te bekijken. Hiervoor activeren we ze met de outl [hmailServerAdministrator]:


- in [2] zijn de logbestanden geactiveerd;
- in [3-5]: we activeren ze voor de protocollen SMTP, POP3, IMAP;
- in [7] vraagt men om ze te bekijken;
- in [8]: open het logbestand met een willekeurige teksteditor;

In het volgende voorbeeld is de client [Thunderbird] en de server [hMailServer]. Zorg er in Thunderbird voor dat de gebruiker [guest@localhost.com] een bericht naar zichzelf verstuurt:

De logbestanden zien er dan als volgt uit:
"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"
De bovenstaande regels beschrijven de communicatie die heeft plaatsgevonden tussen de client SMTP (de e-mailclient Thunderbird) en de server SMTP (hMailServer). De regels [SENT] geven aan wat de server SMTP naar zijn client heeft verzonden. De regels [RECEIVED] geven aan wat de server SMTP van zijn client heeft ontvangen.
- regel 1: direct nadat de client verbinding heeft gemaakt met de server SMTP, stuurt deze het welkomstbericht naar zijn client;
- regel 2: de client verstuurt het commando [EHLO] om zich te identificeren. Hier geeft hij zijn adres IP [127.0.0.1] op, dat verwijst naar de machine [localhost], d.w.z. de machine waarop de client SMTP draait;
- regel 3: de server stuurt een reeks antwoorden [250]. [nl] staat voor [newline], d.w.z. het teken \n. De antwoorden hebben de vorm [250-], behalve het laatste, dat de vorm [250 ] heeft. Zo weet de client SMTP dat het antwoord van de server SMTP is beëindigd en dat hij een commando kan verzenden. De reeks opdrachten [250] was bedoeld om de client SMTP een reeks opdrachten aan te geven die hij kon gebruiken;
- regel 4: de client SMTP verstuurt het commando [MAIL FROM : adresse_mail_expéditeur], dat aangeeft wie het bericht verstuurt;
- regel 5: de server SMTP antwoordt met [250 OK] om aan te geven dat hij het commando heeft begrepen;
- regel 6: de client SMTP verstuurt het commando [RCPT TO : adresse_mail_destinataire] om het adres van de ontvanger aan te geven;
- regel 7: opnieuw geeft de server SMTP aan dat hij het commando heeft begrepen;
- regel 8: de server SMTP verstuurt het commando [DATA]. Dit betekent dat hij de inhoud van het bericht gaat verzenden;
- regel 9: de server SMTP geeft met het antwoord [354 OK] aan dat hij klaar is om het bericht te ontvangen. De tekst [send .] geeft aan dat de client SMTP zijn bericht moet afsluiten met een regel die slechts één punt bevat;
- wat we daarna niet zien, is dat de client SMTP zijn bericht verstuurt. Dit wordt niet weergegeven in de logbestanden;
- regel 10: de client SMTP heeft de punt verzonden die het einde van het bericht aangeeft. De server SMTP antwoordt dat hij het bericht in de wachtrij heeft geplaatst (queued);
- de client SMTP stuurt hem het commando [QUIT] om aan te geven dat hij de verbinding gaat verbreken;
- regel 12: de server antwoordt;
Nu we de client/server-dialoog van het protocol SMTP kennen, gaan we proberen deze na te bootsen met onze client [RawTcpClient]. We gebruiken een terminal PyCharm:

Laten we een nieuw voorbeeld bekijken:
- client A is de generieke client TCP ([RawTcpClient]);
- server B is de mailserver [hMailServer];
- client A zal server B vragen om een e-mail te versturen die door gebruiker [guest@localhost.com] voor zichzelf is verzonden;
- we zullen controleren of de ontvanger de verzonden e-mail daadwerkelijk heeft ontvangen;
We starten de client als volgt:
(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]
- regel [1]: we maken verbinding met poort 25 van de lokale machine, waar de dienst SMTP van [hMailServer] draait. Het argument [--quit bye] geeft aan dat de gebruiker het programma zal afsluiten door het commando [bye] in te voeren. Zonder dit argument is het commando om het programma te beëindigen [quit]. Maar [quit] is ook een commando van het protocol SMTP. We moeten deze dubbelzinnigheid dus vermijden;
- regel [2]: de client is inderdaad verbonden;
- regel [3]: de client wacht op commando’s die via het toetsenbord worden ingevoerd;
- regel [4]: de server stuurt de client zijn welkomstbericht;
We zetten de dialoog als volgt voort:
(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] stuurt de client het commando [EHLO nom-de-la-machine-client]. De server antwoordt met een reeks berichten in de vorm [250-xx] (6). De code [250] geeft aan dat het door de client verzonden commando is geslaagd;
- in [10] geeft de client de afzender van het bericht aan, in dit geval [guest@localhost.com];
- in [11] staat het antwoord van de server;
- in [12] wordt de ontvanger van het bericht aangegeven, in dit geval de gebruiker [guest@localhost.com];
- in [13], het antwoord van de server;
- in [14] geeft het commando [DATA] aan de server door dat de client de inhoud van het bericht gaat verzenden;
- in [15], het antwoord van de server;
- in [16-22] moet de client een lijst met tekstregels verzenden, afgesloten met een regel die slechts één punt bevat. Het bericht kan [Subject:, From:, To:]-regels (16-18) bevatten om respectievelijk het onderwerp van het bericht, de afzender en de ontvanger te definiëren;
- in [19] moeten de voorgaande headers worden gevolgd door een lege regel;
- in [20-21] de tekst van het bericht;
- in [22]: de regel die slechts één punt bevat en het einde van het bericht aangeeft;
- in [23]: zodra de server de regel met slechts één punt heeft ontvangen, plaatst hij het bericht in de wachtrij;
- in [24] geeft de client aan de server door dat hij klaar is;
- in [25] zien we dat de server de verbinding met de client heeft verbroken;
Laten we nu met Thunderbird controleren of de gebruiker [guest@localhost.com] het bericht inderdaad heeft ontvangen:

- in [1-6] zien we dat de gebruiker [guest@localhost.com] het bericht inderdaad heeft ontvangen;
Uiteindelijk is onze klant [RawTcpClient] erin geslaagd een bericht te versturen via de server SMTP [localhost]. Laten we nu dezelfde methode gebruiken om een bericht naar [pymailparlexemple@gmail.com] te versturen:
(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
- regel 1: we gebruiken de Gmail-server SMTP die op poort 587 draait;
- regel 15: we lopen vast omdat de server SMTP ons vraagt een beveiligde verbinding tot stand te brengen, wat we niet weten hoe we moeten doen. In tegenstelling tot het vorige voorbeeld vraagt de server [smtp.gmail.com] (regel 1) om authenticatie. Deze accepteert alleen gebruikers die zijn geregistreerd in het domein [gmail.com] als clients. Deze authenticatie is beveiligd en vindt plaats binnen een versleutelde verbinding.
Het eerste voorbeeld heeft ons de basis gegeven om een eenvoudige SMTP-client in Python te bouwen. Het tweede voorbeeld heeft ons laten zien dat sommige SMTP-servers (de meeste eigenlijk) authenticatie vereisen via een versleutelde verbinding.
21.5.6. [smtp/01]-scripts: een eenvoudige SMTP-client
We gaan in Python reproduceren wat we eerder hebben geleerd over het SMTP-protocol.

Het bestand [smtp/01/config] configureert de applicatie als volgt:
def configure() -> dict:
return {
# beschrijving: beschrijving van de verzonden e-mail
# smtp-server: server SMTP
# smtp-port: poort van de server SMTP
# van: afzender
# to: ontvanger
# onderwerp: onderwerp van de e-mail
# bericht: inhoud van de 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",
# we sturen UTF-8
"content-type": 'text/plain; charset="utf-8"',
# we testen de tekens met accenten
"message": "aglaë séléné\nva au marché\nacheter des fleurs"
},
{
"description": "mail to gmail via gmail",
"smtp-server": "smtp.gmail.com",
"smtp-port": "587",
"from": "pymailparlexemple@gmail.com",
"to": "pymailparlexemple@gmail.com",
"subject": "to gmail via gmail",
# we verzenden UTF-8
"Content-type": 'text/plain; charset="utf-8"',
# we testen tekens met accenten
"message": "aglaë séléné\nva au marché\nacheter des fleurs"
}
]
}
- regels 10-35: een lijst met te verzenden e-mails. Voor elk daarvan worden de volgende gegevens opgegeven:
- [description]: een tekst die de e-mail beschrijft;
- [smtp-server]: de te gebruiken server SMTP;
- [smtp-port]: de poort van de server;
- [from]: de afzender van de e-mail;
- [to]: de ontvanger van de e-mail;
- [subject]: het onderwerp van de e-mail;
- [content-type]: de codering van de e-mail;
- [message]: de tekst van de e-mail;
De code [01/main] van de klant SMTP is als volgt:
# imports
import socket
# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: bool):
# verzendt een bericht naar de SMTP-server smtpserver namens de afzender
# voor de ontvanger. Als verbose=True, wordt de communicatie tussen client en server bijgehouden
# systeemfouten worden doorgegeven
connexion = None
try:
# naam van de lokale machine (vereist voor het protocol SMTP)
client = socket.gethostbyaddr(socket.gethostbyname("localhost"))[0]
# een verbinding openen op poort 25 van smtpServer
connexion = socket.create_connection((mail["smtp-server"], 25))
# de verbinding vertegenwoordigt een bidirectionele communicatiestroom
# tussen de client (dit programma) en de benaderde SMTP-server
# dit kanaal wordt gebruikt voor de uitwisseling van opdrachten en informatie
# na het tot stand brengen van de verbinding stuurt de server een welkomstbericht dat wordt gelezen
send_command(connexion, "", verbose, True)
# commando ehlo:
send_command(connexion, f"EHLO {client}", verbose, True)
# commando mail from:
send_command(connexion, f"MAIL FROM: <{mail['from']}>", verbose, True)
# commando rcpt to:
send_command(connexion, f"RCPT TO: <{mail['to']}>", verbose, True)
# commando data
send_command(connexion, "DATA", verbose, True)
# voorbereiding van het te verzenden bericht
# het moet de volgende regels bevatten
# Van: afzender
# To: ontvanger
# lege regel
# Bericht
# .
data = f"{mail['message']}"
# bericht verzenden
send_command(connexion, data, verbose, False)
# verzenden .
send_command(connexion, "\r\n.\r\n", verbose, False)
# commando afsluiten
send_command(connexion, "QUIT", verbose, True)
# einde
finally:
# verbinding verbreken
if connexion:
connexion.close()
# --------------------------------------------------------------------------
def send_command(connexion: socket, commande: str, verbose: bool, with_rclf: bool):
# commando verzenden in het verbindingskanaal
# logboekmodus indien verbose=True
# als with_rclf=True, voeg de reeks rclf toe aan het commando
# gegevens
rclf = "\r\n" if with_rclf else ""
# commando verzenden als commando niet leeg is
if commande:
# systeemfouten doorgeven
#
# opdracht verzenden
connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
# eventuele echo
if verbose:
affiche(commande, 1)
# antwoord met minder dan 1000 tekens lezen
reponse = str(connexion.recv(1000), 'utf-8')
# eventuele echo
if verbose:
affiche(reponse, 2)
# ophalen van foutcode
codeErreur = int(reponse[0:3])
# fout teruggestuurd door de server?
if codeErreur >= 500:
# er wordt een uitzondering met de fout gegenereerd
raise BaseException(reponse[4:])
# terugkeer zonder fout
# --------------------------------------------------------------------------
def affiche(echange: str, sens: int):
# uitwisseling weergeven op het scherm
# als sens=1 wordt -->uitwisseling weergegeven
# als sens=2, toon <-- uitwisseling zonder de laatste 2 rclf-tekens
if sens == 1:
print(f"--> [{echange}]")
return
elif sens == 2:
l = len(echange)
print(f"<-- [{echange[0:l - 2]}]")
return
# main ----------------------------------------------------------------
# client SMTP (SendMail Transfer Protocol) waarmee een bericht kan worden verzonden
# de gegevens worden uit een configuratiebestand gehaald dat voor elke server de volgende informatie bevat
# beschrijving: beschrijving van de verzonden e-mail
# smtp-server: server SMTP
# smtp-port: poort van de server SMTP
# van: afzender
# to: ontvanger
# onderwerp: onderwerp van de e-mail
# bericht: inhoud van de e-mail
# communicatieprotocol SMTP client-server
# -> client maakt verbinding met poort 25 van de SMTP-server
# <- de server stuurt hem een welkomstbericht
# -> de client stuurt het commando EHLO: de naam van zijn computer
# <- de server antwoordt met OK of niet
# -> de client verstuurt het commando mail from: <afzender>
# <- server antwoordt met OK of niet
# -> de client verstuurt het commando rcpt to: <ontvanger>
# <- server antwoordt met OK of niet
# -> de client verstuurt het commando data
# <- server antwoordt met OK of niet
# -> de client verstuurt alle regels van zijn bericht en sluit af met een regel die alleen het teken bevat.
# <- server antwoordt met OK of niet
# -> de client verstuurt het commando ‘quit’
# <- de server antwoordt met OK of niet
# de antwoorden van de server hebben de vorm xxx tekst, waarbij xxx een getal van 3 cijfers is. Elk getal xxx >=500
# duidt op een fout. Het antwoord kan uit meerdere regels bestaan die allemaal beginnen met xxx-, behalve de laatste
# in de vorm xxx(spatie)
# de uitgewisselde tekstregels moeten eindigen met de tekens RC(#13) en LF(#10)
# configuratie van de applicatie
import config
config = config.configure()
# e-mails worden één voor één verwerkt
for mail in config['mails']:
try:
# logbestanden
print("----------------------------------")
print(f"Envoi du message [{mail['description']}]")
# voorbereiding van het te verzenden bericht
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']}"
# het bericht verzenden in de logboekmodus
sendmail(mail, True)
# einde
print("Message envoyé...")
except BaseException as erreur:
# de fout wordt weergegeven
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
pass
# volgende e-mail
Opmerkingen
- regels 134-136: de applicatie wordt geconfigureerd;
- regels 139-151: alle e-mails die in de configuratie zijn gevonden, worden getarred;
- regels 141-143: er wordt weergegeven wat er gaat gebeuren;
- regels 144-149: het te verzenden bericht wordt gedefinieerd. Het bericht [message] wordt voorafgegaan door de headers [From, To, Subject, Content-type];
- regel 151: het verzenden van de e-mail wordt verzorgd door de functie [sendmail], die twee parameters accepteert:
- [mail]: het woordenboek met de informatie die nodig is voor het verzenden van de e-mail;
- [verbose]: een booleaanse waarde die aangeeft of de communicatie tussen client en server al dan niet op de console moet worden gelogd;
- regels 154-156: alle uitzonderingen die uit de functie [sendmail] komen, worden opgevangen. Ze worden weergegeven;
- regel 6: [mail] is het woordenboek dat de te verzenden e-mail beschrijft;
- regel 14: in het protocol SMTP moet de client zijn naam verzenden. Hier wordt de naam opgehaald van de lokale machine die als client zal fungeren;
- regel 16: verbinding met de server SMTP waarnaar het bericht zal worden verzonden;
- regels 22-23: als de verbinding met de server SMTP tot stand is gebracht, stuurt deze een welkomstbericht dat hier wordt gelezen;
- de functie [sendmail] verstuurt vervolgens de verschillende commando’s die een SMTP-client moet versturen:
- regels 24-25: het commando EHLO;
- regels 26-27: het commando MAIL FROM: ;
- regels 28-29: de opdracht RCPT TO: ;
- regels 30-31: de opdracht DATA;
- regels 32-41: verzending van het bericht (From, To, Subject, Content-type, tekst);
- regels 42-43: verzending van het eindteken;
- regels 44-457: het commando QUIT dat de dialoog tussen de client en de server SMTP beëindigt;
- de uitvoering van [sendmail] vindt plaats binnen een [try / finally], waardoor alle uitzonderingen worden doorgegeven aan de aanroepende code. We weten dat deze alle uitzonderingen opvangt om ze weer te geven;
- regels 48-50: vrijgeven van bronnen;
- regel 54: de functie [send_command] is verantwoordelijk voor het verzenden van de opdrachten van de client naar de server SMTP. Deze functie accepteert vier parameters:
- [connexion]: de verbinding tussen de client en de server;
- [commande]: de te verzenden opdracht;
- [verbose]: als TRUE, dan wordt de communicatie tussen client en server op de console gelogd;
- [with_rclf]: als TRUE, stuur dan het commando afgesloten met de reeks \r\n. Dit is nodig voor alle commando’s van het protocol SMTP, maar [send_command] wordt ook gebruikt om het bericht te verzenden. Hier wordt de reeks \r\n niet toegevoegd;
- regel 62: het commando wordt alleen verzonden als het niet leeg is;
- regels 65-66: het commando wordt naar de server verzonden in de vorm van een byte-reeks UTF-8;
- regels 70-71: alle regels van het antwoord worden gelezen. Er wordt aangenomen dat het antwoord minder dan 1000 tekens bevat. Het antwoord kan uit meerdere regels bestaan. Elke regel heeft de vorm XXX-YYY, waarbij XXX een numerieke code is, behalve de laatste regel van het antwoord, die de vorm XXX YYY heeft (zonder het teken -);
- regel 76: uitlezen van de foutcode XXX uit de eerste regel;
- regels 78-80: als de numerieke code XXX groter is dan 500, dan heeft de server een fout teruggestuurd. Er wordt dan een uitzondering gegenereerd;
Resultaten
De uitvoering van het script levert de volgende console-uitvoer op:
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
- regels 3-30: het gebruik van de server SMTP [hMailServer] om een e-mail te verzenden naar [guest@localhost] verloopt goed;
- regels 32-46: het gebruik van de server SMTP [smtp.gmail.com] om een e-mail te verzenden naar [pymailparlexemple@gmail.com] verloopt niet goed: op regel 45 stuurt de server SMTP een foutcode 530 met een foutmelding. Hierin staat dat de client SMTP zich eerst via een beveiligde verbinding moet authenticeren. Onze client heeft dit niet gedaan en wordt daarom geweigerd;
De resultaten in Thunderbird zijn als volgt:

21.5.7. scripts [smtp/02]: een link SMTP geschreven met de bibliotheek [smtplib]

De vorige client vertoont ten minste twee tekortkomingen:
- hij kan geen gebruik maken van een beveiligde verbinding als de server daarom vraagt;
- hij kan geen bijlagen aan het bericht toevoegen;
We gaan de eerste tekortkoming aanpakken in het script [smtp/02]. In ons nieuwe script gaan we de Python-module [smtplib] gebruiken.
Het script [smtp/02/main] zal het volgende configuratiebestand jSON [smtp/02/config] gebruiken:
def configure() -> dict:
return {
# beschrijving: beschrijving van de verzonden e-mail
# smtp-server: server SMTP
# smtp-port: poort van de server SMTP
# from: afzender
# to: ontvanger
# onderwerp: onderwerp van de e-mail
# bericht: inhoud van de e-mail
"mails": [
{
"description": "mail to localhost via localhost avec smtplib",
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost.com",
"to": "guest@localhost.com",
"subject": "to localhost via localhost avec smtplib",
# we testen de tekens met accenten
"message": "aglaë séléné\nva au marché\nacheter des fleurs",
},
{
"description": "mail to gmail via gmail avec smtplib",
"smtp-server": "smtp.gmail.com",
"smtp-port": "587",
"from": "pymail2parlexemple@gmail.com",
"to": "pymail2parlexemple@gmail.com",
"subject": "to gmail via gmail avec smtplib",
# we testen de tekens met accenten
"message": "aglaë séléné\nva au marché\nacheter des fleurs",
# SMTP met authenticatie
"user": "pymail2parlexemple@gmail.com",
"password": "#6prIlh@1QZ3TG",
}
]
}
We zien dezelfde rubrieken als in het bestand [smtp/01/config], met twee extra rubrieken wanneer de server SMTP om authenticatie vraagt:
- regel 31, [user]: de gebruikersnaam waarmee de verbinding wordt geauthenticeerd;
- regel 32, [password]: zijn wachtwoord;
Deze twee velden zijn alleen aanwezig als de benaderde server SMTP authenticatie vereist. Dit gebeurt dan via een beveiligde verbinding.
De code van het script [smtp/02/main.py] is als volgt:
# import
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate
# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: True):
# verzendt een bericht naar de SMTP-server smtpserver namens de afzender
# voor de ontvanger. Als verbose=True, wordt de communicatie tussen client en server bijgehouden
# er wordt gebruikgemaakt van de smtplib-bibliotheek
# uitzonderingen worden doorgegeven
#
# de server SMTP
server = smtplib.SMTP(mail["smtp-server"])
# uitgebreide modus
server.set_debuglevel(verbose)
# beveiligde verbinding?
if "user" in mail:
# beveiligde verbinding
server.starttls()
# EHLO opdracht + authenticatie
server.login(mail["user"], mail["password"])
# opbouw van een Multipart-bericht – dit is het Multipart-bericht dat zal worden verzonden
msg = MIMEText(mail["message"])
msg['from'] = mail["from"]
msg['to'] = mail["to"]
msg['date'] = formatdate(localtime=True)
msg['subject'] = mail["subject"]
# het bericht wordt verzonden
server.send_message(msg)
# afsluiten
server.quit()
# main ----------------------------------------------------------------
# de gegevens worden opgehaald uit een configuratiebestand dat voor elke server de volgende informatie bevat
# beschrijving: beschrijving van de verzonden e-mail
# smtp-server: server SMTP
# smtp-port: poort van de server SMTP
# van: afzender
# to: ontvanger
# onderwerp: onderwerp van de e-mail
# content-type: codering van de e-mail
# bericht: inhoud van de e-mail
# configuratie van de applicatie
import config
config = config.configure()
# e-mails worden één voor één verwerkt
for mail in config['mails']:
try:
# logbestanden
print("----------------------------------")
print(f"Envoi du message [{mail['description']}]")
# bericht verzenden in logboekmodus
sendmail(mail, True)
# einde
print("Message envoyé...")
except BaseException as erreur:
# de fout wordt weergegeven
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
pass
# volgende e-mail
Opmerkingen
- regels 8-35: alleen de functie [sendmail] wordt gebruikt. Deze zal voortaan de module [smtplib] (regel 2) gebruiken;
- regel 16: verbinding met de server SMTP;
- regel 18: indien [verbose=True], wordt de communicatie tussen client en server weergegeven op de console;
- regels 20-24: eventuele authenticatie wordt uitgevoerd als de server SMTP dit vereist;
- regel 22: de authenticatie vindt plaats via een beveiligde verbinding;
- regel 24: authenticatie;
- regels 26-33: verzending van het bericht. De dialoog met het script [smtp/01/main] zal dan plaatsvinden. Als er authenticatie heeft plaatsgevonden, verloopt deze via een beveiligde verbinding;
- regel 35: de dialoog tussen client en server wordt beëindigd;
Voordat u het script [smtp/02/main] uitvoert, moet u de instellingen van het Gmail-account [pymailparlexemple@gmail.com] aanpassen:
- log in op het Gmail-account [pymailparlexemple@gmail.com];
- wijzig de volgende instellingen:
- in [2]: geef minder veilige apps toestemming om toegang te krijgen tot het account;
Doe hetzelfde met het tweede Gmail-account [pymail2parlexemple@gmail.com].
Resultaten
Wanneer het script [smtp/02/main] wordt uitgevoerd, krijgt men de volgende console-uitvoer:
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
- regel 40: de client [smtplib] start de dialoog om een versleutelde verbinding tot stand te brengen met de server SMTP, wat in het script [smtp/main/01] niet was gelukt;
- voor het overige zien we de bekende commando’s van het protocol SMTP;
Als we het Gmail-account van de gebruiker [pymail2parlexemple] bekijken, zien we het volgende:

21.5.8. scripts [smtp/03]: beheer van bijlagen
We vullen het script [smtp/02/main] aan, zodat de verzonden e-mail bijlagen kan bevatten.

Het script [smtp/03/main] wordt geconfigureerd door het volgende script [smtp/03/config]:
import os
def configure() -> dict:
# configuratie van de applicatie
script_dir = os.path.dirname(os.path.abspath(__file__))
return {
# beschrijving: beschrijving van de verzonden e-mail
# smtp-server: server SMTP
# smtp-port: poort van de server SMTP
# from: afzender
# to: ontvanger
# onderwerp: onderwerp van de e-mail
# bericht: inhoud van de e-mail
"mails": [
{
"description": "mail to gmail via gmail avec smtplib",
"smtp-server": "smtp.gmail.com",
"smtp-port": "587",
"from": "pymail2parlexemple@gmail.com",
"to": "pymail2parlexemple@gmail.com",
"subject": "to gmail via gmail avec smtplib",
# we testen de tekens met accenten
"message": "aglaë séléné\nva au marché\nacheter des fleurs",
# SMTP met authenticatie
"user": "pymail2parlexemple@gmail.com",
"password": "#6prIlhD&@1QZ3TG",
# hier moeten absolute paden worden opgegeven voor de bijgevoegde bestanden
"attachments": [
f"{script_dir}/attachments/fichier attaché.docx",
f"{script_dir}/attachments/fichier attaché.pdf",
]
}
]
}
Het bestand [smtp/03/config] verschilt alleen van het eerder gebruikte bestand [smtp/02/config] door de optionele aanwezigheid van een lijst [attachments] (regels 30-32), die de lijst aangeeft van bestanden die aan het te verzenden bericht moeten worden toegevoegd.
Het script [smtp/03/main] is als volgt:
# imports
import email
import mimetypes
import os
import smtplib
from email import encoders
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.mime.message import MIMEMessage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate
# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: True):
# verstuurt mail[message] naar de SMTP-server mail[smtp-server] namens mail[from]
# voor mail[to]. Als verbose=True, wordt de communicatie tussen client en server bijgehouden
# wordt de bibliotheek smtplib gebruikt
# uitzonderingen worden doorgegeven
#
# de server SMTP
server = smtplib.SMTP(mail["smtp-server"])
# uitgebreide modus
server.set_debuglevel(verbose)
# beveiligde verbinding?
if "user" in mail:
server.starttls()
server.login(mail["user"], mail["password"])
# opbouw van een Multipart-bericht – dit is het bericht dat wordt verzonden
# bron: 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"]
# het tekstbericht wordt bijgevoegd in het formaat MIMEText
msg.attach(MIMEText(mail["message"]))
# de bijlagen worden doorlopen
for path in mail["attachments"]:
# het pad moet een absoluut pad zijn
# het type van het bijgevoegde bestand wordt geraden
ctype, encoding = mimetypes.guess_type(path)
# als er geen gok is gedaan
if ctype is None or encoding is not None:
# Er kon geen schatting worden gemaakt, of het bestand is gecodeerd (gecomprimeerd), dus
# wordt een generiek ‘bag-of-bits’-type gebruikt.
ctype = 'application/octet-stream'
# wordt het type opgesplitst in hoofdtype/subtype
maintype, subtype = ctype.split('/', 1)
# de verschillende gevallen worden behandeld
if maintype == 'text':
with open(path) as fp:
# Opmerking: we moeten de berekening van de tekenset afhandelen
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)
# gevallen van het type message / rfc822
elif maintype == 'message':
with open(path, 'rb') as fp:
part = MIMEMessage(email.message_from_bytes(fp.read()))
else:
# andere gevallen
with open(path, 'rb') as fp:
part = MIMEBase(maintype, subtype)
part.set_payload(fp.read())
# De payload coderen met Base64
encoders.encode_base64(part)
# Stel de parameter voor de bestandsnaam in
basename = os.path.basename(path)
part.add_header('Content-Disposition', 'attachment', filename=basename)
# het bestand wordt aan het te verzenden bericht toegevoegd
msg.attach(part)
# alle bijlagen zijn toegevoegd – het bericht wordt als tekenreeks verzonden
server.send_message(msg)
# main ----------------------------------------------------------------
..
Opmerkingen
- regels 18-32: de functie [sendmail] blijft hetzelfde als toen er nog geen bijlagen waren;
- regel 35: de volgende code is ontleend aan officiële Python-documentatie;
- regel 36: het bericht dat verzonden gaat worden, bestaat uit verschillende onderdelen: tekst en bijlagen. Dit wordt een [Multipart]-bericht genoemd;
- regels 37-40: in het bericht [Multipart] zijn de gebruikelijke velden van elke e-mail te vinden;
- regel 42: de verschillende onderdelen van het bericht [Multipart] [msg] worden via de methode [msg.attach] (regel 81) aan het bericht toegevoegd. De bijgevoegde delen kunnen van allerlei aard zijn. Deze worden gekenmerkt door een type MIME. Het type MIME van een gewone tekst is het type [MIMEText];
- regels 44-81: alle bijlagen van het te verzenden bericht worden aan het bericht [msg Multipart] toegevoegd (regel 81);
- regel 44: [path] staat voor het absolute pad naar het bij te voegen bestand;
- regel 47: om het type MIME te vinden dat voor het bij te voegen onderdeel moet worden gebruikt, gebruiken we de extensie (.docx, .php…) van het bij te voegen bestand. De methode [mimetypes.guess_type] voert deze taak uit. Deze methode levert twee gegevens op:
- [ctype]: het type MIME van het bestand;
- [encoding]: informatie over de codering;
- regels 49-52: als het type MIME van het bestand niet kan worden bepaald, wordt het aangemerkt als een binair bestand (regel 52);
- regel 54: het type MIME van een bestand bestaat uit een hoofdtype en een secundair type, bijvoorbeeld [application/pdf]. Deze twee elementen worden gescheiden;
- regels 56-76: er worden verschillende gevallen behandeld, afhankelijk van de waarde van het hoofdtype MIME. Bijvoorbeeld, in het geval van [application/pdf] van een bestand PDF, worden de regels 70-76 uitgevoerd:
- regels 56-59: het geval waarin het bijgevoegde bestand een tekstbestand is. In dit geval wordt een element van het type [MIMEText] aangemaakt met de inhoud [fp.read];
- regels 60-62: het geval waarin het bestand een afbeelding bevat. In dit geval wordt een element van het type [MIMEImage] met de inhoud [fp.read] aangemaakt;
- regels 63-65: het geval waarin het bestand een audiobestand is. In dit geval wordt een element van het type [MIMEAudio] met de inhoud [fp.read] aangemaakt;
- regels 66-69: het geval waarin het bestand een e-mail is. In dit geval wordt een element van het type [MIMEMessage] (regel 69) aangemaakt met de inhoud [email.message_from_bytes(fp.read())]. In tegenstelling tot de voorgaande gevallen, waarin de inhoud van het element MIME de binaire inhoud van het bijbehorende bestand was, is de inhoud van het element MIMEMessage hier van het type [email.message.Message];
- regels 70-76: de overige gevallen. Dit omvat bijvoorbeeld de Word-bestanden en PDF uit ons voorbeeld;
- regel 72: het bij te voegen bestand wordt in binaire modus geopend (rb=read binary);
- regel 74: [fp.read] leest het volledige binaire bestand;
- regels 72-74: de structuur [with open(…) as file] doet twee dingen:
- het opent het bestand en wijst er de descriptor [file] aan;
- het zorgt ervoor dat bij het verlaten van [with], ongeacht of er een fout optreedt, de descriptor [file] wordt gesloten. Het is dus een alternatief voor de structuur [try file=open(…)/ finally];
- regel 73: er wordt een nieuw element [part] aangemaakt om in het Multipart-bericht op te nemen. Hier wordt de klasse [MIMEBase] gebruikt en worden de elementen [maintype, subtype], die op regel 54 zijn bepaald, doorgegeven aan de constructor;
- regel 74: het element dat in het Multipart-bericht moet worden opgenomen, moet inhoud hebben. Deze kan worden geïnitialiseerd met de methode [set_payload];
- regels 75-76: de bijgevoegde bestanden moeten een 7-bits codering ondergaan. Historisch gezien ondersteunden sommige SMTP-servers namelijk alleen 7-bits gecodeerde tekens. Hier wordt de codering ‘Base64’ gebruikt;
- regel 77: vanaf deze regel is de verwerking identiek voor alle typen die we in de regels 56-76 hebben aangemaakt;
- regel 79: het element dat aan het Multipart-bericht moet worden toegevoegd, heeft een header die het beschrijft. Hier wordt aangegeven dat het toegevoegde element een bijlage is. De naam van dit bestand is de derde parameter die aan de methode [add_header] wordt doorgegeven. De naam van dit bestand wordt vaak door e-mailprogramma’s gebruikt om het bijgevoegde bestand onder die naam in het bestandssysteem van het programma op te slaan. Tot nu toe hebben we gewerkt met de absolute naam van het bijgevoegde bestand. Hier geven we simpelweg de naam door zonder het pad (regel 78);
- regel 81: de binaire inhoud van het bestand wordt opgenomen in het bericht [msg Multipart];
- regel 83: zodra alle delen van het bericht aan [msg Multipart] zijn toegevoegd, wordt dit bericht verzonden;
Resultaten
Als het script [smtp/03/main] wordt uitgevoerd met het eerder getoonde bestand [smtp/02/config], ontvangt de rekening [pymail2parlexemple@gmail.com] het volgende:

We zien de bijgevoegde bestanden in [4, 9-11].
Laten we nu een voorbeeld bekijken met een e-mail als bijlage. We gaan de hierboven ontvangen e-mail opslaan in [3]:

We slaan de e-mail op onder de naam [mail attaché 1.eml] in de map [smtp/03/attachments].
We wijzigen nu het bestand [smtp/03/config] als volgt:
import os
def configure() -> dict:
# configuratie van de applicatie
script_dir = os.path.dirname(os.path.abspath(__file__))
return {
# beschrijving: beschrijving van de verzonden e-mail
# smtp-server: server SMTP
# smtp-port: poort van de server SMTP
# van: afzender
# to: ontvanger
# onderwerp: onderwerp van de e-mail
# bericht: inhoud van de e-mail
"mails": [
{
"description": "mail to gmail via gmail avec smtplib",
"smtp-server": "smtp.gmail.com",
"smtp-port": "587",
"from": "pymail2parlexemple@gmail.com",
"to": "pymail2parlexemple@gmail.com",
"subject": "to gmail via gmail avec smtplib",
# we testen de tekens met accenten
"message": "aglaë séléné\nva au marché\nacheter des fleurs",
# SMTP met authenticatie
"user": "pymail2parlexemple@gmail.com",
"password": "#6prIlhD&@1QZ3TG",
# hier moeten absolute paden worden opgegeven voor de bijgevoegde bestanden
"attachments": [
f"{script_dir}/attachments/fichier attaché.docx",
f"{script_dir}/attachments/fichier attaché.pdf",
f"{script_dir}/attachments/mail attaché 1.eml",
]
}
]
}
- op regel 33 hebben we een bijlage toegevoegd;
Nu voeren we het script [smtp/03/main] opnieuw uit. Dit levert het volgende resultaat op in de mailbox van de gebruiker [pymail2parlexemple@gmail.com]:

- in [1], de ontvangen e-mail;
- in [2]: de tekst van het bericht;
- in [3]: de tekst van de bijgevoegde e-mail;
- in [4]: Thunderbird heeft 5 bijlagen gevonden:
- [fichier attaché.docx];
- [fichier attaché.pdf];
- [mail attaché 1.eml]. Deze bijlage is zelf een e-mail met twee bijlagen:
- [fichier attaché.docx];
- [fichier attaché.pdf];
21.6. Het protocol POP3
21.6.1. Inleiding
Om e-mails te lezen die op een e-mailserver zijn opgeslagen, bestaan er twee protocollen:
- het protocol POP3 (Post Office Protocol), historisch gezien het eerste protocol, maar tegenwoordig weinig gebruikt;
- het protocol IMAP (Internet Message Access Protocol), een recenter protocol dan POP3 en momenteel het meest gebruikte;
Om het protocol POP3 te verkennen, gebruiken we de volgende architectuur:

- [Serveur B] is, afhankelijk van de situatie:
- een lokale POP3-server, geïmplementeerd door de [hMailServer]-mailserver;
- de server [pop.gmail.com], die de server POP3 is van de e-mailmanager [gmail.com];
- [Client A] zal in verschillende vormen een client zijn van POP3:
- de client [RawTcpClient] om het protocol POP3 te ontdekken;
- een Python-script dat het protocol POP3 van de client [RawTcpClient] nabootst;
- een Python-script dat gebruikmaakt van Python-modules voor het beheer van bijgevoegde bestanden en het gebruik van een versleutelde en geauthenticeerde verbinding wanneer de server POP3 dit vereist;
21.6.2. Verkenning van het protocol POP3
Net zoals we hebben gedaan met het protocol SMTP, gaan we het protocol POP3 verkennen aan de hand van de logbestanden van de e-mailserver [hMailServer]. Hiervoor moeten we deze server opstarten.
Met Thunderbird gaan we:
- een e-mail sturen naar de gebruiker [guest@localhost.com];
- de mailbox van deze gebruiker lezen;


In [3-6] hierboven staat het bericht dat de gebruiker [guest@localhost.com] heeft ontvangen.
We bekijken nu de logbestanden van de server [hMailServer]. Hiervoor gebruiken we de beheertool [hMailServer Administrator]:

De logbestanden van POP3 zien er als volgt uit (de laatste regels in het logbestand van vandaag):
"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..."
- regel 1: de server POP3 stuurt een welkomstbericht naar de client (Thunderbird) die zojuist verbinding heeft gemaakt;
- regel 2: de client stuurt het commando [CAPA] (capabilities) om de lijst met commando’s op te vragen die hij kan gebruiken;
- regel 3: de server antwoordt dat hij de commando’s [USER, UIDL, TOP] kan gebruiken. De server POP begint zijn antwoorden met [+OK] of [-ERR] om aan te geven of het uitvoeren van het commando van de client is geslaagd of mislukt;
- regel 4: de client verstuurt het commando [USER guest] om aan te geven dat hij de mailbox van gebruiker [guest] wil raadplegen;
- regel 5: de server antwoordt met [+OK] en vraagt om het wachtwoord van [guest];
- regel 6: de client verstuurt het commando [PASS password] om het wachtwoord van de gebruiker [guest] te verzenden. Hier wordt het wachtwoord in leesbare vorm verzonden, omdat de server POP3 geen beveiligde verbinding heeft opgelegd. We zullen zien dat dit anders zal zijn bij de Gmail-server POP3;
- regel 7: de server heeft de combinatie van gebruikersnaam en wachtwoord gevalideerd. Hij geeft aan dat hij de mailbox van de gebruiker [guest] blokkeert;
- regel 8: de client stuurt de opdracht [STAT], waarmee informatie over de mailbox wordt opgevraagd;
- regel 9: de server antwoordt dat er één bericht van 612 bytes is. Over het algemeen antwoordt de server dat er N berichten zijn en geeft hij de totale grootte van deze berichten aan;
- regel 10: de client stuurt het commando [LIST]. Dit commando vraagt om de lijst met berichten;
- regel 11: de server stuurt de lijst met berichten in de volgende vorm:
- een overzichtregel met het aantal berichten en hun totale grootte;
- één regel per bericht met het berichtnummer en de grootte;
- regel 13: de client verstuurt het commando [UIDL], waarmee de lijst met berichten en hun identificatienummers wordt opgevraagd. Elk bericht wordt namelijk binnen de e-maildienst aangeduid met een uniek nummer;
- regel 14: het antwoord van de server. Hieruit blijkt dat bericht nr. 1 in de lijst de identificatiecode 42 heeft;
- regel 15: de client verstuurt het commando [RETR 1], waarmee wordt gevraagd om bericht nr. 1 uit de lijst door te sturen;
- regel 16: de server POP3 voert dit uit;
- regel 17: de client verstuurt het commando [QUIT] om aan te geven dat hij de verbinding met de server POP3 gaat verbreken;
- regel 18: de server verbreekt ook de verbinding met de client, maar stuurt hem eerst een afscheidsbericht;
We gaan nu onderdelen van de bovenstaande dialoog nabootsen met behulp van de client [RawTcpClient], uitgevoerd in een venster PyCharm:

De dialoog ziet er als volgt uit:
(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
- regel 1: er wordt een verbinding geopend met poort 110 van de machine [localhost]. Daar draait de service POP3 van [hMailServer];
- op de regels 5, 7, 9, 13 en 34 gebruiken we de commando’s [USER, PASS, LIST, RETR, QUIT];
- regel 4: het welkomstbericht van de server POP3;
- regel 5: we geven aan dat we toegang willen tot de mailbox van de gebruiker [guest];
- regel 7: we sturen het wachtwoord van de gebruiker [guest] in leesbare tekst;
- regel 9: de lijst met berichten in de mailbox wordt opgevraagd;
- regel 13: het bericht nr. 1 wordt opgevraagd;
- regels 14-33: de server POP3 verstuurt bericht nr. 1;
- regel 34: de sessie wordt beëindigd;
Hier volgt een overzicht van enkele veelgebruikte commando's die door een POP3-server worden geaccepteerd:
- het commando [USER] wordt gebruikt om de gebruiker te definiëren wiens mailbox men wil lezen;
- het commando [PASS] dient om het wachtwoord in te stellen;
- het commando [LIST] vraagt de lijst met berichten op die zich in de mailbox van de gebruiker bevinden;
- het commando [RETR] vraagt om het bericht waarvan het nummer wordt doorgegeven;
- het commando [DELE] vraagt om het bericht met het opgegeven nummer te verwijderen;
- het commando [QUIT] geeft aan de server door dat men klaar is;
Het antwoord van de server kan verschillende vormen aannemen:
- een enkele regel die begint met [+OK] om aan te geven dat het vorige commando van de client is geslaagd;
- één regel die begint met [-ERR] om aan te geven dat het vorige commando van de client is mislukt;
- meerdere regels waarbij:
- de eerste regel begint met [+OK];
- de laatste regel bestaat uit één punt;
21.6.3. scripts [pop3/01]: een standaard POP3-client

Aangezien het protocol POP3 dezelfde structuur heeft als het protocol SMTP, is het script [pop3/01/main.py] een aanpassing van het script [smtp/01/main.py]. Het zal het volgende configuratiebestand [pop3/01/config.py] hebben:
def configure() -> dict:
# de mailboxen waaruit de e-mails worden opgehaald
mailboxes = [
# server: server POP3
# poort: poort van de server POP3
# gebruiker: de gebruiker wiens berichten je wilt lezen
# wachtwoord: zijn wachtwoord
# maxmails: het maximale aantal e-mails dat moet worden gedownload
# timeout: maximale wachttijd voor een antwoord van de server
# encoding: codering van de ontvangen e-mails
# delete: indien True, worden de e-mails uit de mailbox verwijderd
# zodra ze lokaal zijn gedownload
{
"server": "localhost",
"port": "110",
"user": "guest",
"password": "guest",
"maxmails": 10,
"timeout": 1.0,
"encoding": "utf-8",
"delete": False
}
]
# wordt de configuratie teruggezet
return {
"mailboxes": mailboxes
}
- regels 3-24: de lijst met te raadplegen mailboxen. Hier is er slechts één;
- regels 4-12: betekenis van de woordenboekelementen die elk van de mailboxen definiëren;
- regel 15: de opgevraagde server POP3 is de lokale server [hMailServer];
- regels 17-18: we willen de mailbox van de gebruiker [guest@localhost] lezen;
- regel 19: er worden maximaal 10 e-mails gelezen;
- regel 20: de client wacht maximaal 1 seconde op een antwoord van de server;
- regel 21: het coderingstype van de gelezen berichten;
- regel 22: de gedownloade berichten worden niet verwijderd;
Het script [pop3/01/main.py] is als volgt:
# imports
import re
import socket
# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
# leest de mailbox die wordt beschreven door het woordenboek [mailbox]
# als verbose=True, houdt het client-serververkeer bij
…
# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
# verzendt een commando naar het verbindingskanaal
# uitgebreide modus als verbose=True
# als with_rclf=True, voegt de rclf-reeks toe aan de uitwisseling
# geeft de eerste regel van het antwoord weer
…
# --------------------------------------------------------------------------
def affiche(echange: str, sens: int):
…
# main ----------------------------------------------------------------
# client POP3 (Post Office Protocol) waarmee berichten uit een mailbox kunnen worden gelezen
# communicatieprotocol POP3 client-server
# -> client maakt verbinding met poort 110 van de SMTP-server
# <- de server stuurt hem een welkomstbericht
# -> de client stuurt het commando USER gebruiker
# <- de server antwoordt met OK of niet
# -> de client verstuurt het commando PASS mot_de_passe
# <- server antwoordt met OK of niet
# -> de client verstuurt het commando LIST
# <- server antwoordt met OK of niet
# -> de client verstuurt het commando RETR, met een uniek nummer voor elke e-mail
# <- server antwoordt met OK of niet. Indien OK, verzendt de inhoud van de gevraagde e-mail
# -> de server verstuurt alle regels van de e-mail en sluit af met een regel die het
# enige teken bevat.
# -> de client verstuurt het commando DELE nr. om een e-mail te verwijderen
# <- de server antwoordt met OK of niet
# # -> de client verstuurt het commando QUIT om de dialoog met de server te beëindigen
# <- server antwoordt met OK of niet
# de antwoorden van de server hebben de vorm +OK tekst of -ERR tekst
# Het antwoord kan uit meerdere regels bestaan. In dat geval bestaat de laatste regel uit één enkele punt
# De uitgewisselde tekstregels moeten eindigen met de tekens RC(#13) en LF(#10)
#
# De configuratie van de applicatie wordt opgehaald
import config
config = config.configure()
# de mailboxen worden één voor één verwerkt
for mailbox in config['mailboxes']:
try:
# consoleweergave
print("----------------------------------")
print(
f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
# de mailbox wordt in de uitgebreide modus gelezen
readmails(mailbox, True)
# einde
print("Lecture terminée...")
except BaseException as erreur:
# de fout wordt weergegeven
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
pass
Opmerkingen
Zoals gezegd is [pop3/01/main.py] een aanpassing van het script [smtp/01/main.py] dat we al hebben besproken. We zullen alleen de belangrijkste verschillen toelichten:
- regel 64: de functie [readmails] is verantwoordelijk voor het inlezen van de e-mails uit een mailbox. De inloggegevens voor deze mailbox staan in het woordenboek [mailbox]. De tweede parameter [True] is de parameter [Verbose], die hier vraagt om het bijhouden van de communicatie tussen client en server;
De functie [readmails] is als volgt:
# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
# leest de e-mails uit de mailbox die wordt beschreven door het woordenboek [mailbox]
# als verbose=True, houdt het client-serververkeer bij
# de parameters van de mailbox worden geïsoleerd
# er wordt aangenomen dat het woordenboek [mailbox] geldig is
server = mailbox['server']
port = int(mailbox['port'])
user = mailbox['user']
password = mailbox['password']
maxmails = mailbox['maxmails']
delete = mailbox['delete']
timeout = mailbox['timeout']
# systeemfouten worden doorgegeven
connexion = None
try:
# een verbinding openen op poort [port] van [server] met een time-out van één seconde
connexion = socket.create_connection((server, port), timeout=timeout)
# de verbinding is een bidirectionele communicatiestroom
# tussen de client (dit programma) en de benaderde POP3-server
# dit kanaal wordt gebruikt voor de uitwisseling van opdrachten en informatie
# welkomstbericht lezen
send_command(mailbox, connexion, "", verbose, True)
# commando USER
send_command(mailbox, connexion, f"USER {user}", verbose, True)
# commando PASS
send_command(mailbox, connexion, f"PASS {password}", verbose, True)
# opdracht LIST
première_ligne = send_command(mailbox, connexion, "LIST", verbose, True)
# analyse van de eerste regel om het aantal berichten te bepalen
match = re.match(r"^\+OK (\d+)", première_ligne)
nbmessages = int(match.groups()[0])
# we doorlopen de berichten
imessage = 0
while imessage < nbmessages and imessage < maxmails:
# commando RETR
send_command(mailbox, connexion, f"RETR {imessage + 1}", verbose, True)
# commando DELE
if delete:
send_command(mailbox, connexion, f"DELE {imessage + 1}", verbose, True)
# volgend bericht
imessage += 1
# commando QUIT
send_command(mailbox, connexion, "QUIT", verbose, True)
# einde
finally:
# verbinding verbreken
if connexion:
connexion.close()
Opmerkingen
- regels 8-14: de configuratiegegevens van de te raadplegen mailbox worden opgehaald;
- regels 19-20: er wordt een verbinding met de server POP3 tot stand gebracht;
- regels 26-27: het welkomstbericht dat door de server is verzonden, wordt gelezen;
- regels 28-29: het commando [USER] wordt verzonden om de gebruiker te identificeren wiens e-mails we willen ophalen;
- regels 30-31: het commando [PASS] wordt verzonden om het wachtwoord van deze gebruiker op te geven;
- regels 32-33: het commando [LIST] wordt verzonden om te achterhalen hoeveel e-mails er in de mailbox van deze gebruiker staan. De functie [sendCommand] retourneert de eerste regel van het antwoord van de server. Hierin geeft de server aan hoeveel berichten er in de mailbox staan;
- regels 34-36: het aantal berichten wordt opgehaald uit de eerste regel van het antwoord;
- regels 39-46: er wordt een lus doorlopen voor elk bericht. Voor elk bericht worden twee commando’s verzonden:
- RETR i: om bericht nr. i op te halen (regels 40-41);
- DELE i: om het bericht te verwijderen als de configuratie vereist dat gelezen berichten van de server worden verwijderd (regels 43-44);
- regels 47-48: we sturen het commando [QUIT] om de server te laten weten dat we klaar zijn;
De functie [send_command] is als volgt:
# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
# verzendt commando naar het verbindingskanaal
# logboekmodus indien verbose=True
# als with_rclf=True, voeg de rclf-reeks toe aan de uitwisseling
# geeft de eerste regel van het antwoord weer
# einde-van-regel-teken
if with_rclf:
rclf = "\r\n"
else:
rclf = ""
# verzend commando indien niet leeg
if commande:
connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
# eventuele echo
if verbose:
affiche(commande, 1)
# de socket lezen alsof het een tekstbestand is
encoding = f"{mailbox['encoding']}" if mailbox['encoding'] else None
file = connexion.makefile(encoding=encoding)
# dit bestand wordt regel voor regel verwerkt
# eerste regel lezen
première_ligne = réponse = file.readline().strip()
# uitgebreide modus?
if verbose:
affiche(première_ligne, 2)
# foutcode ophalen
code_erreur = réponse[0]
if code_erreur == "-":
# er is een fout opgetreden
raise BaseException(réponse[5:])
# speciaal geval van antwoorden met meerdere regels LIST, RETR
cmd = commande.lower()[0:4]
if cmd == "list" or cmd == "retr":
# laatste regel van het antwoord?
dernière_ligne = False
while not dernière_ligne:
# volgende regel lezen
ligne_suivante = file.readline().strip()
# uitgebreide modus?
if verbose:
affiche(ligne_suivante, 2)
# laatste regel?
dernière_ligne = ligne_suivante == "."
# klaar – de eerste regel wordt weergegeven
return première_ligne
Opmerkingen
- regels 13-18: het commando [command] wordt alleen naar de server POP3 verzonden als het niet leeg is. Dit is nodig om het welkomstbericht van de server POP3 te lezen, dat de server verstuurt zelfs als de client nog geen opdrachten heeft verzonden;
- regels 19-21: de socket wordt gelezen alsof het een tekstbestand is. Hierdoor kunnen we de methode [readline] (regel 24) gebruiken en zo het bericht regel voor regel lezen. We gebruiken de sleutel [encoding] uit het woordenboek [mailbox] om de codering aan te geven van de regels die gelezen gaan worden;
- regel 24: we lezen de eerste regel van het antwoord;
- regels 28-32: we behandelen het geval van een eventuele fout. Deze zijn van het type [-ERR invalid password, -ERR mailbox unknown, -ERR unable to lock mailbox…];
- regel 32: er wordt een uitzondering gegenereerd met de foutmelding;
- regel 35: alleen de commando’s van het type [list, retr] kunnen antwoorden van meerdere regels bevatten;
- regels 36-45: in het geval van een antwoord met meerdere regels worden alle ontvangen regels (regels 42-43) weergegeven totdat de laatste regel is ontvangen (regel 45);
- regel 46: de eerste gelezen regel wordt weergegeven, omdat deze bij het commando [LIST] het aantal berichten in de mailbox bevat;
Resultaten
Laten we het vorige voorbeeld nemen. Met Thunderbird hadden we het volgende bericht naar de gebruiker [guest@localhost] verzonden (de server hMailServer moet gestart zijn):

Bij uitvoering krijgen we de volgende resultaten:
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
- regels 15-31: het bericht dat naar [guest@localhost] is verzonden, wordt correct opgehaald.
Dit is een eenvoudige POP3-client waaraan bepaalde mogelijkheden ontbreken:
- de mogelijkheid om te communiceren met een beveiligde POP3-server;
- de mogelijkheid om bijlagen bij een bericht te lezen;
We gaan deze twee mogelijkheden implementeren met een nieuw script dat deze keer complexer zal zijn.
21.6.4. scripts [pop3/02]: POP3-client met de modules [poplib] en [email]
We gaan een client POP3 schrijven waarmee bijlagen kunnen worden beheerd en communicatie met beveiligde servers mogelijk is. Daarnaast zullen we de berichten en hun bijlagen opslaan in bestanden.
We gaan twee Python-modules gebruiken:
- [poplib]: deze zorgt voor het protocol POP3;
- [email]: deze bevat talrijke submodules waarmee we de ontvangen berichten kunnen analyseren. Elk bericht is een gestructureerde tekenreeks waarin we het volgende kunnen terugvinden:
- de kopteksten van het bericht [From, To, Subject, Return-Path…];
- het bericht in tekstvorm en eventueel in HTML;
- de bijlagen;

Het script [inet/pop3/02/main] [1] wordt geconfigureerd door het bestand [inet/pop3/02/config] [2] en maakt gebruik van de module [inet/shared/mail_parser] [3].
Het bestand [pop3/02/config] ziet er als volgt uit:
import os
def configure() -> dict:
# app-configuratie
config = {
# lijst met te beheren mailboxen
"mailboxes": [
# server: server POP3
# poort: poort van de server POP3
# gebruiker: gebruiker wiens berichten men wil lezen
# wachtwoord: zijn wachtwoord
# maxmails: het maximale aantal e-mails dat moet worden gedownload
# timeout: maximale wachttijd voor een antwoord van de server
# delete: op 'waar' indien de gedownloade berichten van de server moeten worden verwijderd
# ssl: op 'waar' indien de e-mails via een beveiligde verbinding worden gelezen
# output: de map waarin de gedownloade berichten worden opgeslagen
{
"server": "pop.gmail.com",
"port": "995",
"user": "pymail2parlexemple@gmail.com",
"password": "#6prIlhD&@1QZ3TG",
"maxmails": 10,
"delete": False,
"ssl": True,
"timeout": 2.0,
"output": "output"
}
]
}
# absoluut pad naar de map van het script
script_dir = os.path.dirname(os.path.abspath(__file__))
# absolute paden van de mappen die in de syspath moeten worden opgenomen
absolute_dependencies = [
# lokaal bestand
f"{script_dir}/../../shared",
]
# configuratie van de syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# de configuratie wordt weergegeven
return config
Het bestand definieert de lijst met te raadplegen mailboxen en stelt het Python-pad van de applicatie in.
Er is hier slechts één mailbox:
- regels 22-23: de gebruiker wiens e-mails we willen lezen;
- regels 20-21: de naam en de poort van de server POP3 waarop de e-mails van deze gebruiker zijn opgeslagen;
- regel 24: het maximale aantal e-mails dat moet worden opgehaald. Als u dit script namelijk op uw eigen mailbox probeert, wilt u waarschijnlijk niet de honderden e-mails ophalen die daar staan;
- regel 25: een booleaanse waarde die aangeeft of een e-mail na het lezen moet worden verwijderd (delete=True);
- regel 26: als het attribuut [ssl] op True staat, betekent dit dat de server POP3, gedefinieerd in de regels 20-21, een versleutelde verbinding gebruikt;
- regel 27: de maximale wachttijd voor antwoorden van de server, uitgedrukt in seconden;
- regel 28: de map waarin de gelezen e-mails moeten worden opgeslagen. Deze wordt aangemaakt als hij nog niet bestaat. Hier wordt een relatieve naam gebruikt. Bij uitvoering is deze relatief ten opzichte van de map van waaruit u het script start. Met [Pycharm] is dit de map van het script [pop3/02];
Het script [pop3/02/main] ziet er als volgt uit:
# imports
import email
import os
import poplib
import shutil
# een mailbox lezen
def readmails(mailbox: dict, verbose: bool):
# leest de mailbox die wordt beschreven door het woordenboek [mailbox]
# als verbose=True, houdt het client-serververkeer bij
…
# main ----------------------------------------------------------------
# POP3-client (Post Office Protocol) waarmee e-mails kunnen worden gelezen
# de configuratie van de applicatie wordt opgehaald
import config
config = config.configure()
# de mailboxen worden één voor één verwerkt
for mailbox in config['mailboxes']:
try:
# consoleweergave
print("----------------------------------")
print(
f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
# de mailbox lezen in de uitgebreide modus
readmails(mailbox, True)
# einde
print("Lecture terminée...")
except BaseException as erreur:
# de fout wordt weergegeven
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
pass
- regels 17-36: het gedeelte [main] van het script is vergelijkbaar met dat van het script [pop3/01];
De functie [readmails] is als volgt:
# een mailbox lezen
def readmails(mailbox: dict, verbose: bool):
# leest de mailbox die wordt beschreven door het woordenboek [mailbox]
# als verbose=True, houdt het client-serververkeer bij
# mail_parser importeren
from mail_parser import save_message
# de parameters van de mailbox worden geïsoleerd
# er wordt aangenomen dat het woordenboek [mailbox] geldig is
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']
# we laten systeemfouten doorgeven
pop3 = None
try:
# de opslagmappen worden aangemaakt als ze nog niet bestaan
if not os.path.isdir(output):
os.mkdir(output)
# gebruiker
dir2 = f"{output}/{user}"
# de map [dir2] wordt verwijderd als deze bestaat, waarna deze opnieuw wordt aangemaakt
if os.path.isdir(dir2):
# verwijderen
shutil.rmtree(dir2)
# aanmaken
os.mkdir(dir2)
# een verbinding openen op poort [port] van [server]
if ssl:
pop3 = poplib.POP3_SSL(server, port, timeout=timeout)
else:
pop3 = poplib.POP3(server, port, timeout=timeout)
# verbinding vertegenwoordigt een bidirectionele communicatiestroom
# tussen de client (dit programma) en de benaderde POP3-server
# dit kanaal wordt gebruikt voor de uitwisseling van opdrachten en informatie
# uitgebreide modus
pop3.set_debuglevel(2 if verbose else 0)
# welkomstbericht lezen
pop3.getwelcome( )
# commando USER
réponse = pop3.user(user)
# commando PASS
réponse = pop3.pass_(password)
# commando LIST
liste = pop3.list()
# de e-mails staan in de lijst[1]
imail = 0
nb_mails = len(liste[1])
fini = imail == maxmails or imail == nb_mails
éléments = liste[1]
while not fini:
# huidig element
élément = éléments[imail]
# element is een lijst met bytes die wordt gedecodeerd tot een tekenreeks
desc = élément.decode()
# we hebben een tekenreeks gescheiden door spaties
# het eerste element is het berichtnummer
num = desc.split()[0]
# het bericht wordt opgehaald
message = pop3.retr(int(num))
# de regels van het bericht staan in bericht [1]
str_message = ""
for ligne in message[1]:
# een regel is een reeks bytes die we decoderen tot een tekenreeks
str_message += f"{ligne.decode()}\r\n"
# map van het bericht
dir3 = f"{dir2}/message_{num}"
# als de map niet bestaat, wordt deze aangemaakt
if not os.path.isdir(dir3):
os.mkdir(dir3)
# onderwerp email.message.Message
save_message(dir3, email.message_from_string(str_message), 0)
# nog een e-mail
imail += 1
# is het maximum bereikt?
fini = imail == maxmails or imail == nb_mails
# bestelling QUIT
pop3.quit()
finally:
# verbinding verbreken
if pop3:
pop3.close()
Opmerkingen
- regels 6-7: de functie [mail_parser.save_message], die op regel 80 wordt gebruikt, wordt geïmporteerd;
- de functiecode is ingekapseld in een try (regel 22)/finally (regel 88). Zo worden alle uitzonderingen doorgegeven aan de hoofdcode, die ze afbreekt en weergeeft;
- regels 11-18: de configuratiegegevens van de mailbox worden opgehaald;
- regels 23-33: alle berichten worden opgeslagen in de map [output/user], waarbij [output] en [user] in de configuratie zijn gedefinieerd. We maken dus achtereenvolgens de mappen [output] en vervolgens [output/user] aan. Om deze laatste aan te maken, verwijderen we eerst regel 31. [shutil] is een module die moet worden geïmporteerd. [shutil.rmtree(dir)] verwijdert de map [dir] en alles wat deze bevat;
- voor alle bewerkingen op systeembestanden wordt de module [os] gebruikt, die ook moet worden geïmporteerd;
- regels 34-38: er wordt een verbinding met de server POP3 geopend. Als de server beveiligd is, wordt de klasse [poplib.POP3_SSL] gebruikt, anders de klasse [poplib.POP3]. Het attribuut [ssl] dat in regel 35 wordt gebruikt, is afkomstig uit de configuratie van de mailbox;
- regel 45: er wordt een logniveau ingesteld:
- 0: geen logboekregistratie;
- 1: de commando’s die door de client POP3 worden verzonden, worden gelogd;
- 2: gedetailleerde logboeken. We zien ook wat de client POP3 ontvangt;
- regel 47: na het inloggen stuurt de server POP3 een welkomstbericht. Dit bericht wordt weergegeven;
- regels 48-49: commando USER van het protocol POP3;
- regels 50-51: commando PASS van het protocol POP3;
- regels 52-53: commando LIST van het protocol POP3. Het antwoord is een tuple (response, ['mesg_num octets'…], bytes), bijvoorbeeld lijst=(b'+OK 3 berichten (3859 bytes)', [b'1 584', b'2 550', b'3 2725'], 22). We zien dat de eerste twee elementen van het tuple bytes zijn (voorvoegsel b). lijst[1] is een array waarin elk element een reeks bytes is die twee gegevens bevat: het berichtnummer en de grootte in bytes;
- regel 56: uit het voorgaande volgt dat het aantal berichten in de mailbox kan worden verkregen via [len[liste1]];
- regels 59-84: er wordt een lus doorlopen voor elk bericht. De lus stopt wanneer alle berichten zijn gelezen of wanneer het maximale aantal e-mails is bereikt dat in de configuratie is ingesteld;
- regel 61: het huidige element van de array lijst[1], dus zoiets als b'1 584', een reeks bytes;
- regel 63: we zetten de reeks bytes om in een tekenreeks. We hebben nu de tekenreeks '1 584';
- regel 66: we halen het berichtnummer op, in dit geval de tekenreeks '1';
- regel 68: we sturen het commando POP3 RETR num. We krijgen een antwoord in de vorm van:
[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)]
- (vervolg)
- message is een tuple met drie elementen;
- message[1] is een array van regels. Elke regel is een reeks bytes (voorvoegsel b). Het volledige bericht wordt gevormd door deze verzameling regels;
- [Return-Path, Received, To, Subject, Message-ID, Content-Type, Content-Transfer-Encoding, Content-Language] zijn de headers van het bericht. Elk daarvan geeft informatie over het ontvangen bericht. Met deze informatie kan de hoofdtekst van het bericht worden opgehaald (het voorlaatste element van de array message[1]);
- regels 71-73: we maken de tekenreeks [strMessage] aan, die bestaat uit alle regels van het bericht. We hebben het bericht nu in de vorm van een tekenreeks. Dit bericht kan andere berichten bevatten, evenals bijlagen. Bijlagen worden namelijk ook in de vorm van een tekenreeks meegestuurd. Een belangrijk punt om te onthouden is dus dat een e-mail in eerste instantie een tekenreeks is en dat deze tekenreeks moet worden geanalyseerd om de bijlagen, eventuele andere ingekapselde berichten en natuurlijk de hoofdtekst van het bericht – wat de afzender heeft geschreven – eruit te halen;
- regels 74-78: we gaan de hoofdtekst van het bericht en de bijlagen opslaan in de map [dir3];
- regels 79-80: we delegeren de analyse van het bericht aan een functie [save_message]:
- de eerste parameter is [dir3], de map waarin de inhoud van het bericht moet worden opgeslagen;
- De tweede parameter is van het type [email.message.Message]. Dit object beschikt over methoden om de verschillende onderdelen van het bericht (hoofdtekst, bijlagen) en alle headers op te halen. Om over dit object te kunnen beschikken, moet de module [email] worden geïmporteerd. Met de functie [email.message_from_string] kan een object van het type [email.message.Message] worden aangemaakt op basis van de tekenreeks van het bericht;
De functie [save_message] maakt deel uit van de module [mail_parser]:

De module [mail_parser] is geïmporteerd in de regels 6-7 van de functie [readmails];
In [mail_parser.py] is de functie [save_message] als volgt:
# importen
import codecs
import email.contentmanager
import email.header
import email.iterators
import email.message
import os
# een bericht van het type email.message.Message opslaan
# deze functie kan recursief worden aangeroepen
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
# uitvoer: map voor het opslaan van berichten
# email_message: het bericht dat moet worden opgeslagen
# irfc822: lopend nummer in de nummering van de bijgevoegde e-mails
#
# deel van het bericht
part = email_message
# de headers [From, To, Subject] worden aangetroffen in een van de multipart-delen
# of in een deel [text/*] wanneer er geen deel [multipart] is
keys = part.keys()
# "From" moet deel uitmaken van de headers, anders bevat het deel niet de headers die we zoeken
if "From" in keys:
# we halen bepaalde headers op
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'))}"]
# de headers worden opgeslagen in een tekstbestand
with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
# schrijven naar bestand
string = '\r\n'.join(headers)
file.write(f"{string}\r\n")
# type van het bestand [part]
main_type = part.get_content_maintype()
…
Opmerkingen
- regel 12: de functie ontvangt maximaal drie parameters:
- [output]: de map waarin het bericht moet worden opgeslagen (2e parameter);
- [email_message]: een bericht van het type [email.message.Message]. Dit type is een gestructureerd type. Het bevat de tekst van de e-mail en alle bijgevoegde bestanden en biedt methoden om de verschillende elementen ervan op te halen;
- [irfc822]: deze parameter wordt gebruikt om de e-mails te nummeren die zijn ingekapseld in [email_message];
- regel 18: het object [email_message] wordt in [part] geplaatst. Het type [email.message.Message] bevat onderdelen [part] (berichttekst, bijlagen, ingekapselde e-mails) die eveneens het type [email.message.Message] hebben. Elk onderdeel van het type [part] kan subonderdelen hebben. Het type [email.message.Message] is dus een boomstructuur van elementen van het type [email.message.Message]:
- [part.ismultipart()] is gelijk aan [True] als het onderdeel [part] subonderdelen bevat. Deze zijn dan beschikbaar via [part.get_payload()];
- wanneer [part.ismultipart()] gelijk is aan [False], betekent dit dat men bij een blad van de boomstructuur van het oorspronkelijke bericht is aangekomen: dit kan zijn:
- de hoofdtekst van het bericht in de vorm van gewone tekst;
- de hoofdtekst van het bericht in de vorm van een HTML-tekst;
- een bijlage (met uitzondering van een ingekapseld bericht, waarbij [part.ismultipart()] gelijk is aan [True]);
- vanwege de boomstructuur van de parameter [email.message.Message] wordt de functie [save_message] recursief aangeroepen. De recursie stopt wanneer de bladeren van de boomstructuur worden bereikt, d.w.z. een onderdeel [part] waarvoor [part.ismultipart()] gelijk is aan [False];
- regel 21: we vragen om de sleutels (of headers) van het bericht dat momenteel wordt geanalyseerd (dat door de recursie een onderdeel van het oorspronkelijke bericht kan zijn);
- regels 23-35: we willen de headers opslaan:
- [From]: de afzender van het bericht;
- [To]: de ontvanger van het bericht;
- [Subject]: het onderwerp van het bericht;
- [Return-Path]: de ontvanger aan wie we moeten antwoorden als we willen reageren. Deze informatie staat namelijk niet altijd in [From];
- [User-Agent]: de client POP3 die communiceert met de server POP3;
- [Date]: datum waarop de e-mail is verzonden;
- regel 23: slechts één van de delen van een bericht bevat deze headers. Voor de andere delen wordt de code van de regels 23-35 genegeerd;
- regels 25-30: er wordt een lijst aangemaakt met de zes headers;
- regel 25: laten we de eerste header analyseren:
- [part.get(key)] geeft de header die hoort bij de sleutel [key];
- deze header kan gecodeerd zijn. Als deze codering niet UTF-8 is, wordt de header gedecodeerd om deze vervolgens met behulp van de functie [decode_header] opnieuw in UTF-8 te coderen;
- de eerste header heeft de vorm [From: pymail2lexemple@gmail.com];
- regels 31-35: de headers worden opgeslagen in het bestand [output/headers.txt];
De functie [decode_header] is als volgt (nog steeds in [mail_parser.py]):
# decoderen van headers
def decode_header(header: object) -> str:
# de header wordt gedecodeerd
header = email.header.decode_header(f"{header}")
# het resultaat is een array – hier zal deze slechts één element bevatten van het type (header, encoding)
# als encoding==None, dan is header een tekenreeks
# anders is het een lijst met bytes die zijn gecodeerd volgens encoding
header, encoding = header[0]
if not encoding:
# als er geen codering is
return header
else:
# als er codering is, wordt er gedecodeerd
return header.decode(encoding)
Opmerkingen
- regel 4: de koptekst wordt gedecodeerd:
- de module [email.header] moet worden geïmporteerd;
- we krijgen een lijst met tuples [(header1,encoding1) , (header2, encoding2)…];
- voor de headers [From, To, Subject, Return-Path, Date] zal de lijst slechts één element bevatten;
- regel 8: we halen de enige header en de bijbehorende codering op:
- als [encoding==None], dan is [header] de header in de vorm van een tekenreeks;
- anders is [header] een reeks bytes die de gecodeerde header vertegenwoordigt;
- regels 10-11: als er geen codering was, dan wordt de header weergegeven;
- regels 12-14: als er wel een codering was, dan decoderen we de opgehaalde reeks bytes tot een tekenreeks en geven we deze weer;
Laten we teruggaan naar de functie [save_message]:
# opslaan van een bericht van het type email.message.Message
# deze functie kan recursief worden aangeroepen
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
# uitvoer: map waarin de berichten worden opgeslagen
# email_message: het op te slaan bericht
# irfc822: lopend nummer in de nummering van de bijgevoegde e-mails
#
# deel van het bericht
part = email_message
# de headers [From, To, Subject] worden aangetroffen in een van de multipart-delen
# of in een deel [text/*] wanneer er geen deel [multipart] is
keys = part.keys()
# "From" moet deel uitmaken van de headers, anders bevat het deel niet de headers die we zoeken
if "From" in keys:
# we halen bepaalde headers op
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'))}"]
# de headers worden opgeslagen in een tekstbestand
with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
# schrijven naar bestand
string = '\r\n'.join(headers)
file.write(f"{string}\r\n")
# type van het bericht [part]
main_type = part.get_content_maintype()
sub_type = part.get_content_subtype()
type_of_part = f"{main_type}/{sub_type}"
# als het bericht van het type text/plain is
if type_of_part == "text/plain":
# tekstbericht
save_textmessage(output, part, 0)
# als het bericht van het type text/html is
elif type_of_part == "text/html":
# bericht HTML
save_textmessage(output, part, 1)
# als het bericht uit onderdelen bestaat
elif part.is_multipart():
…
else:
…
# de overige delen worden genegeerd (geen text/plain, geen text/html, geen bijlage)
# de huidige waarde van irfc822 wordt weergegeven (nummering van de bijgevoegde e-mails die in de map output zijn opgeslagen)
return irfc822
Opmerkingen
- regels 1-26: de headers van het oorspronkelijke bericht zijn verwerkt;
- regels 28-31: de onderdelen van een bericht van het type [email.message.Message] hebben een hoofdtype en een subtype. Deze worden opgehaald;
- regels 32-35: als het verwerkte onderdeel het type [text/plain] heeft, zijn we bij een blad van de boomstructuur van het oorspronkelijke bericht aangekomen. Dit is de tekst die de afzender in zijn bericht heeft geschreven;
- regel 35: deze tekst wordt opgeslagen in een bestand:
- de eerste parameter [output] is de map waarin de tekst moet worden opgeslagen;
- de tweede parameter is het deel van het bericht dat de op te slaan tekst bevat;
- de derde parameter is 0 om normale tekst op te slaan, 1 voor een tekst van het type HTML;
- regels 37-40: als het gedeelte van het type [text/html] is, dan zijn we ook bij een blad van de boomstructuur van het oorspronkelijke bericht aangekomen. Dit is de tekst die de afzender in zijn bericht heeft geschreven, ditmaal in het formaat HTML. Niet alle e-mailprogramma’s ondersteunen dit formaat;
De functie [save_textmessage] werkt als volgt:
# opslaan van een tekstbericht
def save_textmessage(output: str, part: email.message.Message, type_of_text: int):
# kopteksten
headers = []
# tekenset van het bericht
charset = part.get_content_charset()
if charset is not None:
charset = part.get_content_charset().lower()
headers.append(f"Charset: {charset}")
# codering van de inhoud
content_transfer_encoding = part.get("Content-Transfer-Encoding")
if content_transfer_encoding is not None:
headers.append(f"Transfer-Content-Encoding: {content_transfer_encoding}")
# de 8-bitsmodus leverde problemen op
if content_transfer_encoding == "8bit":
# het bericht uit de e-mail wordt opgehaald
msg = part.get_payload()
else:
# het e-mailbericht wordt opgehaald
msg = email.contentmanager.raw_data_manager.get_content(part)
# afhankelijk van het teksttype
filename = None
if type_of_text == 0:
# kopteksten opslaan
with codecs.open(f"{output}/headers.txt", "a", "utf-8") as file:
# schrijven naar bestand
string = '\r\n'.join(headers)
file.write(f"{string}\r\n")
# tekstbestand voor de inhoud
filename = f"{output}/mail.txt"
elif type_of_text == 1:
# HTML-bestand voor de inhoud
filename = f"{output}/mail.html"
# bericht opslaan
with codecs.open(filename, "w", "utf-8") as file:
# schrijven naar bestand
file.write(msg)
Opmerkingen
- Net als de headers kan ook de tekst van het bericht gecodeerd zijn. Er kunnen twee coderingen zijn:
- de oorspronkelijke codering van de tekst (utf-8, iso-8859-1…). Dit is de codering die wordt gebruikt door het e-mailprogramma dat het bericht heeft verzonden. Deze is bekend via de header [Content-Type] van het ontvangen bericht;
- een tweede codering die de bovenstaande tekst mogelijk heeft ondergaan om te kunnen worden verzonden. Deze is bekend via de header [Transfer-Content-Encoding] van het ontvangen bericht;
- regel 6: de oorspronkelijke codering van de tekst;
- regel 11: de tweede codering die de tekst heeft ondergaan voor de overdracht naar de ontvanger;
- regels 9, 13: deze twee gegevens worden opgenomen in de lijst [headers]. Ze worden toegevoegd aan de gegevens in het bestand [headers.txt], waarin bepaalde headers van het bericht zijn vastgelegd;
- regel 20: met [email.contentmanager.raw_data_manager.get_content] kan het bericht met zijn oorspronkelijke codering 1 worden verkregen. Codering 2 is verwijderd. Alleen het object [email.contentmanager.raw_data_manager] ondersteunt slechts twee soorten [Transfer-Content-Encoding]:
- [quoted-printable] ;
- [base64];
De overige worden genegeerd. Thunderbird gebruikt bijvoorbeeld de codering [Transfer-Content-Encoding], ook wel "8bit" genoemd. Deze codering wordt genegeerd en berichten met accenten worden vervormd. Het bericht kan dan worden verkregen via de methode [part.get_payload()] (regels 15-17);
- regel 21: op dit punt is de codering voor de overdracht uit het bericht verwijderd, dus is dit het bericht zoals het door de afzender is geschreven;
- regels 22-37: we bevinden ons in het geval waarin we een tekstbericht moeten opslaan;
- regels 24-28: de twee kopteksten die in de regels 9 en 13 zijn samengesteld, worden opgeslagen in het bestand [headers.txt]. Dit bestand bestaat al en bevat kopteksten. Daarom gebruiken we de modus "a" (regel 25) om dit bestand te openen. "a" staat voor "append" en de nieuwe kopteksten worden (aan het einde van het bestand) toegevoegd aan de bestaande inhoud van het bestand [headers.txt];
- regel 30: de naam van het bestand waarin het tekstbericht moet worden opgeslagen;
- regel 33: de naam van het bestand waarin het bericht HTML moet worden opgeslagen;
- regels 34-37: de UTF-8-tekst wordt in een bestand opgeslagen;
Laten we teruggaan naar de functie [save_message]:
# opslaan van een bericht van het type email.message.Message
# deze functie kan recursief worden aangeroepen
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
# uitvoer: map waarin berichten worden opgeslagen
# email_message: het op te slaan bericht
# irfc822: lopend nummer in de nummering van de bijgevoegde e-mails
#
# deel van het bericht
part = email_message
# de headers [From, To, Subject] worden aangetroffen in een van de multipart-delen
# of in een deel [text/*] wanneer er geen deel [multipart] is
keys = part.keys()
# "From" moet deel uitmaken van de headers, anders bevat het deel niet de headers die we zoeken
if "From" in keys:
# we halen bepaalde headers op
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'))}"]
# de headers worden opgeslagen in een tekstbestand
with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
# schrijven naar bestand
string = '\r\n'.join(headers)
file.write(f"{string}\r\n")
# type van het bericht [part]
main_type = part.get_content_maintype()
sub_type = part.get_content_subtype()
type_of_part = f"{main_type}/{sub_type}"
# als het bericht van het type text/plain is
if type_of_part == "text/plain":
# tekstbericht
save_textmessage(output, part, 0)
# als het bericht van het type text/html is
elif type_of_part == "text/html":
# bericht HTML
save_textmessage(output, part, 1)
# als het bericht uit meerdere delen bestaat
elif part.is_multipart():
# speciaal geval van een e-mail met bijlage
if type_of_part == "message/rfc822":
# aanmaken van een nieuwe map output2 voor de bijgevoegde e-mail
irfc822 += 1
output2 = f"{output}/rfc822_{irfc822}"
os.mkdir(output2)
# opslaan van de subdelen van het irfc822-bericht in output2
for subpart in part.get_payload():
# in de nieuwe map irfc822 begint opnieuw bij 0
save_message(output2, subpart, 0)
else:
# er is geen sprake van een bijgevoegde e-mail
# onderdelen opslaan in de huidige map output
# irfc822 moet dan voor elk subdeel message/rfc822 worden verhoogd
for subpart in part.get_payload():
# save_message stelt de laatste waarde van irfc822 in
# met 1 verhoogd als subpart="message/rfc822", anders niet verhoogd
irfc822 = save_message(output, subpart, irfc822)
else:
# andere gevallen (niet text/plain, niet text/html, niet multipart)
# bijlage?
disposition = part.get('Content-Disposition')
if disposition and disposition.startswith('attachment'):
save_attachment(output, part)
# de overige delen worden genegeerd (geen text/plain, geen text/html, geen attachment)
# de huidige waarde van irfc822 wordt weergegeven (nummering van bijgevoegde e-mails die in de map output zijn opgeslagen)
return irfc822
Opmerkingen
- regels 33-40: we hebben twee mogelijke gevallen behandeld van een bericht aan het uiteinde van de oorspronkelijke berichtenboom (geen subdelen). Er blijven nog twee gevallen over:
- regels 43-62: het geval waarin het geanalyseerde deel zelf subdelen bevat (part.ismultipart()==True);
- regels 63-68: voor de overige gevallen behandelen we alleen het geval waarin het geanalyseerde deel een bijlage is;
We behandelen dit laatste geval. We bevinden ons hier opnieuw aan het ene uiteinde van het oorspronkelijke bericht (geen subdelen). We zijn al twee gevallen van dit type tegengekomen: de typen text/plain en text/html. We behandelen nu het geval van het bijgevoegde bestand.
- regel 66: de bijlage wordt geïdentificeerd door de sleutel [Content-Disposition];
- regel 67: als deze sleutel bestaat en begint met de tekenreeks [attachment], dan hebben we te maken met een bijlage bij het bericht;
- regel 68: de bijlage wordt opgeslagen in de map [output];
De functie [save_attachment] is als volgt:
# een bijlage opslaan
def save_attachment(output: str, part: email.message.Message):
# naam van het bijgevoegde bestand
filename = os.path.basename(part.get_filename())
# de bestandsnaam kan gecodeerd zijn
# bijvoorbeeld =?utf-8?Q?Cursussen-Tutorials-Serge-Tah=C3=A9-1568x268=2Ep
filename = decode_header(filename)
# het bijgevoegde bestand wordt opgeslagen
with open(f"{output}/{filename}", "wb") as file:
file.write(part.get_payload(decode=True))
- regel 4: als [part] een bijlage is, dan wordt de naam van het bijgevoegde bestand verkregen via [part.get_filename]. Alleen de bestandsnaam wordt bewaard, niet het pad;
- regel 8: bestandsnamen zijn doorgaans gecodeerd, op dezelfde manier als de kopteksten van het bericht. Daarom wordt de functie [decode_header] gebruikt om deze te decoderen;
- regel 11: de inhoud van het bijgevoegde bestand is op dit moment een tekenreeks die is gegencodeerd (vaak base64) uit de oorspronkelijke inhoud van het bestand. Om deze oorspronkelijke inhoud te verkrijgen, gebruiken we de functie [part.get_payload(decode=True)]. De parameter [decode=True] geeft aan dat de inhoud van het bijgevoegde bestand moet worden gedecodeerd. Dit levert een reeks bytes op;
- regel 10: deze reeks bytes wordt opgeslagen in het bestand [output/filename]. De modus „wb“ voor het openen van het bestand staat voor „write binary“;
Laten we teruggaan naar de code van de functie [save_message]:
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
# uitvoer: map waarin berichten worden opgeslagen
# email_message: het bericht dat moet worden opgeslagen
# irfc822: lopend nummer in de nummering van de bijgevoegde e-mails
#
# deel van het bericht
part = email_message
# de headers [From, To, Subject] worden aangetroffen in een van de multipart-delen
# of in een deel [text/*] wanneer er geen deel [multipart] is
keys = part.keys()
# "From" moet deel uitmaken van de headers, anders bevat het deel niet de headers die we zoeken
if "From" in keys:
# we halen bepaalde headers op
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'))}"]
# de headers worden opgeslagen in een tekstbestand
with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
# schrijven naar bestand
string = '\r\n'.join(headers)
file.write(f"{string}\r\n")
# type van het bericht [part]
main_type = part.get_content_maintype()
sub_type = part.get_content_subtype()
type_of_part = f"{main_type}/{sub_type}"
# als het bericht van het type text/plain is
if type_of_part == "text/plain":
# tekstbericht
save_textmessage(output, part, 0)
# als het bericht van het type text/html is
elif type_of_part == "text/html":
# bericht HTML
save_textmessage(output, part, 1)
# als het bericht uit meerdere delen bestaat
elif part.is_multipart():
# speciaal geval van een e-mail met bijlage
if type_of_part == "message/rfc822":
# aanmaken van een nieuwe map output2 voor de bijgevoegde e-mail
irfc822 += 1
output2 = f"{output}/rfc822_{irfc822}"
os.mkdir(output2)
# opslaan van de subdelen van het irfc822-bericht in output2
for subpart in part.get_payload():
# in de nieuwe map irfc822 begint opnieuw bij 0
save_message(output2, subpart, 0)
else:
# er is geen sprake van een bijgevoegde e-mail
# onderdelen opslaan in de huidige map output
# irfc822 moet vervolgens voor elk subonderdeel message/rfc822 worden verhoogd
for subpart in part.get_payload():
# save_message geeft de laatste waarde van irfc822 weer
# met 1 verhoogd als subpart="message/rfc822", anders niet verhoogd
irfc822 = save_message(output, subpart, irfc822)
else:
# andere gevallen (niet text/plain, niet text/html, niet multipart)
# bijlage?
disposition = part.get('Content-Disposition')
if disposition and disposition.startswith('attachment'):
save_attachment(output, part)
# de overige delen worden genegeerd (geen text/plain, geen text/html, geen attachment)
# de huidige waarde van irfc822 wordt weergegeven (nummering van bijgevoegde e-mails die in de map output zijn opgeslagen)
return irfc822
Opmerkingen
- we hebben de gevallen behandeld waarbij de boomstructuur van het oorspronkelijke bericht eindigt: de delen [text/plain, text/html et Content-Disposition=attachment;…]. Nu moeten we nog het geval behandelen waarin het geanalyseerde deel een container van delen is, d.w.z. dat het subdelen bevat, zoals [part.is_multipart()==True], regel 41. Om de eindpunten van de berichtboom te bereiken, moeten deze subdelen dus worden geanalyseerd;
- regel 43: we behandelen op een speciale manier het geval waarin het geanalyseerde onderdeel het type [message/rfc822] heeft. Dit is het type van een e-mail. Dit is dus het geval waarin een e-mail een andere e-mail als bijlage heeft;
De code is als volgt:
# als het bericht uit meerdere delen bestaat
elif part.is_multipart():
# speciaal geval van de bijgevoegde e-mail
if type_of_part == "message/rfc822":
# aanmaken van een nieuwe map output2 voor de bijgevoegde e-mail
irfc822 += 1
output2 = f"{output}/rfc822_{irfc822}"
os.mkdir(output2)
# opslaan van de subdelen van het irfc822-bericht in output2
for subpart in part.get_payload():
# in de nieuwe map irfc822 begint opnieuw bij 0
save_message(output2, subpart, 0)
else:
# er is geen sprake van een bijgevoegde e-mail
# onderdelen opslaan in de huidige map output
# irfc822 moet vervolgens voor elk subonderdeel message/rfc822 worden verhoogd
for subpart in part.get_payload():
# save_message levert de laatste waarde van irfc822 op
# met 1 verhoogd als subpart="message/rfc822", anders niet verhoogd
irfc822 = save_message(output, subpart, irfc822)
…
return irfc822
- het verschil tussen een onderdeel [message/rfc822] en de andere multipart-onderdelen is dat de opslagmap verandert;
- regels 6-8: voor het deel [message/rfc822] wordt de opslagmap die van regel 7, [output/rfc822_x], waarbij x het nummer is van de bijgevoegde e-mail, 1 voor de eerste, 2 voor de tweede…;
- regel 21: voor de andere multipart-delen blijft de opslagmap de map [output] van het oorspronkelijke bericht. Er wordt niet van map gewisseld;
- regels 10-12: elk subdeel wordt opgeslagen via een recursieve aanroep van [save_message]. De derde parameter is de nummerindex van de e-mails die zijn ingekapseld in [subpart]. In het begin is deze index gelijk aan 0;
- regel 21: dezelfde uitleg als voor regel 12, maar de waarde van de derde parameter [irfc822] verandert. Als er in de lus van de regels 18-21 meerdere ingekapselde e-mails zijn, moeten deze worden opgeslagen in mappen met de naam […/rfc822-1…/rfc822_2…]. De derde parameter van de functie [save_message] moet dus achtereenvolgens de waarden 1, 2, 3… aannemen. Hiervoor stelt [save_message] de waarde van [irfc822] in (regel 21).
Laten we een voorbeeld nemen en aannemen dat de lijst met subonderdelen van regel 18 [subpart1, subpart2, subpart3, subpart4, subpart5] is en dat [subpart1, subpart3, subpart5] bijgevoegde e-mails zijn, [subpart2] een text/plain-onderdeel is en [subpart4] een bijlage, en dat we in het bericht [irfc822=0] nog geen e-mail met bijlage zijn tegengekomen. In dat geval:
- (vervolg)
- wordt [subpart1] opgeslagen door regel 21: de functie [saveMessage] wordt uitgevoerd met irfc822=0;
- [subpart1] is een bijgevoegd e-mailbericht, dus irfc822 wordt op 1 gezet (regel 6 van de code). Er wordt een map [output/irfc822_1] aangemaakt. De door [saveMessage(ouput,subpart1,0)] geretourneerde waarde is dus 1 (regel 23);
- [subpart2] wordt opgeslagen via regel 21: de functie [saveMessage] wordt uitgevoerd met irfc822=1;
- [subpart2] is geen bijlage bij een e-mail. Daarom blijft irfc822 op 1 staan. Dit is de waarde die in regel 21 wordt opgehaald;
- [subpart3] wordt opgeslagen via regel 21: de functie [save_message] wordt uitgevoerd met irfc822=1;
- [subpart3] is een e-mail met bijlage, dus irfc822 wordt 2 (regel 6 van de code). Er wordt een map [output/irfc822_2] aangemaakt. De waarde die door [save_message(ouput,subpart1,1)] wordt geretourneerd, is dus 2 (regel 21);
- [subpart4] wordt opgeslagen via regel 21: de functie [save_message] wordt uitgevoerd met irfc822=2;
- [subpart4] is geen bijlage bij een e-mail. Daarom blijft irfc822 op 2 staan. Dit is de waarde die in regel 21 wordt opgehaald;
- [subpart5] wordt opgeslagen via regel 21: de functie [save_message] wordt uitgevoerd met irfc822=2;
- [subpart5] is een e-mail met bijlage, dus irfc822 wordt 3 (regel 6 van de code). Er wordt een map [output/irfc822_3] aangemaakt. De waarde die door [save_message(ouput,subpart1,2)] wordt geretourneerd, is dus 3 (regel 21);
Uitvoeringsvoorbeelden
We sturen 4 e-mails naar [pymail2parlexemple@gmail.com] vanuit: [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 krijgen het onderwerp [hélène va au marché] en als tekst [acheter des légumes]. We willen testen hoe de tekens met accenten worden weergegeven.
We lezen ze met het script [pop3/02/main], geconfigureerd met het volgende bestand [pop3/02/config]:
import os
def configure() -> dict:
# configuratie van de app
config = {
# lijst met te beheren mailboxen
"mailboxes": [
# server: server POP3
# poort: poort van de server POP3
# gebruiker: gebruiker wiens berichten men wil lezen
# wachtwoord: zijn wachtwoord
# maxmails: het maximale aantal e-mails dat moet worden gedownload
# timeout: maximale wachttijd voor een antwoord van de server
# delete: op 'waar' indien de gedownloade berichten van de server moeten worden verwijderd
# ssl: op 'waar' indien de e-mails via een beveiligde verbinding worden gelezen
# output: de map waarin de gedownloade berichten worden opgeslagen
{
"server": "pop.gmail.com",
"port": "995",
"user": "pymail2parlexemple@gmail.com",
"password": "#6prD&@1QZ3TG",
"maxmails": 10,
"delete": False,
"ssl": True,
"timeout": 2.0,
"output": "output"
}
]
}
# absoluut pad naar de map van het script
script_dir = os.path.dirname(os.path.abspath(__file__))
# absolute paden van de mappen die in de syspath moeten worden opgenomen
absolute_dependencies = [
# lokaal bestand
f"{script_dir}/../../shared",
]
# configuratie van de syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# de configuratie wordt doorgegeven
return config
Het resultaat is als volgt:

Bericht 1 is het bericht dat door Thunderbird is verzonden:

- in [5] gebruikt Thunderbird [3] een [Transfer-Content-Encoding] van het type [8bit];
- in [4]: het bericht is gecodeerd in UTF-8;
Bericht 2 is het bericht dat door em Client is verzonden:


Opvallend is dat [em Client] de teksten in UTF-8 [4] codeert en deze vervolgens doorstuurt naar [quoted-printable] [5]. Hij heeft ook een kopie van het bericht verzonden in HTML en [7-8]. Alle hier geteste e-mailprogramma’s kunnen dit doen. Dit is een configuratie-instelling.
Bericht 3 is het bericht dat door Gmail is verzonden:

Opvallend is dat Gmail de teksten codeert in utf-8 [3] en deze doorstuurt in [quoted-printable] [4]. In [6], de versie HTML van het bericht.
Bericht 4 is het bericht dat door Outlook is verzonden:

Opvallend is dat Outlook de teksten codeert in iso-8859-1 [3] en deze omzet naar [quoted-printable] en [4].
De voorgaande voorbeelden laten twee dingen zien:
- onze client [pop3/02] werkte naar behoren;
- e-mailprogramma’s hebben verschillende manieren om een e-mail te versturen;
Laten we nu eens kijken naar de bijlagen. Met Thunderbird legen we de mailbox van de gebruiker [pymail2parlexemple@gmail.com]. Vervolgens gebruiken we het script [smtp/03/main] om een e-mail te versturen met de volgende configuratie [smtp/03/config]:
import os
def configure() -> dict:
# configuratie van de applicatie
script_dir = os.path.dirname(os.path.abspath(__file__))
return {
# beschrijving: beschrijving van de verzonden e-mail
# smtp-server: server SMTP
# smtp-port: poort van de server SMTP
# van: afzender
# to: ontvanger
# onderwerp: onderwerp van de e-mail
# bericht: inhoud van de e-mail
"mails": [
{
"description": "mail to gmail via gmail avec smtplib",
"smtp-server": "smtp.gmail.com",
"smtp-port": "587",
"from": "pymail2parlexemple@gmail.com",
"to": "pymail2parlexemple@gmail.com",
"subject": "to gmail via gmail avec smtplib",
# we testen de tekens met accenten
"message": "aglaë séléné\nva au marché\nacheter des fleurs",
# SMTP met authenticatie
"user": "pymail2parlexemple@gmail.com",
"password": "#6prIlhD&@1QZ3TG",
# hier moeten absolute paden voor de bijgevoegde bestanden worden opgegeven
"attachments": [
f"{script_dir}/attachments/fichier attaché.docx",
f"{script_dir}/attachments/fichier attaché.pdf",
f"{script_dir}/attachments/mail attaché 1.eml",
]
}
]
}
- regels 31-33: we voegen aan de e-mail toe:
- een Word-bestand;
- een PDF-bestand;
- een e-mail met dezelfde twee bijlagen;
Zodra de e-mail is verzonden, voeren we het script [pop3/02] uit om de mailbox van de gebruiker [pymail2parlexemple@gmail.com] te lezen. De resultaten zijn als volgt:

- in [1]: het bericht met de twee bijlagen;
- in [2]: de bijgevoegde e-mail zelf met de twee bijlagen;
Conclusie
De module [mail_parser.py] is bijzonder complex. Dit komt door de complexiteit van de e-mails zelf. We gaan deze module hergebruiken voor het protocol IMAP.
21.7. Het protocol IMAP
21.7.1. Inleiding
Om e-mails te lezen die op een e-mailserver zijn opgeslagen, bestaan er twee protocollen:
- het protocol POP3 (Post Office Protocol), historisch gezien het eerste protocol, maar tegenwoordig weinig gebruikt;
- het protocol IMAP (Internet Message Access Protocol), een recenter protocol dan POP3 en momenteel het meest gebruikte;
Om het protocol IMAP te verkennen, gebruiken we de volgende architectuur:

- [Serveur B] is, afhankelijk van de situatie:
- een lokale IMAP-server, geïmplementeerd door de [hMailServer]-mailserver;
- de server [imap.gmail.com:993], die de server IMAP is van de e-mailmanager [Gmail];
- [Client A] zal een Python-script zijn dat gebruikmaakt van Python-modules voor het beheer van bijlagen en voor het gebruik van een versleutelde en geauthenticeerde verbinding wanneer de server IMAP dit vereist;
Het protocol IMAP gaat verder dan het protocol POP3:
- e-mails worden opgeslagen op de server IMAP en kunnen in mappen worden geordend;
- de client IMAP kan opdrachten verzenden om deze mappen aan te maken, te wijzigen of te verwijderen;
Laten we een voorbeeld bekijken met Thunderbird. In de volgende architectuur:

- is Thunderbird client A;
- [imap.gmail.com] is server B (Gmail);
Laten we met Thunderbird een map aanmaken in de e-mails van gebruiker [pymail2parlexemple@gmail.com]:

- in [1-6] maken we de map [dossier1] aan;

- in [7-8] verplaatsen we (met de muis) alle bestanden uit de map [Courrier entrant] naar de map [dossier1];
Laten we nu inloggen op de Gmail-website en ons aanmelden als de gebruiker [pymail2parlexemple@gmail.com]:

- in [2-3] is de inbox leeg;
- bij [1] is de map [dossier1] aangemaakt;

- in [4-6]: de e-mails die zijn verplaatst naar de map [dossier1];
We hebben hier te maken met de volgende architectuur:

- Client A is de Thunderbird-applicatie;
- Client C is de webapplicatie van Gmail;
- Server B is de Gmail-server IMAP;
De mappenboom van de gebruiker wordt beheerd door de IMAP-server. Vervolgens synchroniseren alle IMAP-clients hiermee om de mappen van het account aan de gebruiker weer te geven. Hier heeft Thunderbird verschillende opdrachten verzonden om:
- de map [dossier1] aan te maken;
- berichten naar deze map over te zetten;
21.7.2. script [imap/main]: client IMAP met de module [imaplib]

Het script [imap/main] wordt geconfigureerd door het volgende script [imap/config]:
import os
def configure() -> dict:
# configuratie van de app
config = {
# lijst met te beheren mailboxen
"mailboxes": [
# server: server IMAP
# poort: poort van de server IMAP
# gebruiker: gebruiker wiens berichten je wilt lezen
# wachtwoord: zijn wachtwoord
# maxmails: het maximale aantal e-mails dat moet worden gedownload
# timeout: maximale wachttijd voor een antwoord van de server
# delete: op 'waar' indien de gedownloade berichten van de server moeten worden verwijderd
# ssl: op 'waar' indien de e-mails via een beveiligde verbinding worden gelezen
# output: de map waarin de gedownloade berichten worden opgeslagen
{
"server": "imap.gmail.com",
"port": "993",
"user": "pymail2parlexemple@gmail.com",
"password": "#6prIlhD&@1QZ3TG",
"maxmails": 10,
"ssl": True,
"timeout": 2.0,
"output": "output"
}
]
}
# absoluut pad naar de map van het script
script_dir = os.path.dirname(os.path.abspath(__file__))
# absolute paden van de mappen die in de syspath moeten worden opgenomen
absolute_dependencies = [
# lokaal bestand
f"{script_dir}/../shared",
]
# configuratie van de syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# de configuratie wordt weergegeven
return config
Opmerkingen
- regels 8-29: de sleutel [mailboxes] is gekoppeld aan de lijst met te raadplegen mailboxen;
- regel 20: de server IMAP;
- regel 21: de servicepoort;
- regels 22-23: de gebruiker wiens e-mails men wil lezen;
- regel 24: het maximale aantal e-mails dat men wil lezen;
- regel 25: geeft aan of er een beveiligde verbinding met de server IMAP moet worden opgezet (True) of niet (False);
- regel 26: de maximale wachttijd voor een antwoord van de server;
- regel 27: map waarin de gelezen e-mails worden opgeslagen;
Het script [imap/main] is als volgt:
# imports
import email
import imaplib
import os
import shutil
# -----------------------------------------------------------------------
def readmails(mailbox: dict):
…
# main ----------------------------------------------------------------
# IMAP-client waarmee e-mails kunnen worden gelezen
# de configuratie van de applicatie wordt opgehaald
import config
config = config.configure()
# de mailboxen worden één voor één verwerkt
for mailbox in config['mailboxes']:
try:
# consoleweergave
print("----------------------------------")
print(
f"Lecture de la boîte mail POP3 {mailbox['user']} / {mailbox['server']}:{mailbox['port']}")
# de mailbox wordt gelezen
readmails(mailbox)
# einde
print("Lecture terminée...")
# behalve BaseException als fout:
# # de fout wordt weergegeven
# print(f"De volgende fout is opgetreden: {fout}")
finally:
pass
Opmerkingen
- regels 14-36: hier zien we dezelfde aanpak terug als in het script |pop3/02/main|;
De functie [readmails] is als volgt:
def readmails(mailbox: dict):
# we laten de uitzonderingen doorgaan
#
# module voor het parseren van e-mail
from mail_parser import save_message
# configuratiegegevens ophalen
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']
#
# daar gaan we
imap_resource = None
try:
# de opslagmappen worden aangemaakt als ze nog niet bestaan
if not os.path.isdir(output):
os.mkdir(output)
# gebruiker
dir2 = f"{output}/{user}"
# de map [dir2] wordt verwijderd als deze bestaat, waarna deze opnieuw wordt aangemaakt
if os.path.isdir(dir2):
# verwijderen
shutil.rmtree(dir2)
# aanmaken
os.mkdir(dir2)
# verbinding maken met de server IMAP
if ssl:
imap_resource = imaplib.IMAP4_SSL(server, port)
else:
imap_resource = imaplib.IMAP4(server, port)
# time-out van de communicatie met de client
sock = imap_resource.socket()
sock.settimeout(timeout)
# authenticatie
imap_resource.login(user, password)
# de map INBOX (inkomende e-mail) wordt geselecteerd
imap_resource.select('INBOX')
# alle berichten uit deze map worden opgehaald: criterium ALL
# geen specifieke codering: None
typ1, data1 = imap_resource.search(None, 'ALL')
# print(f"typ={typ1}, data={data1}")
# data1[0] is een byte-array met de nummers van alle berichten, gescheiden door een spatie
nums = data1[0].split()
imail = 0
fini = imail >= maxmails or imail >= len(nums)
# de e-mails worden één voor één gelezen
while not fini:
# num is een berichtnummer in binaire vorm
num = nums[imail]
# print(f"bericht nr. {num}")
# we halen bericht nr. num op
typ2, data2 = imap_resource.fetch(num, '(RFC822)')
# print(f"type={typ2}, data={data2}")
# data is een lijst die tuples bevat, in dit geval slechts één
# data[0] is het tuple, dataQZXW2HTMLBWzBdZQXQZXW2HTMLBWzFdZQX is het tweede element van het tuple
# dataQZXW2HTMLBWzBdZQXQZXW2HTMLBWzFdZQX bevat een reeks bytes die alle regels van het bericht vertegenwoordigen
# onder ‘bericht’ wordt verstaan: de tekst van het bericht plus alle bijgevoegde bestanden
# het bericht wordt opgehaald als type email.message.Message
message = email.message_from_bytes(data2[0][1])
# map van het bericht
dir3 = f"{dir2}/message_{int(num)}"
# als de map niet bestaat, wordt deze aangemaakt
if not os.path.isdir(dir3):
os.mkdir(dir3)
# het bericht wordt opgeslagen
save_message(dir3, message)
# volgend bericht
imail += 1
fini = imail >= maxmails or imail >= len(nums)
finally:
if imap_resource:
# de verbinding met de mailbox wordt verbroken
imap_resource.close()
# we melden ons af bij de server IMAP
imap_resource.logout()
Opmerkingen
- regels 7-15: de configuratie-elementen worden opgehaald;
- regels 19, 79: de code wordt gecontroleerd door een try/finally-blok. Uitzonderingen worden dus niet opgevangen (er is geen except-clausule), waardoor ze worden doorgegeven aan de aanroepende code, die ze afbreekt en weergeeft;
- regels 23-30: de map voor het opslaan van e-mails wordt aangemaakt;
- regels 31-35: er wordt verbinding gemaakt met de server IMAP. De gebruikte klasse verschilt naargelang het gaat om een beveiligde IMAP-server (IMAP4_SSL) of een onbeveiligde (IMAP4);
- regels 36-38: de time-out voor de communicatie tussen client en server wordt ingesteld;
- regels 39-40: men authenticeert zich bij de server IMAP;
- regels 41-42: we hebben gezien dat de mailbox van een gebruiker IMAP in mappen kan worden ingedeeld. De map [INBOX] is de map voor inkomende e-mail. Om de map [dossier1] te selecteren, zou men [imapResource.select('dossier1')] moeten invoeren;
- regels 43-45: we vragen de lijst op van alle berichten die in [INBOX] zijn gevonden:
- de eerste parameter van [imapResource.search] is een coderingstype. [None] betekent „geen coderingsfilter“;
- De tweede parameter is een criterium. Er zijn verschillende manieren om dit criterium uit te drukken. Het criterium [ALL] betekent dat alle berichten in de map worden opgehaald;
Het resultaat van [imapResource.search] ziet er als volgt uit:
typ=OK, data=[b'1 2']
[data] is een lijst met de nummers van de gevonden berichten. Deze zijn in binaire vorm. Hierboven zijn twee berichten gevonden in de map [INBOX];
- regel 49: we halen de nummers van de berichten op. Hierboven krijgen we de lijst [b'1' b'2'], een lijst met nummers die in binair zijn gecodeerd;
- regels 53-78: we doorlopen de lijst om de berichten uit de map [INBOX] te lezen;
- regels 54-55: berichtnummer;
- regels 58-59: bericht nr. [num] wordt opgevraagd bij de server IMAP;
- de eerste parameter is het nummer van het gewenste bericht;
- de tweede parameter is een tekenreeks "(deel1)(deel2)…" waarbij [parti] de naam is van een deel van het bericht. Ik heb dit punt niet verder onderzocht. De naam (RFC822) verwijst naar de volledige e-mail;
Je ontvangt iets in de volgende vorm:
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')']
Het element [data] is hier een lijst met één element en dit enige element is een tuple van drie 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')'
]
Het tweede element van dit tupel is een binaire string die het volledige opgevraagde bericht vertegenwoordigt. Hierboven herkennen we elementen die al aan bod zijn gekomen bij de bespreking van de module [mail_parser].
data[0] vertegenwoordigt een tuple met twee elementen. data[0][1] vertegenwoordigt de regels van het bericht in binaire vorm.
- regel 68: de functie [email.message_from_bytes(data2[0][1])] construeert een object van het type [email.message.Message] op basis van de regels van het bericht. Het type [email.message.Message] is het type van de parameter van de module [mail_parser] die we eerder hebben geschreven;
- regels 69-73: we maken de opslagmap aan voor bericht nr. [num];
- regel 75: we roepen de functie [save_message] aan uit de module [mail_parser] van regel 5. Deze functie is beschreven in paragraaf |pop3/02/main|;
- regels 76-78: we herhalen de cyclus om het volgende bericht te verwerken;
- regels 79-84: of er nu een fout is opgetreden of niet:
- regel 82: de verbinding met de opgevraagde map wordt verbroken;
- regel 84: er wordt verbinding verbroken met de server IMAP;
De verkregen resultaten zijn identiek aan die van het script [pop3/02/main]. Dit is normaal, aangezien dezelfde e-mailparser [mail_parser] wordt gebruikt.