Skip to content

21. Funkcje internetowe

Przechodzimy teraz do funkcji internetowych języka Python, które umożliwiają nam programowanie w protokołach TCP / IP (Transfer Control Protocol / Internet Protocol).

Image

21.1. Podstawy programowania internetowego

21.1.1. Informacje ogólne

Rozważmy komunikację między dwoma zdalnymi komputerami A i B:

Image

Gdy aplikacja AppA na komputerze A chce komunikować się z aplikacją AppB na komputerze B w Internecie, musi znać kilka rzeczy:

  • adres IP (protokół internetowy) lub nazwa komputera B;
  • numer portu, z którym współpracuje aplikacja AppB. Komputer B może bowiem obsługiwać wiele aplikacji działających w Internecie. Gdy otrzymuje informacje z sieci, musi wiedzieć, do której aplikacji są one przeznaczone. Aplikacje na komputerze B mają dostęp do sieci poprzez „okienka”, zwane również portami komunikacyjnymi. Informacja ta jest zawarta w pakiecie odebranym przez komputer B, aby mógł on zostać dostarczony do właściwej aplikacji;
  • protokoły komunikacyjne rozumiane przez komputer B. W naszym opracowaniu będziemy korzystać wyłącznie z protokołów TCP-IP;
  • protokół dialogowy akceptowany przez aplikację AppB. Komputery A i B będą bowiem „komunikować się” ze sobą. Treść tej komunikacji zostanie zakodowana w protokołach TCP-IP. Niemniej jednak, gdy na końcu łańcucha aplikacja AppB odbierze informacje wysłane przez aplikację AppA, musi być w stanie je zinterpretować. Sytuacja ta przypomina przypadek, w którym dwie osoby, A i B, komunikują się przez telefon: ich rozmowa jest przekazywana przez telefon. Mowa zostanie zakodowana w postaci sygnałów przez telefon A, przetransportowana liniami telefonicznymi, dotrze do telefonu B, gdzie zostanie zdekodowana. Osoba B słyszy wówczas słowa. W tym miejscu pojawia się pojęcie protokołu dialogowego: jeśli A mówi po francusku, a B nie rozumie tego języka, A i B nie będą w stanie prowadzić sensownej rozmowy;

Dlatego obie komunikujące się aplikacje muszą uzgodnić, jaki rodzaj dialogu przyjmą. Na przykład dialog z usługą ftp nie przebiega tak samo jak z usługą pop: te dwie usługi nie akceptują tych samych poleceń. Mają one inny protokół dialogowy;

21.1.2. Charakterystyka protokołu TCP

W niniejszym opracowaniu zajmiemy się wyłącznie komunikacją sieciową wykorzystującą protokół transportowy TCP, którego główne cechy przedstawiono poniżej:

  • proces, który chce wysłać dane, najpierw nawiązuje połączenie z procesem, do którego mają trafić te informacje. Połączenie to odbywa się między portem komputera wysyłającego a portem komputera odbierającego. Pomiędzy tymi dwoma portami powstaje w ten sposób wirtualna ścieżka, która będzie zarezerwowana wyłącznie dla tych dwóch procesów, które nawiązały połączenie;
  • wszystkie pakiety wysyłane przez proces źródłowy przechodzą tą wirtualną ścieżką i docierają w kolejności, w jakiej zostały wysłane;
  • przesyłane informacje mają charakter ciągły. Proces wysyłający przesyła informacje we własnym tempie. Nie muszą one być wysyłane natychmiast: protokół TCP czeka, aż zgromadzi ich wystarczającą ilość, aby je wysłać. Są one przechowywane w strukturze zwanej segmentem TCP. Segment ten, po zapełnieniu, zostanie przekazany do warstwy IP, gdzie zostanie zamknięty w pakiecie IP;
  • każdy segment wysyłany przez protokół TCP jest ponumerowany. Protokół odbiorczy TCP sprawdza, czy segmenty są odbierane w odpowiedniej kolejności. Za każdy poprawnie odebrany segment wysyła potwierdzenie odbioru do nadawcy;
  • gdy nadawca je otrzyma, informuje o tym proces wysyłający. Dzięki temu proces ten może stwierdzić, że segment dotarł do miejsca przeznaczenia;
  • jeśli po upływie pewnego czasu protokół TCP, który wysłał segment, nie otrzyma potwierdzenia odbioru, ponownie wysyła dany segment, gwarantując w ten sposób jakość usługi przekazywania informacji;
  • wirtualny obwód ustanowiony między dwoma komunikującymi się procesami to full-duplex: oznacza to, że informacje mogą przepływać w obu kierunkach. W ten sposób proces docelowy może wysyłać potwierdzenia odbioru, nawet gdy proces źródłowy nadal wysyła informacje. Pozwala to na przykład protokołowi źródłowemu TCP na wysyłanie wielu segmentów bez oczekiwania na potwierdzenie odbioru. Jeśli po upływie pewnego czasu stwierdzi, że nie otrzymał potwierdzenia odbioru określonego segmentu o numerze n, wznowi wysyłanie segmentów od tego punktu;

21.1.3. Relacja klient-serwer

Komunikacja w Internecie jest często asymetryczna: komputer A inicjuje połączenie, aby zażądać usługi od komputera B: określa, że chce nawiązać połączenie z usługą SB1 na komputerze B. Ten ostatni akceptuje lub odrzuca żądanie. Jeśli zgodzi się, maszyna A może wysyłać swoje żądania do usługi SB1. Muszą one być zgodne z protokołem komunikacyjnym rozumianym przez usługę SB1. W ten sposób nawiązywana jest wymiana żądania i odpowiedzi między komputerem A, zwanym komputerem klienckim, a komputerem B, zwanym komputerem serwerowym. Jeden z partnerów zamknie połączenie.

21.1.4. Architektura klienta

Architektura programu sieciowego korzystającego z usług aplikacji serwerowej będzie wyglądać następująco:

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

21.1.5. Architektura serwera

Architektura programu oferującego usługi będzie wyglądać następująco:

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

Program serwerowy inaczej traktuje początkowe żądanie połączenia od klienta niż jego kolejne żądania mające na celu uzyskanie usługi. Program nie świadczy usługi samodzielnie. Gdyby tak było, w trakcie świadczenia usługi nie nasłuchiwałby już żądań połączenia, a klienci nie byliby obsługiwani. Postępuje inaczej: gdy tylko żądanie połączenia zostanie odebrane na porcie nasłuchowym, a następnie zaakceptowane, serwer tworzy zadanie odpowiedzialne za realizację usługi żądanej przez klienta. Usługa ta jest realizowana na innym porcie serwera, zwanym portem usługowym. W ten sposób można obsługiwać wielu klientów jednocześnie.

Zadanie obsługi będzie miało następującą strukturę:

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

21.2. Poznaj protokoły komunikacyjne w Internecie

21.2.1. Wprowadzenie

Gdy klient nawiąże połączenie z serwerem, między nimi rozpoczyna się komunikacja. Charakter tej komunikacji określa się mianem protokołu komunikacyjnego serwera. Do najpopularniejszych protokołów internetowych należą:

  • HTTP: HyperText Transfer Protocol – protokół komunikacji z serwerem WWW (serwer HTTP);
  • SMTP: Simple Mail Transfer Protocol – protokół komunikacji z serwerem wysyłającym wiadomości e-mail (serwer SMTP);
  • POP: Post Office Protocol – protokół komunikacji z serwerem przechowującym wiadomości e-mail (serwer POP). Służy on do pobierania otrzymanych wiadomości e-mail, a nie do ich wysyłania;
  • IMAP: Internet Message Access Protocol – protokół komunikacji z serwerem przechowującym wiadomości e-mail (serwer IMAP). Protokół ten stopniowo zastąpił starszy protokół POP;
  • FTP: File Transfer Protocol – protokół komunikacji z serwerem przechowującym pliki (serwer FTP);

Wszystkie te protokoły charakteryzują się tym, że są protokołami opartymi na wierszach tekstowych: klient i serwer wymieniają między sobą wiersze tekstowe. Jeśli dysponujemy klientem zdolnym do:

  • nawiązać połączenie z serwerem TCP;
  • wyświetlać na konsoli wiersze tekstowe przesyłane przez serwer;
  • wysyłać do serwera wiersze tekstowe wpisywane przez użytkownika na klawiaturze;

to jesteśmy w stanie komunikować się z serwerem TCP wykorzystującym protokół oparty na wierszach tekstowych, o ile znamy zasady działania tego protokołu.

21.2.2. Narzędzia TCP

Image

W kodach związanych z tym dokumentem znajdują się dwa narzędzia komunikacyjne TCP:

  • [RawTcpClient] umożliwia połączenie się z portem P serwera S;
  • [RawTcpServer] umożliwia utworzenie serwera, który oczekuje na klientów na porcie P;

Są to dwa programy napisane w języku C#, których kody źródłowe zostały udostępnione. Można je zatem modyfikować.

Serwer TCP [RawTcpServer]wywoływa się za pomocą składni [RawTcpServeur port] w celu utworzenia usługi TCP na porcie [port] na komputerze lokalnym (komputerze, na którym pracujesz):

  • serwer może obsługiwać wielu klientów jednocześnie;
  • serwer wykonuje polecenia wpisywane przez użytkownika za pomocą klawiatury. Są to następujące polecenia:
    • list: wyświetla listę klientów aktualnie podłączonych do serwera. Są oni wyświetlani w formacie [id=x-nom=y]. Pole [id] służy do identyfikacji klientów;
    • send x [texte]: wysyła tekst do klienta nr x (id=x). Nawiasy kwadratowe [] nie są wysyłane. Są one niezbędne w poleceniu. Służą do wizualnego oddzielenia tekstu wysyłanego do klienta;
    • close x: zamyka połączenie z klientem nr x;
    • quit: zamyka wszystkie połączenia i zatrzymuje działanie usługi;
  • wiersze wysyłane przez klienta do serwera są wyświetlane na konsoli;
  • cała wymiana danych jest rejestrowana w pliku tekstowym o nazwie [machine-port.txt], gdzie
    • [machine] to nazwa komputera, na którym uruchomiony jest kod;
    • [port] to port usługi, który odpowiada na żądania klienta;

Klient TCP [RawTcpClient] wywołuje się za pomocą składni [RawTcpClient serveur port], aby połączyć się z portem [port] serwera [serveur]:

  • wiersze wpisane przez użytkownika na klawiaturze są wysyłane do serwera;
  • wiersze wysyłane przez serwer są wyświetlane na konsoli;
  • cała komunikacja jest rejestrowana w pliku tekstowym o nazwie [serveur-port.txt];

Spójrzmy na przykład. Otwieramy dwa okna terminala o nazwach PyCharm i w każdym z nich przechodzimy do folderu narzędzi:

Image

W jednym z okien uruchamiamy serwer [RawTcpServer] na porcie 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 :
  • w wierszu 1 znajdujemy się w folderze narzędzi;
  • w wierszu 1 uruchamiamy serwer TCP na porcie 100;
  • w wierszach 2–4 serwer oczekuje na klienta TCP i wyświetla listę poleceń, które użytkownik może wprowadzić za pomocą klawiatury;
  • w wierszu 5 serwer oczekuje na polecenie wpisane przez użytkownika za pomocą klawiatury;

W drugim oknie poleceń uruchamiamy klienta 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) :
  • w wierszu 1 znajdujemy się w folderze narzędzi;
  • w wierszu 1 uruchamiamy klienta TCP: nakazujemy mu połączyć się z portem 100 na komputerze lokalnym (tym, na którym działa kod [RawTcpClient]);
  • w wierszu 2 klient pomyślnie połączył się z serwerem. Podajemy dane klienta: znajduje się on na komputerze [DESKTOP-30FF5FB] (w tym przykładzie jest to komputer lokalny) i używa portu [51173] do komunikacji z serwerem:
  • wiersz 3: klient oczekuje na polecenie wpisane przez użytkownika za pomocą klawiatury;

Wróćmy do okna serwera. Jego zawartość uległa zmianie:


(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...
  • w wierszu 5 wykryto klienta. Serwer przydzielił mu numer 1. Serwer poprawnie zidentyfikował klienta zdalnego (komputer i port);
  • wiersz 6: serwer ponownie oczekuje na nowego klienta;

Wróćmy do okna klienta i wyślijmy polecenie do serwera:


(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
  • wiersz 4, polecenie wysłane do serwera;

Wróćmy do okna serwera. Jego zawartość uległa zmianie:


(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]
  • wiersz 7, w nawiasach kwadratowych – komunikat odebrany przez serwer;

Wyślijmy odpowiedź do klienta:


(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 :
  • wiersz 8, odpowiedź wysłana do klienta 1. Wysyłany jest tylko tekst w nawiasach kwadratowych, a nie same nawiasy;

Wróćmy do okna klienta:


(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]
  • wiersz 5, odpowiedź otrzymana przez klienta. Otrzymany tekst to ten znajdujący się w nawiasach kwadratowych;

Wróćmy do okna serwera, aby zobaczyć inne polecenia:


(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
  • wiersz 9, żądamy listy klientów;
  • wiersz 10, odpowiedź;
  • wiersz 11, zamykamy połączenie z klientem nr 1;
  • wiersz 12 – potwierdzenie serwera;
  • wiersz 13: wyłączamy serwer;
  • wiersz 14 – potwierdzenie serwera;

Wróćmy do okna klienta:


(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...
  • wiersz 6, klient wykrył zakończenie usługi;

Utworzono dwa pliki logów, jeden dla serwera, drugi dla klienta:

Image

  • w pliku [1] znajdują się logi serwera: nazwa pliku to nazwa klienta w formacie [machine-port]. Dzięki temu dla różnych klientów powstają różne pliki logów;
  • w pliku [2] znajdują się logi klienta: nazwa pliku to nazwa serwera w formacie [machine-port];

Dzienniki serwera mają następujący format:


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

Logi klienta to:


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

21.3. Uzyskanie nazwy lub adresu IP komputera w Internecie

Image

Komputery w Internecie są identyfikowane za pomocą adresu IP (IPv4 lub IPv6), a najczęściej za pomocą nazwy. Ostatecznie jednak protokoły komunikacyjne w Internecie wykorzystują wyłącznie adres IP. Należy zatem poznać adres IP urządzenia identyfikowanego za pomocą nazwy.

Skrypt [ip-01.py] wygląda następująco:


# importy
import socket


# ------------------------------------------------
def get_ip_and_name(nom_machine: str):
    # nom_machine: nazwa komputera, którego adres chcemy uzyskać IP
    try:
        # nom_machine-->adres IP
        ip = socket.gethostbyname(nom_machine)
        print(f"ip[{nom_machine}]={ip}")
    except socket.error as erreur:
        # wyświetla się błąd
        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:
        # wyświetlany jest błąd
        print(f"names[{ip}]={erreur}")
        return


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

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

# adresy IP maszyn HOTES
for host in hosts:
    print("-------------------------------------")
    get_ip_and_name(host)
# koniec
print("Terminé...")

Komentarze

  • wiersz 2: moduł [socket] udostępnia funkcje niezbędne do obsługi gniazd internetowych. [socket] oznacza gniazdko elektryczne, gniazdko sieciowe;
  • wiersz 6: funkcja [get_ip_and_name] pozwala na podstawie nazwy internetowej komputera uzyskać:
    • adres IP tego komputera;
    • nazwę urządzenia uzyskaną na podstawie poprzedniego adresu IP;
  • wiersz 10: funkcja [socket.gethostbyname] pozwala uzyskać adres IP komputera na podstawie jednej z jego nazw (komputer podłączony do Internetu może mieć nazwę główną i aliasy);
  • wiersz 12: funkcje dotyczące gniazd (socketów) generują wyjątek [socket.error] w momencie wystąpienia błędu;
  • wiersz 19: funkcja [socket.gethostbyaddr] pozwala uzyskać nazwę komputera na podstawie jego adresu IP. Zobaczymy, że można uzyskać nazwę inną niż ta przekazana w wierszu 6;
  • wiersz 30: lista nazw maszyn. Ostatnia nazwa jest błędna. Nazwa [localhost] odnosi się do maszyny, na której pracujesz i która wykonuje skrypt;
  • wiersze 33–35: wyświetlane są nazwy IP tych maszyn;

Wyniki:


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. Protokół HTTP (HyperText Transfer Protocol)

21.4.1. Przykład 1

Image

Gdy przeglądarka wyświetla plik URL, pełni ona rolę klienta serwera internetowego, czyli serwera HTTP. To przeglądarka podejmuje inicjatywę i jako pierwsza wysyła do serwera pewną liczbę poleceń. W tym pierwszym przykładzie:

  • serwerem będzie narzędzie [RawTcpServer];
  • klientem będzie przeglądarka;

Najpierw uruchamiamy serwer na porcie 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 :

Następnie za pomocą przeglądarki wysyłamy żądanie do serwera URL [http://localhost:100], co oznacza, że serwer HTTP, do którego kierujemy zapytanie, działa na porcie 100 na komputerze lokalnym:

Image

Wróćmy do okna serwera:


(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...
  • wiersz 5 – klient, który nawiązał połączenie;
  • wiersze 9–22: seria wierszy tekstu, które wysłał:
    • wiersz 9: ten wiersz ma format [GET URL HTTP/1.1]. Zawiera on żądanie dotyczące URL / oraz prośbę do serwera o użycie protokołu HTTP 1.1;
    • wiersz 10: ten wiersz ma format [Host: serveur:port]. Wielkość liter w poleceniu [Host] nie ma znaczenia. Przypominamy, że klient łączy się z lokalnym serwerem działającym na porcie 100;
    • wiersz 14: polecenie [User-Agent] podaje tożsamość klienta;
    • wiersz 15: polecenie [Accept] wskazuje, jakie typy dokumentów są akceptowane przez klienta;
    • wiersz 21: polecenie [Accept-Language] określa, w jakim języku mają być dostarczone żądane dokumenty, jeśli są one dostępne w wielu językach;
    • wiersz 11: polecenie [Connection] określa preferowany tryb połączenia: [keep-alive] oznacza, że połączenie musi być utrzymane do momentu zakończenia wymiany danych;
    • wiersz 22: klient kończy swoje polecenia pustym wierszem;

Zamyka się połączenie poprzez zamknięcie serwera:


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

21.4.2. Przykład 2

Teraz, gdy znamy polecenia wysyłane przez przeglądarkę w celu uzyskania URL, poprosimy o ten URL za pomocą naszego klienta TCP [RawTcpClient]. Serwer Apache w Laragonie (akapit |Installation de Laragon|) będzie naszym serwerem WWW.

Uruchommy Laragon, a następnie serwer WWW Apache:

Image

Image

Teraz za pomocą przeglądarki wywołajmy adresy URL i [http://localhost:80]. W tym przypadku podajemy jedynie adres serwera [localhost:80], a nie adres dokumentu URL. W takiej sytuacji żądany jest adres URL, czyli katalog główny serwera WWW:

Image

  • na [1], czyli żądany plik URL. Początkowo wpisano [http://localhost:80], a przeglądarka (w tym przypadku Firefox) przekształcił ją po prostu na [localhost], ponieważ protokół [http] jest domyślny, gdy nie podano żadnego protokołu, a port [80] jest domyślny, gdy nie określono portu;
  • w [2], strona główna / zapytanego serwera WWW;

Teraz przyjrzyjmy się tekstowi otrzymanemu przez przeglądarkę:

Image

  • Klikamy prawym przyciskiem myszy na otrzymaną stronę i wybieramy opcję [2]. Otrzymujemy następujący kod źródłowy:

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

Teraz wywołajmy URL i [http://localhost:80] za pomocą naszego klienta 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) :
  • W wierszu 1 łączymy się z portem 80 serwera localhost. To właśnie tam działa serwer WWW Laragon;

Teraz wpisujemy polecenia, które znaleźliśmy w poprzednim akapicie:


(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...
  • w wierszu 4 polecenie [GET]. Żądamy dostępu do katalogu głównego / serwera WWW;
  • wiersz 5, polecenie [Host];
  • są to jedyne dwa niezbędne polecenia. W przypadku pozostałych poleceń serwer internetowy przyjmie wartości domyślne;
  • wiersz 6 – pusty wiersz, który musi kończyć polecenia klienta;
  • poniżej wiersza 6 znajduje się odpowiedź serwera WWW;
  • wiersze 7–12: nagłówki HTTP odpowiedzi serwera;
  • wiersz 13: pusty wiersz sygnalizujący koniec nagłówków HTTP;
  • wiersze 14–82: dokument HTML, o który poproszono w wierszu 4;

Ładujemy plik logów o nazwie [localhost-80.txt]:

Image


--> [GET / HTTP/1.1]
--> [Host: localhost:80]
--> []
<-- [HTTP/1.1 200 OK]
<-- [Date: Sun, 05 Jul 2020 12:42:14 GMT]
<-- [Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19]
<-- [X-Powered-By: PHP/7.2.19]
<-- [Content-Length: 1776]
<-- [Content-Type: text/html; charset=UTF-8]
<-- []
<-- [<!DOCTYPE html>]
<-- [<html>]
<-- [    <head>]
<-- [        <title>Laragon</title>]
<-- []
<-- [        <link href="https://fonts.googleapis.com/css?family=Karla:400" rel="stylesheet" type="text/css">]
<-- []
<-- [        <style>]
<-- [            html, body {]
<-- [                height: 100%;]
<-- [            }]
<-- []
<-- [            body {]
<-- [                margin: 0;]
<-- [                padding: 0;]
<-- [                width: 100%;]
<-- [                display: table;]
<-- [                font-weight: 100;]
<-- [                font-family: 'Karla';]
<-- [            }]
<-- []
<-- [            .container {]
<-- [                text-align: center;]
<-- [                display: table-cell;]
<-- [                vertical-align: middle;]
<-- [            }]
<-- []
<-- [            .content {]
<-- [                text-align: center;]
<-- [                display: inline-block;]
<-- [            }]
<-- []
<-- [            .title {]
<-- [                font-size: 96px;]
<-- [            }]
<-- []
<-- [            .opt {]
<-- [                margin-top: 30px;]
<-- [            }]
<-- []
<-- [            .opt a {]
<-- [              text-decoration: none;]
<-- [              font-size: 150%;]
<-- [            }]
<-- [            ]
<-- [            a:hover {]
<-- [              color: red;]
<-- [            }]
<-- [        </style>]
<-- [    </head>]
<-- [    <body>]
<-- [        <div class="container">]
<-- [            <div class="content">]
<-- [                <div class="title" title="Laragon">Laragon</div>]
<-- [     ]
<-- [                <div class="info"><br />]
<-- [                      Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19<br />]
<-- [                      PHP version: 7.2.19   <span><a title="phpinfo()" href="/?q=info">info</a></span><br />]
<-- [                      Document Root: C:/MyPrograms/laragon/www<br />]
<-- []
<-- [                </div>]
<-- [                <div class="opt">]
<-- [                  <div><a title="Getting Started" href="https://laragon.org/docs">Getting Started</a></div>]
<-- [                </div>]
<-- [            </div>]
<-- []
<-- [        </div>]
<-- [    </body>]
<-- [</html>]
  • wiersze 11–79: otrzymano dokument HTML. W poprzednim przykładzie Firefox otrzymał ten sam dokument;

Mamy teraz podstawy do zaprogramowania klienta TCP, który żądałby pliku URL.

21.4.3. Przykład 3

Image

Skrypt [http/01/main.py] jest klientem HTTP skonfigurowanym przez plik [config.py]. Jego zawartość jest następująca:


def configure():
    # URLs do sprawdzenia
    urls = [
        # strona: nazwa strony, z którą należy się połączyć
        # port: port serwisu internetowego
        # GET: żądana wartość URL
        # nagłówki: nagłówki HTTP do wysłania w żądaniu
        # endOfLine: znak końca linii w wysyłanych nagłówkach HTTP
        # kodowanie: kodowanie odpowiedzi serwera
        # timeout: maksymalny czas oczekiwania na odpowiedź serwera
        {
            "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
        }
    ]
    # przekazujemy konfigurację
    return {
        "urls": urls
    }
  • Zawartość pliku stanowi lista elementów o nazwie URL, przy czym każdy element tej listy jest słownikiem. Słownik ten określa sposób nawiązania połączenia z witryną wskazaną przez klucz [site];
  • wiersze 4–10: znaczenie kluczy w każdym słowniku;

Skrypt [http/01/main.py] ma następującą postać:


# importy
import codecs
import socket


# -----------------------------------------------------------------------
def get_url(url: dict, suivi: bool = True):
    # odczytuje adres URL URL strony url["GET"] i zapisuje go w pliku url[site].html
    # komunikacja klient–serwer odbywa się zgodnie z protokołem HTTP określonym w słowniku [url]
    # pozwala się na przekazywanie wyjątków

    sock = None
    html = None
    try:
        # połączenie z [site] na porcie 80 z limitem czasu
        site = url['site']
        sock = socket.create_connection((site, int(url['port'])), float(url['timeout']))

        # połączenie stanowi dwukierunkowy strumień komunikacji
        # między klientem (tym programem) a serwerem WWW, z którym nawiązano połączenie
        # kanał ten służy do wymiany poleceń i informacji
        # protokołem komunikacji jest HTTP

        # utworzenie pliku site.html – zastąpiono niepożądane znaki nazwą pliku
        site2 = site.replace("/", "_")
        site2 = site2.replace(".", "_")
        html_filename = f'{site2}.html'
        html = codecs.open(f"output/{html_filename}", "w", "utf-8")

        # klient rozpocznie komunikację HTTP z serwerem
        if suivi:
            print(f"Client : début de la communication avec le serveur [{site}]")

        # w zależności od serwera, linie klienta muszą kończyć się znakiem \n lub \r\n
        end_of_line = url["endOfLine"]
        # klient wysyła polecenie GET, aby zażądać konfiguracji URL ["GET"]
        # składnia GET URL HTTP/1.1
        commande = f"GET {url['GET']} HTTP/1.1{end_of_line}"
        # śledzenie?
        if suivi:
            print(f"--> {commande}", end='')
        # wysyłamy polecenie do serwera
        sock.send(bytearray(commande, 'utf-8'))
        # wysyłanie nagłówków HTTP
        for verb, value in url['headers'].items():
            # tworzenie polecenia do wysłania
            commande = f"{verb}: {value}{end_of_line}"
            # kontynuacja?
            if suivi:
                print(f"--> {commande}", end='')
            # wysyłamy polecenie do serwera
            sock.send(bytearray(commande, 'utf-8'))
        # wysyłamy nagłówek HTTP [Connection: close], aby poprosić serwer WWW
        # o zamknięcie połączenia po wysłaniu żądanego dokumentu
        sock.send(bytearray(f"Connection: close{end_of_line}", 'utf-8'))
        # nagłówki (headers) protokołu HTTP muszą kończyć się pustym wierszem
        sock.send(bytearray(end_of_line, 'utf-8'))
        #
        # serwer odpowie teraz na kanale sock. Wyśle wszystkie
        # swoje dane, a następnie zamknie kanał. Klient odczytuje zatem wszystko, co przychodzi z sock
        # aż do zamknięcia kanału
        #
        # najpierw odczytuje się nagłówki HTTP wysłane przez serwer
        # one również kończą się pustym wierszem
        if suivi:
            print(f"Réponse du serveur [{site}]")

        # odczyt gniazda tak, jakby było plikiem tekstowym
        encoding = f"{url['encoding']}" if url['encoding'] else None
        if encoding:
            file = sock.makefile(encoding=encoding)
        else:
            file = sock.makefile()
        # przetwarzamy ten plik wiersz po wierszu
        fini = False
        while not fini:
            # odczyt bieżącego wiersza
            ligne = file.readline().strip()
            # czy mamy wiersz niepusty?
            if ligne:
                if suivi:
                    # wyświetlanie nagłówka HTTP
                    print(f"<-- {ligne}")
            else:
                # to był pusty wiersz – nagłówki HTTP zostały zakończone
                fini = True
        # odczytuje się dokument HTML, który będzie następował po pustym wierszu
        # odczyt bieżącego wiersza
        ligne = file.readline()
        while ligne:
            # zapis w pliku dziennika
            html.write(str(ligne))
            # następny wiersz
            ligne = file.readline()
            # pętla kończy się, gdy serwer zamyka połączenie
    finally:
        # klient zamyka połączenie
        if sock:
            sock.close()
        # zamknięcie pliku HTML
        if html:
            html.close()


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

# konfiguracja aplikacji
import config
config = config.configure()

# pobieranie URL z pliku konfiguracyjnego
for url in config['urls']:
    print("-------------------------")
    print(url['site'])
    print("-------------------------")
    try:
        # odczyt URL ze strony [site]
        get_url(url)
    except BaseException as erreur:
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
# koniec
print("Terminé...")

Komentarze do kodu:

  • wiersze 108–109: pobierany jest słownik [config] z modułu [config.py];
  • wiersze 111–122: słownik ten jest wykorzystywany;
  • wiersz 118, 7: funkcja [get_url(url)] pobiera dokument ze strony internetowej url[site] i zapisuje go w pliku tekstowym url[site].HTML. Domyślnie komunikacja między klientem a serwerem jest rejestrowana w konsoli (monitorowanie=True);
  • wszystko odbywa się w pliku [try / finally] (wiersze 14–96). Nie ma klauzuli [except]. Wyjątki są przekazywane do kodu wywołującego i to on je zatrzymuje oraz wyświetla (wiersze 119–120);
  • wiersze 16–17: nawiązanie połączenia z serwerem WWW. Funkcja [socket.create_connection] przyjmuje trzy parametry:
    • [param1]: nazwa komputera w sieci Internet, z którym chcemy się połączyć;
    • [param2]: to numer portu usługi, z którą chcemy się połączyć;
    • [param3]: funkcja [socket.create_connection] zwraca gniazdo, a funkcja [param3], o ile jest obecna, określa limit czasu dla utworzonego gniazda. Limit czasu to maksymalny czas oczekiwania gniazda na odpowiedź od zdalnego komputera;
  • wiersze 27–28: utworzenie pliku [site.html], w którym zostanie zapisany otrzymany dokument HTML;
  • wiersze 34–43: pierwszym poleceniem klienta musi być polecenie [GET URL HTTP/1.1];
  • wiersz 43: funkcja [sock.send] umożliwia klientowi wysyłanie danych do serwera. W tym przypadku wysłany wiersz tekstu ma następujące znaczenie: „Chcę (GET) stronę [URL] z witryny internetowej, z którą jestem połączony. Korzystam z protokołu HTTP w wersji 1.1”;
  • wiersz 43: instrukcja [sock.send(bytearray(commande, 'utf-8'))] wysyła tablicę bajtów (bytearray). Tablica ta powstaje w wyniku konwersji ciągu znaków [commande] na sekwencję bajtów zakodowaną w formacie UTF-8;
  • wiersze 44–52: wysyłane są pozostałe wiersze protokołu HTTP [Host, User-Agent, Accept, Accept-Language…]. Ich kolejność nie ma znaczenia;
  • wiersze 53–55: wysyłamy nagłówek HTTP [Connection: close], aby poprosić serwer o zamknięcie połączenia po wysłaniu żądanego dokumentu. Domyślnie serwer tego nie robi. Należy więc wyraźnie o to poprosić. Zaletą tego rozwiązania jest to, że zamknięcie połączenia zostanie wykryte po stronie klienta i w ten sposób klient będzie wiedział, że otrzymał cały żądany dokument;
  • wiersze 56–57: wysyłamy do serwera pusty wiersz, aby zasygnalizować, że klient zakończył wysyłanie swoich nagłówków HTTP i teraz oczekuje na żądany dokument;
  • wiersze 68–86: serwer najpierw wyśle serię nagłówków HTTP, które zawierają różne informacje o żądanym dokumencie. Nagłówki te kończą się pustym wierszem;
  • wiersze 69–73: aby móc odczytywać odpowiedź serwera wiersz po wierszu, stosuje się metodę [sock.makefile(encoding=encoding)]. Opcjonalny parametr [encoding] określa kodowanie oczekiwanego tekstu. Po tej operacji strumień wierszy wysyłanych przez serwer będzie można odczytać jak zwykły plik tekstowy;
  • wiersz 78: odczytujemy wiersz wysłany przez serwer za pomocą metody [readline]. Usuwamy z niego spacje (znaki spacji, znaki końca wiersza) na początku i na końcu wiersza;
  • wiersze 81–83: jeśli wiersz nie jest pusty i zgłoszono prośbę o monitorowanie, odebrany wiersz jest wyświetlany na konsoli;
  • wiersze 84–86: jeśli pobrano pusty wiersz oznaczający koniec nagłówków HTTP wysłanych przez serwer, wówczas przerywa się pętlę z wiersza 76;
  • wiersze 90–95: wiersze tekstu zawarte w odpowiedzi serwera można odczytywać po jednym wierszu za pomocą pętli while i zapisywać w pliku tekstowym [html]. Gdy serwer WWW wyśle całą żądaną stronę, zamyka połączenie z klientem. Po stronie klienta zostanie to wykryte jako koniec pliku i nastąpi wyjście z pętli w wierszach 90–95;
  • wiersze 96–102: niezależnie od tego, czy wystąpił błąd, czy nie, zwalnia się wszystkie zasoby wykorzystywane przez kod;

Wyniki:

Konsola wyświetla następujące logi:


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/kursy-i-samouczki-programowania
<-- 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/kursy-i-samouczki-programowania/
<-- Set-Cookie: SERVERID68971=2620178|XwH/h|XwH/h; path=/
<-- X-IPLB-Instance: 17095
Terminé...

Process finished with exit code 0

Komentarze

  • wiersz 12: znaleziono plik URL [http://localhost/] (kod 200);
  • wiersz 29: nie znaleziono pliku URL [http://sergetahe.com/] (kod 302). Kod 302 oznacza, że żądana strona zmieniła swój identyfikator URL. Nowy adres URL jest wskazany w nagłówku HTTP [Location] w wierszu 36;
  • wiersz 49: żądanie wysłane do serwera [http://tahe.developpez.com] jest nieprawidłowe (kod 400);
  • wiersz 65: nie znaleziono strony o adresie URL [http://www.sergetahe.com/] (kod 301). Kod 301 oznacza, że adres żądanej strony uległ zmianie na URL i to w sposób ostateczny. Nowa strona jest wskazana w nagłówku w wierszu 71;

Ogólnie rzecz biorąc, kody 3xx, 4xx i 5xx serwera HTTP są kodami błędów.

W wyniku wykonania utworzono następujące pliki:

Image

Otrzymany plik [output/localhost.HTML] ma następującą treść:


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

Otrzymaliśmy ten sam dokument, co w przeglądarce Firefox.

Otrzymany dokument o nazwie [output/sergetahe_com.html] wygląda następująco:

Image

Większość serwerów HTTP wysyła odpowiedzi na otrzymane żądania w fragmentach. Każdy wysyłany fragment poprzedzony jest wierszem wskazującym liczbę bajtów następującego po nim fragmentu. Pozwala to klientowi odczytać dokładnie tę liczbę bajtów, aby pobrać fragment. W tym przypadku cyfra 0 oznacza, że następujący fragment ma zero bajtów. Przypomnijmy, że serwer poinformował, iż dokument [http://sergetahe.com/] został zastąpiony przez URL. Nie wysłał więc żadnego dokumentu.

Dokument [output/tahe_developpez_com.html] ma następującą treść:


<!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>
  • wiersze 1–12: serwer wysłał dokument HTML pomimo tego, że żądanie było nieprawidłowe (wiersz 49 wyników). Dokument HTML pozwala serwerowi określić przyczynę błędu. Jest ona wskazana w wierszach 6 i 7:
    • wiersz 7: nasz klient użył protokołu HTTP;
    • wiersz 8: serwer obsługuje protokół HTTPS (S = bezpieczny) i nie akceptuje protokołu HTTP;

Dokument [output/www_sergetahe_com.html] ma następującą treść:


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

Również w tym przypadku wystąpił błąd (wiersz 3). Niemniej jednak serwer wysyła dokument HTML zawierający szczegółowe informacje na temat tego błędu (wiersze 1–7).

21.4.4. Przykład 4

Poprzednie przykłady pokazały nam, że nasz klient HTTP był niewystarczający. Przedstawimy teraz narzędzie o nazwie [curl], które umożliwia pobieranie dokumentów internetowych, radząc sobie z wymienionymi trudnościami: protokołem HTTPS, dokumentem wysyłanym fragmentami, przekierowaniami… Narzędzie [curl] zostało zainstalowane wraz z Laragonem:

Image

Otwórzmy terminal PyCharm [1]:

Image

  • w [1] – dostęp do terminali z PyCharm;
  • w [2-3] – terminale już aktywne;
  • w [4] – folder, w którym się znajdujesz. W dalszej części nie ma to znaczenia;

W terminalu wpisujemy następujące polecenie:


(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

Fakt, że polecenie [curl –help] dało wyniki, wskazuje, że polecenie [curl] znajduje się w katalogu PATH terminala. W systemie Windows PATH to zbiór folderów przeszukiwanych, gdy użytkownik wpisze polecenie wykonywalne, w tym przypadku [curl]. Wartość PATH można ustalić:


(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;

Wiersz 2 zawiera katalogi z pliku PATH, oddzielone średnikami. Na tej liście nie pojawia się żaden katalog powiązany z programem Laragon. Po dokładniejszym zbadaniu okazuje się, że w katalogu [c:\windows\system32] znajduje się plik [curl]. To właśnie ten plik udzielił wcześniej odpowiedzi.

Jeśli chcemy skorzystać z narzędzia [curl] dostarczonego wraz z Laragonem, możemy postępować w następujący sposób:

Image

Image

  • w [2], terminal Laragon;
  • w [3] – ten przycisk pozwala tworzyć nowe terminale, z których każdy instaluje się w osobnej zakładce powyższego okna;
  • w [4] żądamy PATH terminala Laragon;
  • otrzymujemy coś zupełnie innego niż to, co uzyskano w terminalu PyCharm. Ten PATH zawiera wiele folderów utworzonych podczas instalacji Laragon. Folder zawierający narzędzie [curl] jest jednym z nich:

Image

Następnie należy skorzystać z wybranego terminala. Należy jednak pamiętać, że w przypadku korzystania z narzędzi dostarczonych przez Laragon zaleca się użycie terminala Laragon.

Polecenie [curl --help] wyświetla wszystkie opcje konfiguracyjne narzędzia [curl]. Jest ich kilkadziesiąt. My wykorzystamy z nich bardzo niewiele. Aby wywołać URL, wystarczy wpisać polecenie [curl URL]. Polecenie to wyświetli na konsoli żądany dokument. Jeśli dodatkowo chcemy uzyskać komunikację HTTP między klientem a serwerem, wpiszemy [curl --verbose URL]. Wreszcie, aby zapisać żądany dokument HTML do pliku, wpiszemy [curl --verbose --output fichier URL].

Aby uniknąć zaśmiecania systemu plików naszego komputera, przejdźmy do innej lokalizacji (używam tutaj terminala Laragon):


λ 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                                          
  • w wierszu 3 przechodzimy do folderu [c:\temp]. Jeśli ten folder nie istnieje, można go utworzyć lub wybrać inny;
  • w wierszu 6 tworzymy folder o nazwie [curl];
  • w wierszu 9 przechodzimy do tego folderu;
  • w wierszu 12 wyświetlamy zawartość folderu. Jest on pusty (wiersz 20);

Upewnij się, że serwer Apache w Laragonie jest uruchomiony, a następnie za pomocą pliku [curl] wywołaj pliki URL i [http://localhost/] za pomocą polecenia [curl –verbose –output localhost.html http://localhost/]. Otrzymujemy następujące wyniki:


λ 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, aby zachować nazwę hosta „localhost” bez zmian
  • wiersze 10–13: wiersze wysłane przez [curl] do serwera [localhost]. Rozpoznajemy protokół HTTP;
  • wiersze 14–20: wiersze wysłane w odpowiedzi przez serwer;
  • wiersz 14: wskazuje, że żądany dokument został pomyślnie odebrany;

Plik [localhost.html] zawiera żądany dokument. Można to sprawdzić, otwierając plik w edytorze tekstowym.

Teraz poprośmy o plik URL i [https://tahe.developpez.com:443/]. Aby uzyskać ten plik URL, klient HTTP musi obsługiwać format HTTPS. Tak jest w przypadku klienta [curl].

Wyniki wyświetlone na konsoli są następujące:


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 do hosta tahe.developpez.com pozostawiono bez zmian
  • wiersze 10–39: wymiana danych między klientem a serwerem w celu zabezpieczenia połączenia: połączenie zostanie zaszyfrowane;
  • wiersze 41–44: nagłówki HTTP wysłane przez klienta [curl] do serwera;
  • wiersz 52: żądany dokument został znaleziony;
  • wiersz 57: dokument jest wysyłany fragmentami;

[curl] poprawnie obsługuje zarówno protokół zabezpieczony HTTPS, jak i fakt, że dokument jest wysyłany w częściach. Wysłany dokument znajdzie się tutaj, w pliku [tahe.developpez.com.html].

Poprośmy teraz o plik URL [http://sergetahe.com/cours-tutoriels-de-programmation]. Widzieliśmy już, że w przypadku pliku URL nastąpiło przekierowanie do plików URL i [http://sergetahe.com/cours-tutoriels-de-programmation/] (z końcowym znakiem /).

Wyniki wyświetlane w konsoli są następujące:


C:\Temp\curl
λ curl --verbose --output sergetahe.com.html --location http://sergetahe.com/kursy-i-samouczki-programowania
  % 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/kursy-i-samouczki-programowania/
< 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, aby zachować sergetahe.com w niezmienionej postaci
* Issue another request to this URL: 'http://sergetahe.com/kursy-i-samouczki-programowania/'
* Found bundle for host sergetahe.com: 0x14385f8 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) z hostem 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/kursy-i-samouczki-programowania/
< 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 do hosta sergetahe.com pozostawiono bez zmian
* Issue another request to this URL: 'https://sergetahe.com/kursy-i-samouczki-programowania/'
*   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/kursy-i-samouczki-programowania/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/kursy-i-samouczki-programowania/>; 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 do hosta sergetahe.com pozostawiono bez zmian
  • wiersz 2: używamy opcji [--location], aby wskazać, że chcemy podążać za przekierowaniami wysyłanymi przez serwer;
  • wiersz 13: serwer informuje, że żądany dokument zmienił się na URL;
  • wiersz 18: podaje nowy adres URL żądanego dokumentu;
  • wiersz 31: [curl] wysyła nowe żądanie, tym razem do nowego adresu URL;
  • wiersz 36: serwer ponownie odpowiada, że identyfikator URL uległ zmianie;
  • wiersz 41: nowy identyfikator URL jest dokładnie taki sam jak ten, który został przekierowany, z jednym wyjątkiem: zmienił się protokół. Stał się on HTTPS (wiersz 41), podczas gdy wcześniej był to http (wiersz 31);
  • wiersz 49: wysyłane jest nowe żądanie do nowego adresu URL. Jest ono zaszyfrowane. W związku z tym rozpoczyna się cały dialog dotyczący ustanowienia zabezpieczeń, wiersze 53–91;
  • wiersz 92: wysyłane jest żądanie do nowego serwera URL, tym razem z protokołem HTTP/2;
  • wiersz 100: dokument został znaleziony;

Żądany dokument zostanie znaleziony w pliku [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. Przykład 5

Python posiada moduł o nazwie [pyccurl], który umożliwia wykorzystanie możliwości narzędzia [curl] w programie napisanym w języku Python. Instalujemy ten moduł:

Image

Napiszemy nowy skrypt o nazwie [http/02/main.py]:

Image

Plik [http/02/config] wygląda następująco:


def configure():
    # lista URL do sprawdzenia
    urls = [
        # serwer docelowy: serwer, z którym należy nawiązać połączenie
        # timeout: maksymalny czas oczekiwania na odpowiedź serwera
        # target: adres URL, do którego należy wysłać żądanie
        # kodowanie: kodowanie odpowiedzi serwera
        {
            "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"
        }
    ]
    # konfiguracja jest zwracana
    return {
        '„urls”: adresy URL
    }

Plik zawiera listę słowników, z których każdy ma następującą strukturę:

  • site: nazwa serwera WWW;
  • encoding: oczekiwany typ kodowania dokumentu;
  • timeout: maksymalny czas oczekiwania na odpowiedź serwera wyrażony w milisekundach. Po upływie tego czasu klient rozłączy się;
  • url: URL żądanego dokumentu;

Kod skryptu [http/02/main.py] jest następujący:


# importy
import codecs
from io import BytesIO

import pycurl


# -----------------------------------------------------------------------
def get_url(url: dict, suivi=True):
    # odczytuje adres URL URL i zapisuje go w pliku output/url['site'].html
    # jeśli [suivi=True], to następuje monitorowanie komunikacji klient–serwer za pomocą konsoli
    # url[timeout] to limit czasu dla wywołań klienckich;
    # adres URL [encoding] określa kodowanie żądanego dokumentu

    # pobierane są dane konfiguracyjne
    server = url['site']
    timeout = url['timeout']
    target = url['target']
    encoding = url['encoding']
    # śledzenie
    print(f"Client : début de la communication avec le serveur [{server}]")

    # pozwalamy na przekazywanie wyjątków
    html = None
    curl = None
    try:
        # Inicjalizacja sesji cURL
        curl = pycurl.Curl()
        # strumień binarny
        flux = BytesIO()
        # opcje curl
        options = {
            # URL
            curl.URL: target,
            # WRITEDATA: miejsce, w którym będą przechowywane otrzymane dane
            curl.WRITEDATA: flux,
            # tryb szczegółowy
            curl.VERBOSE: suivi,
            # nowe połączenie – brak pamięci podręcznej
            curl.FRESH_CONNECT: True,
            # limit czasu żądania (w sekundach)
            curl.TIMEOUT: timeout,
            curl.CONNECTTIMEOUT: timeout,
            # nie sprawdzaj ważności certyfikatów SSL
            curl.SSL_VERIFYPEER: False,
            # śledź przekierowania
            curl.FOLLOWLOCATION: True
        }
        # konfiguracja curl
        for option, value in options.items():
            curl.setopt(option, value)
        # Wykonanie zapytania CURL z takimi ustawieniami
        curl.perform()
        # utworzenie pliku server.html – zamiana niepożądanych znaków na nazwę pliku
        server2 = server.replace("/", "_")
        server2 = server2.replace(".", "_")
        html_filename = f'{server2}.html'
        html = codecs.open(f"output/{html_filename}", "w", encoding)
        # zapisanie otrzymanego dokumentu w pliku HTML
        html.write(flux.getvalue().decode(encoding))
    finally:
        # zwolnienie zasobów
        if curl:
            curl.close()
        if html:
            html.close()


# -------------------główne
# konfiguracja aplikacji
import config
config = config.configure()

# pobieranie danych URL z pliku konfiguracyjnego
for url in config['urls']:
    print("-------------------------")
    print(url['site'])
    print("-------------------------")
    try:
        # odczyt URL ze strony [site]
        get_url(url)
    # z wyjątkiem BaseException jako błąd:
    #     print(f"Wystąpił następujący błąd: {błąd}")
    finally:
        pass
# koniec
print("Terminé...")

Komentarze

  • wiersz 5: importujemy moduł [pycurl];
  • wiersz 3: importujemy klasę [BytesIO], która pozwoli nam zapisać dane otrzymane z serwera w strumieniu binarnym;
  • wiersze 70–72: pobieramy konfigurację aplikacji;
  • wiersze 75–85: przechodzimy przez pętlę listy obiektów URL znalezionych w konfiguracji;
  • wiersz 81: dla każdego z elementów URL wywoływana jest funkcja [get_url], która pobiera adres URL URL z limitem czasu [‘target’];
  • wiersz 9: funkcja [get_url] otrzymuje konfigurację URL, do której ma zostać wysłane zapytanie;
  • wiersze 16–19: pobieramy konfigurację URL do oddzielnych zmiennych;
  • wiersze 26, 61: wszystkie operacje są wykonywane w bloku try / finally. Wyjątki nie są przechwytywane, więc są przekazywane do kodu wywołującego, który je przechwytuje;
  • wiersz 28: przygotowujemy sesję [curl]. [pycurl.Curl()] zwraca zasób [curl], który przeprowadzi transakcję z serwerem;
  • wiersz 30: instancja strumienia binarnego, który będzie przechowywał otrzymane dane;
  • wiersze 32–48: słownik [options] skonfiguruje połączenie [curl] z serwerem. Ich rola została opisana w komentarzach;
  • wiersze 49–51: opcje połączenia są przekazywane do zasobu [curl];
  • wiersz 53: żądane połączenie z funkcją URL z określonymi opcjami. Ze względu na opcję [curl.WRITEDATA: flux] (wiersz 36) funkcja [curl.perform()] zapisze otrzymane dane w pliku [flux];
  • wiersze 54–60: tworzony jest plik HTML, w którym zostanie zapisany odebrany dokument HTML;
  • wiersz 60: strumień binarny [flux.getvalue()] zostanie zapisany jako ciąg znaków w pliku HTML. Kodowanie tego ciągu jest określone w metodzie [decode(encoding)]. Należy zatem znać kodowanie dokumentu przesłanego przez serwer. W przypadku pomyłki operacja dekodowania strumienia binarnego zakończy się niepowodzeniem. Kodowanie jest określone w pliku konfiguracyjnym URL (na przykład w wierszu 12). Można było dynamicznie zarządzać tą informacją, ponieważ serwer wysyła ją w nagłówkach HTTP. Byłoby to lepszym rozwiązaniem. Aby zachować prostotę kodu, nie zrobiliśmy tego. Aby poznać rodzaj kodowania dokumentu, wystarczy wywołać żądany plik URL w przeglądarce i sprawdzić nagłówki HTTP wysyłane przez przeglądarkę w trybie debugowania (F12) lub sam dokument, ponieważ on również określa kodowanie:

Image

Image

  • wiersze 61–66: przydzielone zasoby są zwalniane;

Po uruchomieniu skryptu [main.py] otrzymujemy następujące wyniki w konsoli:


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/kursy-i-samouczki-programowania
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0, aby zachować adres sergetahe.com w niezmienionej postaci
* Issue another request to this URL: 'http://sergetahe.com/kursy-i-samouczki-programowania'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) z hostem 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/kursy-i-samouczki-programowania/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 do hosta sergetahe.com pozostawiono bez zmian
* Issue another request to this URL: 'http://sergetahe.com/kursy-i-samouczki-programowania/'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) z hostem 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/kursy-i-samouczki-programowania/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 do hosta sergetahe.com pozostawiono bez zmian
* Issue another request to this URL: 'https://sergetahe.com/kursy-i-samouczki-programowania/'
*   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/kursy-i-samouczki-programowania/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/kursy-i-samouczki-programowania/>; rel=shortlink
< vary: Accept-Encoding
< x-iplb-instance: 17080
< set-cookie: SERVERID68971=26218|XwLIp|XwLIp; path=/
< 
* Connection #1 do hosta sergetahe.com pozostawionego bez zmian
-------------------------
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, aby zachować tahe.developpez.com w niezmienionej postaci
-------------------------
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 do hosta www.polytech-angers.fr pozostawiono bez zmian
* 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) z hostem 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 do hosta www.polytech-angers.fr pozostało nienaruszone
-------------------------
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 do hosta localhost pozostało nienaruszone
Terminé...

Process finished with exit code 0

Komentarze

  • na niebiesko – polecenia HTTP wysłane do serwera;
  • na zielono – dane otrzymane przez klienta w odpowiedzi;
  • otrzymujemy tę samą wymianę danych, co w przypadku narzędzia [curl];
    • wiersz 9: wysłano żądanie dotyczące URL [http://sergetahe.com/];
    • wiersz 15: serwer odpowiada, że strona została przeniesiona. Wiersz 21: nowy plik URL;
    • wiersz 32: wysyłane jest żądanie o adres URL [http://sergetahe.com/cours-tutoriels-de-programmation];
    • wiersz 38: serwer odpowiada, że strona została przeniesiona. Wiersz 43, nowy adres URL;
    • wiersz 54: wysyłane jest żądanie o adres URL [http://sergetahe.com/cours-tutoriels-de-programmation/];
    • wiersz 60: serwer odpowiada, że strona została przeniesiona. Wiersz 65, nowy adres URL. Wykorzystuje on bezpieczny protokół [HTTPS];
    • wiersze 71–75: nawiązywany jest bezpieczny protokół z serwerem;
    • wiersz 76: wysyłane jest żądanie dotyczące pliku URL [https://sergetahe.com/cours-tutoriels-de-programmation/];
    • wiersz 82: żądany dokument został znaleziony;

21.4.6. Wnioski

W tej sekcji zapoznaliśmy się z protokołem HTTP i napisaliśmy skrypt [http/02/main.py] umożliwiający pobranie pliku URL z sieci.

21.5. Protokół SMTP (Simple Mail Transfer Protocol)

21.5.1. Wprowadzenie

Image

W tym rozdziale:

  • [Serveur B] będzie lokalnym serwerem SMTP, który zainstalujemy;
  • [Client A] będzie klientem SMTP występującym w różnych postaciach:
    • klient [RawTcpClient] służący do odkrywania protokołu SMTP;
    • skrypt w języku Python odtwarzający protokół SMTP klienta [RawTcpClient];
    • skrypt w języku Python wykorzystujący moduł [smtplib], umożliwiający wysyłanie wszelkiego rodzaju wiadomości e-mail;

21.5.2. Utworzenie adresu [gmail]

Aby przeprowadzić nasze testy SMTP, będziemy potrzebować adresu e-mail, na który możemy wysłać wiadomość. W tym celu utworzymy adres Gmaila [https://www.google.com/intl/fr/gmail/about/]:

Image

Uwaga: Wyślij kilka wiadomości e-mail na utworzony adres. Przejdź do kolejnego kroku dopiero wtedy, gdy upewnisz się, że utworzone konto może odbierać wiadomości e-mail.

21.5.3. Instalacja serwera SMTP

W ramach naszych testów zainstalujemy serwer pocztowy [hMailServer], który jest jednocześnie serwerem SMTP umożliwiającym wysyłanie wiadomości e-mail, serwer POP3 (Post Office Protocol), umożliwiający odczytywanie wiadomości e-mail przechowywanych na serwerze, oraz serwer IMAP (Internet Message Access Protocol), który również pozwala na odczytywanie wiadomości e-mail przechowywanych na serwerze, ale oferuje szersze możliwości. Umożliwia on w szczególności zarządzanie przechowywaniem wiadomości e-mail na serwerze.

Serwer pocztowy [hMailServer] jest dostępny na serwerach URL i [https://www.hmailserver.com/] (maj 2019 r.).

Image

Podczas instalacji zostaną Państwo poproszeni o podanie pewnych informacji:

Image

  • w [1-2] należy wybrać zarówno serwer pocztowy, jak i narzędzia do jego administrowania;
  • podczas instalacji zostaniesz poproszony o podanie hasła administratora: zapisz je, ponieważ będzie ono potrzebne;

[hMailServer] instaluje się jako usługa systemu Windows uruchamiana automatycznie przy starcie komputera. Zaleca się wybranie opcji uruchamiania ręcznego:

  • w [3] należy wpisać [services] w polu wprowadzania danych na pasku stanu;

Image

  • w przypadku [4-8] należy ustawić usługę w trybie [manuel] (6), a następnie ją uruchomić (7);

Po uruchomieniu serwer [hMailServer] należy skonfigurować. Serwer został zainstalowany wraz z programem administracyjnym [hMailServer Administrator]:

Image

  • w [2], w polu wprowadzania danych na pasku stanu wpisać [hmailserver];
  • w [3] uruchomić administratora;
  • w [4] zaloguj administratora do serwera [hMailServer];
  • w [5] wpisz hasło podane podczas instalacji [hMailServer];

Jeśli zapomniałeś hasła, wykonaj następujące czynności:

  • zatrzymaj serwer [hMailServer];
  • otwórz plik [<hmailserver>/bin/hmailserver.ini], gdzie <hmailserver> to folder instalacyjny serwera:

Image

  • w pliku [100] usuń hasło z wiersza [AdministratorPassword]. Spowoduje to, że administrator nie będzie już miał hasła. Po wyświetleniu monitu o podanie hasła wpisz po prostu [Entrée];

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

Kontynuujmy konfigurację serwera:

Image

  • w [1-2] dodaj domenę (jeśli jeszcze nie istnieje);

Image

  • w polu [3] można wpisać praktycznie dowolną wartość na potrzeby przeprowadzanych przez nas testów. W rzeczywistości należałoby wpisać nazwę istniejącej domeny;

Image

Utworzymy konto użytkownika:

  • klikamy prawym przyciskiem myszy na [Accounts] (7), a następnie (8), aby dodać nowego użytkownika;
  • w zakładce [General] (9) definiujemy użytkownika [guest] (10) z hasłem [guest] (11). Będzie on miał adres e-mail [guest@localhost] (10);
  • w [12] użytkownik [guest] jest aktywowany;

Image

  • w [13-14] utworzono użytkownika;

Image

  • w [27] port usługi SMTP;
  • w [28] ta usługa nie wymaga uwierzytelniania;
  • w [30] należy umieścić komunikat powitalny, który serwer SMTP wyśle do swoich klientów;

Image

To samo dotyczy serwera POP3:

Image

Powtarzamy tę samą procedurę dla serwera IMAP:

Image

Określamy domyślną domenę serwera [hMailServer] (może ich być kilka) jako :

Image

  • w [37] należy wskazać, że domyślną domeną serwera SMTP jest ta, którą utworzyli Państwo w [38];

Po zapisaniu tej konfiguracji można ją przetestować w następujący sposób. Otwórz terminal PyCharm w folderze narzędzi:

Image

Następnie wpisz następujące polecenie:


(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]
  • wiersz 1: nawiązujemy połączenie z portem 25 urządzenia [localhost]. Tam działa niezabezpieczony serwer SMTP należący do serwera [hMailServer];
  • wiersz 4: otrzymujemy komunikat powitalny, który skonfigurowaliśmy w poprzednim kroku 30;

Serwer SMTP działa więc prawidłowo. Wpisz polecenie [quit], aby zakończyć komunikację z serwerem SMTP na porcie 25.

Teraz wykonajmy tę samą czynność dla portu 587, który jest domyślnym portem zabezpieczonej usługi pobierania poczty 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]
  • wiersz 4, odpowiedź serwera SMTP działającego na porcie 587;

Teraz zróbmy to samo z portem 110, który jest domyślnym portem usługi POP3 do pobierania poczty:


(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]
  • w wierszu 4 otrzymaliśmy wiadomość powitalną z serwera POP3;

Teraz zróbmy to samo z portem 143, który jest domyślnym portem usługi pobierania poczty 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]
  • w wierszu 4 otrzymaliśmy wiadomość powitalną od serwera IMAP;

21.5.4. Instalacja programu do obsługi poczty

Aby przeczytać wiadomość, którą zamierzamy wysłać, potrzebujemy klienta poczty. Dla tych, którzy go nie mają, pokazujemy instalację i konfigurację klienta [Thunderbird]:

  • w [1]: pobierz [thunderbird], a następnie zainstaluj go;

Image

  • uruchom serwer pocztowy [hMailServer], jeśli jeszcze nie jest uruchomiony;
  • w [2-3]: po uruchomieniu programu Thunderbird utworzymy konto pocztowe dla użytkownika [guest@localhost] na serwerze pocztowym [hMailServer];

Image

Image

Image

  • w [7-11]: serwer POP3, który umożliwi nam odczytywanie wiadomości z serwera pocztowego [hMailServer], znajduje się pod adresem [localhost] i działa na porcie 110;
  • w [12-16]: serwer SMTP, który umożliwi nam wysyłanie wiadomości e-mail w imieniu użytkowników serwera pocztowego [hMailServer], znajduje się pod adresem [localhost] i obsługuje port 25;
  • [18]: można sprawdzić poprawność tej konfiguracji;

Image

Image

  • pod adresem [26]: ponieważ nie ma szyfrowania SSL, Thunderbird ostrzega, że nasza konfiguracja wiąże się z ryzykiem;
  • w [28]: konto zostało utworzone;

Aby przetestować utworzone konto, w programie Thunderbird:

  • wyślemy wiadomość e-mail do użytkownika [guest@localhost.com] (protokół SMTP);
  • przeczytamy wiadomość otrzymaną przez tego użytkownika (protokół POP3);

Image

  • w [3]: nadawca;
  • w [4]: odbiorca;
  • w [5]: temat wiadomości e-mail;
  • w [6]: treść wiadomości e-mail;
  • w [7]: wysłanie wiadomości e-mail;

Image

  • w [8-9]: odbieramy wiadomość od użytkownika [guest@localhost];
  • w [10-15]: otrzymana wiadomość;

Wyślemy również wiadomość do użytkownika [pymailparlexemple@gmail.com]. Utwórzmy dla niego konto w programie Thunderbird, aby mógł odczytać otrzymaną wiadomość:

Image

Image

  • w polu [4]: wpisz dowolną treść;
  • w polu [5]: adres to [pymailparlexemple@gmail.com];
  • w polu [6]: wpisz hasło, które nadałeś temu użytkownikowi podczas jego tworzenia;
  • w polu [7]: zatwierdź tę konfigurację;

Image

  • w polu [8]: program Thunderbird pobrał następujące informacje ze swojej bazy danych;
  • w [9]: protokołem pobierania poczty nie jest już POP3, lecz IMAP. Główna różnica między nimi polega na tym, że [POP3] pobiera przeczytane wiadomości na komputer lokalny, na którym znajduje się klient pocztowy, i usuwa je z serwera zdalnego, podczas gdy [IMAP] zachowuje wiadomości na serwerze zdalnym;
  • w [10]: identyfikacja serwera SMTP;
  • w [13]: aby uzyskać więcej informacji na temat serwerów IMAP i SMTP, przechodzi się do trybu konfiguracji ręcznej;

Image

  • w [14-17]: parametry serwera IMAP;
  • na [18-21]: parametry serwera SMTP;
  • w [22]: kończymy konfigurację;

Image

  • w [23-24]: nowe konto w Thunderbirdzie;
  • w [26]: piszemy nową wiadomość;

Image

  • w [27]: nadawcą jest [pymailparlexemple@gmail.com];
  • w [28]: odbiorcą jest [pymailparlexemple@gmail.com];
  • w [29-30]: treść wiadomości;
  • w pliku [31]: w celu wysłania;

Image

  • w [32]: pobieramy pocztę z różnych kont;

Image

  • w [33-36]: wiadomości otrzymane przez użytkownika [pymailparlexemple@gmail.com]

Tworzymy również:

  • nowe konto Gmail o nazwie [pymail2parlexemple@gmail.com];
  • nowe konto Thunderbird o nazwie [pymail2parlexemple@gmail.com] w celu pobierania wiadomości użytkownika o tej samej nazwie:

Image

Image

Mamy teraz narzędzia pozwalające na analizę protokołów SMTP, POP3 i IMAP. Zaczynamy od protokołu SMTP.

21.5.5. Protokół SMTP

Image

Zapoznamy się z protokołem SMTP, analizując logi serwera [hMailServer]. W tym celu aktywujemy je za pomocą narzędzia [hmailServerAdministrator]:

Image

Image

  • w [2] logi są włączone;
  • w [3-5]: włączamy je dla protokołów SMTP, POP3, IMAP;
  • w pliku [7] żąda się wyświetlenia logów;
  • w [8] otwiera się plik logów w dowolnym edytorze tekstowym;

Image

W poniższym przykładzie klientem będzie [Thunderbird], a serwerem – [hMailServer]. W programie Thunderbird należy sprawić, aby użytkownik [guest@localhost.com] wysłał wiadomość do samego siebie:

Image

Wynikowe logi będą wyglądały następująco:


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

Powyższe wiersze opisują dialog, który miał miejsce między klientem SMTP (klientem pocztowym Thunderbird) a serwerem SMTP (hMailServer). Wiersze [SENT] wskazują, co serwer SMTP wysłał do swojego klienta. Wiersze [RECEIVED] wskazują, co serwer SMTP otrzymał od swojego klienta.

  • wiersz 1: zaraz po nawiązaniu połączenia przez klienta z serwerem SMTP serwer wysyła do niego komunikat powitalny;
  • wiersz 2: klient wysyła polecenie [EHLO] w celu identyfikacji. W tym miejscu podaje swój adres IP [127.0.0.1], który wskazuje na maszynę [localhost], tj. maszynę, na której działa klient SMTP;
  • wiersz 3: serwer wysyła serię odpowiedzi [250]. [nl] oznacza [newline], czyli znak \n. Odpowiedzi mają postać [250-], z wyjątkiem ostatniej, która ma postać [250 ]. W ten sposób klient SMTP wie, że odpowiedź serwera SMTP została zakończona i może wysłać polecenie. Seria poleceń [250] miała na celu wskazanie klientowi SMTP zestawu poleceń, z których mógł skorzystać;
  • wiersz 4: klient SMTP wysyła polecenie [MAIL FROM : adresse_mail_expéditeur], które wskazuje, kto wysyła wiadomość;
  • wiersz 5: serwer SMTP odpowiada komunikatem [250 OK], potwierdzającym zrozumienie polecenia;
  • wiersz 6: klient SMTP wysyła polecenie [RCPT TO : adresse_mail_destinataire] w celu wskazania adresu odbiorcy;
  • wiersz 7: ponownie serwer SMTP potwierdza, że zrozumiał polecenie;
  • wiersz 8: serwer SMTP wysyła polecenie [DATA]. Oznacza to, że zamierza wysłać treść wiadomości;
  • wiersz 9: serwer SMTP sygnalizuje w odpowiedzi [354 OK], że jest gotowy do odbioru wiadomości. Tekst [send .] wskazuje, że klient SMTP musi zakończyć swoją wiadomość wierszem zawierającym wyłącznie pojedynczy kropkę;
  • nie widać jednak, że klient SMTP wysyła swoją wiadomość. Logi tego nie pokazują;
  • wiersz 10: klient SMTP wysłał kropkę sygnalizującą koniec wiadomości. Serwer SMTP odpowiada mu, że umieścił wiadomość w kolejce (queued);
  • klient SMTP wysyła do niego polecenie [QUIT], aby poinformować, że zamierza zamknąć połączenie;
  • wiersz 12: serwer odpowiada;

Teraz, gdy znamy już dialog klient–serwer protokołu SMTP, spróbujmy go odtworzyć z naszym klientem [RawTcpClient]. Używamy terminala PyCharm:

Image

Przeanalizujmy nowy przykład:

  • klientem A będzie klient TCP, będący wersją ogólną klienta [RawTcpClient];
  • serwer B będzie serwerem pocztowym o identyfikatorze [hMailServer];
  • klient A zwróci się do serwera B z prośbą o dostarczenie wiadomości e-mail wysłanej przez użytkownika [guest@localhost.com] dla niego samego;
  • sprawdzimy, czy odbiorca rzeczywiście otrzymał wysłaną wiadomość e-mail;

Uruchamiamy klienta w następujący sposób:


(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]
  • w wierszu [1] nawiązujemy połączenie z portem 25 na komputerze lokalnym, gdzie działa usługa SMTP z [hMailServer]. Argument [--quit bye] oznacza, że użytkownik zakończy działanie programu, wpisując polecenie [bye]. Bez tego argumentu poleceniem kończącym działanie programu jest [quit]. Jednak [quit] jest również poleceniem protokołu SMTP. Musimy zatem uniknąć tej niejednoznaczności;
  • w wierszu [2] klient jest prawidłowo podłączony;
  • w wierszu [3] klient oczekuje na polecenia wpisane z klawiatury;
  • wiersz [4] – serwer wysyła do niego komunikat powitalny;

Kontynuujemy dialog w następujący sposób:


(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
  • w [5] klient wysyła polecenie [EHLO nom-de-la-machine-client]. Serwer odpowiada mu serią komunikatów o postaci [250-xx] (6). Kod [250] wskazuje na pomyślne wykonanie polecenia wysłanego przez klienta;
  • w [10] klient podaje nadawcę komunikatu, w tym przypadku [guest@localhost.com];
  • w [11] znajduje się odpowiedź serwera;
  • w [12] podano odbiorcę wiadomości, w tym przypadku użytkownika [guest@localhost.com];
  • w [13] – odpowiedź serwera;
  • w [14] polecenie [DATA] informuje serwer, że klient wyśle treść wiadomości;
  • w [15] – odpowiedź serwera;
  • w [16-22] klient musi wysłać listę wierszy tekstu zakończoną wierszem zawierającym tylko jedną kropkę. Wiadomość może zawierać wiersze [Subject:, From:, To:] (16–18) w celu określenia odpowiednio tematu wiadomości, nadawcy i odbiorcy;
  • w formacie [19] po powyższych nagłówkach musi następować pusty wiersz;
  • w [20-21] – tekst wiadomości;
  • w wierszu [22] znajduje się wiersz zawierający tylko jedną kropkę, która oznacza koniec wiadomości;
  • w [23], gdy serwer odbierze wiersz zawierający tylko jedną kropkę, umieszcza wiadomość w kolejce;
  • w [24] klient informuje serwer, że zakończył transmisję;
  • w [25] widać, że serwer zamknął połączenie z klientem;

Teraz sprawdźmy w programie Thunderbird, czy użytkownik [guest@localhost.com] rzeczywiście otrzymał wiadomość:

Image

  • w [1-6] widać, że użytkownik [guest@localhost.com] rzeczywiście otrzymał wiadomość;

W końcu naszemu klientowi [RawTcpClient] udało się wysłać wiadomość za pośrednictwem serwera SMTP [localhost]. Teraz wykorzystajmy tę samą metodę, aby wysłać wiadomość do [pymailparlexemple@gmail.com]:


(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
  • wiersz 1: korzystamy z serwera SMTP serwisu Gmail, który działa na porcie 587;
  • wiersz 15: utknęliśmy, ponieważ serwer SMTP wymaga od nas nawiązania bezpiecznego połączenia, a nie wiemy, jak to zrobić. W przeciwieństwie do poprzedniego przykładu serwer [smtp.gmail.com] (wiersz 1) wymaga uwierzytelnienia. Akceptuje on jako klientów wyłącznie użytkowników zarejestrowanych w domenie [gmail.com]. Uwierzytelnianie to jest bezpieczne i odbywa się w ramach szyfrowanego połączenia.

Pierwszy przykład dostarczył nam podstaw do stworzenia podstawowego klienta SMTP w języku Python. Drugi przykład pokazał nam, że niektóre serwery SMTP (a właściwie większość z nich) wymagają uwierzytelniania przeprowadzanego za pomocą szyfrowanego połączenia.

21.5.6. Skrypty [smtp/01]: podstawowy klient SMTP

W języku Python odtworzymy to, czego nauczyliśmy się wcześniej na temat protokołu SMTP.

Image

Plik [smtp/01/config] konfiguruje aplikację w następujący sposób:


def configure() -> dict:
    return {
        # opis: opis wysłanej wiadomości e-mail
        # serwer SMTP: serwer SMTP
        # smtp-port: port serwera SMTP
        # from: nadawca
        # do: odbiorca
        # subject: temat wiadomości e-mail
        # treść: treść wiadomości 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",
                # wysyłamy UTF-8
                "content-type": 'text/plain; charset="utf-8"',
                # testujemy znaki z akcentami
                "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",
                # wysyłamy UTF-8
                "Content-type": 'text/plain; charset="utf-8"',
                # testujemy znaki z akcentami
                "message": "aglaë séléné\nva au marché\nacheter des fleurs"
            }
        ]
    }
  • wiersze 10–35: lista wiadomości e-mail do wysłania. Dla każdej z nich podaje się następujące informacje:
    • [description]: tekst opisujący wiadomość e-mail;
    • [smtp-server]: serwer SMTP, z którego należy korzystać;
    • [smtp-port]: port serwisu;
    • [from]: nadawca wiadomości e-mail;
    • [to]: odbiorca wiadomości e-mail;
    • [subject]: temat wiadomości e-mail;
    • [content-type]: kodowanie wiadomości e-mail;
    • [message]: treść wiadomości e-mail;

Kod [01/main] klienta SMTP jest następujący:


# importy
import socket


# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: bool):
    # wysyła wiadomość na serwer SMTP smtpserver w imieniu nadawcy
    # dla odbiorcy. Jeśli verbose=True, śledzi wymianę danych między klientem a serwerem

    # pozwala na zgłaszanie błędów systemowych
    connexion = None
    try:
        # nazwa komputera lokalnego (wymagana przez protokół SMTP)
        client = socket.gethostbyaddr(socket.gethostbyname("localhost"))[0]
        # nawiązanie połączenia na porcie 25 serwera smtpServer
        connexion = socket.create_connection((mail["smtp-server"], 25))

        # połączenie stanowi dwukierunkowy strumień komunikacji
        # między klientem (tym programem) a serwerem SMTP, z którym nawiązano połączenie
        # kanał ten służy do wymiany poleceń i informacji

        # po nawiązaniu połączenia serwer wysyła wiadomość powitalną, którą odczytujemy
        send_command(connexion, "", verbose, True)
        # polecenie „ehlo”:
        send_command(connexion, f"EHLO {client}", verbose, True)
        # polecenie mail from:
        send_command(connexion, f"MAIL FROM: <{mail['from']}>", verbose, True)
        # polecenie rcpt to:
        send_command(connexion, f"RCPT TO: <{mail['to']}>", verbose, True)
        # polecenie data
        send_command(connexion, "DATA", verbose, True)
        # przygotowanie wiadomości do wysłania
        # musi zawierać następujące wiersze
        # From: nadawca
        # Do: odbiorca
        # pusta linia
        # Treść wiadomości
        # .
        data = f"{mail['message']}"
        # wysłanie wiadomości
        send_command(connexion, data, verbose, False)
        # wysłanie .
        send_command(connexion, "\r\n.\r\n", verbose, False)
        # polecenie quit
        send_command(connexion, "QUIT", verbose, True)
        # koniec
    finally:
        # zamknięcie połączenia
        if connexion:
            connexion.close()


# --------------------------------------------------------------------------
def send_command(connexion: socket, commande: str, verbose: bool, with_rclf: bool):
    # wysyła polecenie na kanale połączenia
    # tryb szczegółowy, jeśli verbose=True
    # jeśli with_rclf=True, dodaje sekwencję rclf do polecenia

    # dane
    rclf = "\r\n" if with_rclf else ""
    # wysyła polecenie, jeśli pole „command” nie jest puste
    if commande:
        # pozwala na zgłaszanie błędów systemowych
        #
        # wysyłanie polecenia
        connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
        # ewentualne echo
        if verbose:
            affiche(commande, 1)
        # odczyt odpowiedzi o długości mniejszej niż 1000 znaków
        reponse = str(connexion.recv(1000), 'utf-8')
        # ewentualne echo
        if verbose:
            affiche(reponse, 2)
        # pobieranie kodu błędu
        codeErreur = int(reponse[0:3])
        # błąd zwrócony przez serwer?
        if codeErreur >= 500:
            # wyzwalany jest wyjątek z błędem
            raise BaseException(reponse[4:])
        # powrót bez błędu


# --------------------------------------------------------------------------
def affiche(echange: str, sens: int):
    # wyświetla wymianę na ekranie?
    # jeśli sens=1, wyświetla -->wymiana
    # jeśli sens=2, wyświetla <-- wymiana bez dwóch ostatnich znaków rclf
    if sens == 1:
        print(f"--> [{echange}]")
        return
    elif sens == 2:
        l = len(echange)
        print(f"<-- [{echange[0:l - 2]}]")
        return


# główna ----------------------------------------------------------------

# klient SMTP (protokół transferu SendMail) umożliwiający wysyłanie wiadomości
# dane pobierane są z pliku konfiguracyjnego zawierającego następujące informacje dla każdego serwera

# opis: opis wysyłanej wiadomości e-mail
# smtp-server: serwer SMTP
# smtp-port: port serwera SMTP
# from: nadawca
# do: odbiorca
# subject: temat wiadomości e-mail
# treść: treść wiadomości e-mail


# protokół komunikacji klient-serwer SMTP
# -> klient łączy się z serwerem SMTP na porcie 25
# <- serwer wysyła mu wiadomość powitalną
# -> klient wysyła polecenie EHLO: nazwa swojego komputera
# <- serwer odpowiada OK lub nie
# -> klient wysyła polecenie mail from: <nadawca>
# <- serwer odpowiada OK lub nie
# -> klient wysyła polecenie rcpt to: <odbiorca>
# <- serwer odpowiada OK lub nie
# -> klient wysyła polecenie data
# <- serwer odpowiada OK lub nie
# -> klient wysyła wszystkie wiersze swojej wiadomości i kończy wierszem zawierającym tylko jeden znak .
# <- serwer odpowiada OK lub nie
# -> klient wysyła polecenie quit
# <- serwer odpowiada OK lub nie

# odpowiedzi serwera mają postać xxx tekst, gdzie xxx to trzycyfrowa liczba. Każda liczba xxx >=500
# oznacza błąd. Odpowiedź może składać się z kilku wierszy, z których wszystkie oprócz ostatniego zaczynają się od xxx-
# w postaci xxx (spacja)

# wymieniane wiersze tekstu muszą kończyć się znakami RC(#13) i LF(#10)

# konfiguracja aplikacji
import config
config = config.configure()

# wiadomości e-mail są przetwarzane pojedynczo
for mail in config['mails']:
    try:
        # logi
        print("----------------------------------")
        print(f"Envoi du message [{mail['description']}]")
        # przygotowanie wiadomości do wysłania
        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']}"
        # wysyłanie wiadomości w trybie szczegółowym
        sendmail(mail, True)
        # koniec
        print("Message envoyé...")
    except BaseException as erreur:
        # wyświetlanie błędu
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
    # następna wiadomość e-mail

Uwagi

  • wiersze 134–136: konfiguracja aplikacji;
  • wiersze 139–151: taryfikuje się wszystkie wiadomości e-mail znalezione w konfiguracji;
  • wiersze 141–143: wyświetla się, co zostanie wykonane;
  • wiersze 144–149: definiuje się wiadomość do wysłania. Wiadomość [message] jest poprzedzona nagłówkami [From, To, Subject, Content-type];
  • wiersz 151: wysłanie wiadomości e-mail zapewnia funkcja [sendmail], która przyjmuje dwa parametry:
    • [mail]: słownik zawierający informacje niezbędne do wysłania wiadomości e-mail;
    • [verbose]: wartość logiczna wskazująca, czy komunikacja między klientem a serwerem ma być rejestrowana w konsoli;
  • wiersze 154–156: przechwytujemy wszystkie wyjątki generowane przez funkcję [sendmail]. Są one wyświetlane;
  • wiersz 6: [mail] to słownik opisujący wiadomość e-mail, która ma zostać wysłana;
  • wiersz 14: w protokole SMTP klient musi przesłać swoją nazwę. W tym miejscu pobierana jest nazwa lokalnego komputera, który będzie pełnił rolę klienta;
  • wiersz 16: nawiązywanie połączenia z serwerem SMTP, do którego zostanie wysłana wiadomość;
  • wiersze 22–23: jeśli nawiązano połączenie z serwerem SMTP, serwer ten wyśle wiadomość powitalną, którą odczytujemy w tym miejscu;
  • funkcja [sendmail] wysyła następnie różne polecenia, które musi wysłać klient SMTP:
    • wiersze 24–25: polecenie EHLO;
    • wiersze 26–27: polecenie MAIL FROM: ;
    • wiersze 28–29: polecenie RCPT TO: ;
    • wiersze 30–31: polecenie DATA;
    • wiersze 32–41: wysłanie wiadomości (From, To, Subject, Content-type, tekst);
    • wiersze 42–43: wysłanie znaku końca;
    • wiersze 44–457: polecenie QUIT, które kończy dialog klienta z serwerem SMTP;
  • wykonanie [sendmail] odbywa się w ramach [try / finally], które przekazuje wszystkie wyjątki do kodu wywołującego. Wiadomo, że ten ostatni zatrzymuje je wszystkie w celu ich wyświetlenia;
  • wiersze 48–50: zwolnienie zasobów;
  • wiersz 54: funkcja [send_command] odpowiada za wysyłanie poleceń klienta do serwera SMTP. Przyjmuje cztery parametry:
    • [connexion]: połączenie łączące klienta z serwerem;
    • [commande]: polecenie do wysłania;
    • [verbose]: jeśli TRUE, to komunikacja między klientem a serwerem jest rejestrowana w konsoli;
    • [with_rclf]: jeśli TRUE, wysyła polecenie zakończone sekwencją \r\n. Jest to wymagane w przypadku wszystkich poleceń protokołu SMTP, ale [send_command] służy również do wysyłania komunikatu. W tym przypadku nie dodaje się sekwencji \r\n;
  • wiersz 62: polecenie jest wysyłane tylko wtedy, gdy nie jest puste;
  • wiersze 65–66: polecenie jest wysyłane do serwera w postaci ciągu bajtów UTF-8;
  • wiersze 70–71: odczyt wszystkich wierszy odpowiedzi. Zakłada się, że ma ona mniej niż 1000 znaków. Odpowiedź może składać się z kilku wierszy. Każdy wiersz ma postać XXX-YYY, gdzie XXX jest kodem numerycznym, z wyjątkiem ostatniego wiersza odpowiedzi, który ma postać XXX YYY (brak znaku -);
  • wiersz 76: odczyt kodu błędu XXX z pierwszego wiersza;
  • wiersze 78–80: jeśli kod numeryczny XXX jest większy niż 500, oznacza to, że serwer zwrócił błąd. Wówczas generowany jest wyjątek;

Wyniki

Wykonanie skryptu daje następujące wyniki w konsoli:


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
  • wiersze 3–30: korzystanie z serwera SMTP [hMailServer] w celu wysłania wiadomości e-mail do [guest@localhost] przebiega pomyślnie;
  • wiersze 32–46: korzystanie z serwerów SMTP i [smtp.gmail.com] w celu wysłania wiadomości e-mail do serwera [pymailparlexemple@gmail.com] nie przebiega prawidłowo: w wierszu 45 serwer SMTP wysyła kod błędu 530 wraz z komunikatem o błędzie. Komunikat ten wskazuje, że klient SMTP musi najpierw uwierzytelnić się za pośrednictwem bezpiecznego połączenia. Nasz klient tego nie zrobił i w związku z tym został odrzucony;

Wyniki w programie Thunderbird są następujące:

Image

Image

Poprzedni klient ma co najmniej dwie wady:

  1. nie potrafi korzystać z bezpiecznego połączenia, jeśli serwer tego wymaga;
  1. nie potrafi dołączać załączników do wiadomości;

Pierwszą z tych niedoskonałości zajmiemy się w skrypcie [smtp/02]. W naszym nowym skrypcie wykorzystamy moduł Pythona [smtplib].

Skrypt [smtp/02/main] będzie korzystał z następującego pliku konfiguracyjnego jSON [smtp/02/config]:


def configure() -> dict:
    return {
        # opis: opis wysłanej wiadomości e-mail
        # serwer SMTP: serwer SMTP
        # smtp-port: port serwera SMTP
        # from: nadawca
        # do: odbiorca
        # subject: temat wiadomości e-mail
        # treść: treść wiadomości 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",
                # testujemy znaki z akcentami
                "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",
                # testujemy znaki z akcentami
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP z uwierzytelnianiem
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlh@1QZ3TG",
            }
        ]
    }

Znajdują się tu te same pozycje, co w pliku [smtp/01/config], z dwoma dodatkowymi pozycjami, gdy serwer SMTP wymaga uwierzytelnienia:

  • wiersz 31, [user]: nazwa użytkownika uwierzytelniającego połączenie;
  • wiersz 32, [password]: jego hasło;

Te dwa pola są obecne tylko wtedy, gdy serwer SMTP, z którym nawiązano połączenie, wymaga uwierzytelnienia. Uwierzytelnianie odbywa się wówczas za pośrednictwem bezpiecznego połączenia.

Kod skryptu [smtp/02/main.py] jest następujący:


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


# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: True):
    # wysyła wiadomość na serwer SMTP smtpserver w imieniu nadawcy
    # dla odbiorcy. Jeśli verbose=True, śledzi wymianę danych między klientem a serwerem

    # wykorzystuje się bibliotekę smtplib
    # pozwala na przekazywanie wyjątków
    #
    # serwer SMTP
    server = smtplib.SMTP(mail["smtp-server"])
    # tryb verbose
    server.set_debuglevel(verbose)
    # bezpieczne połączenie?
    if "user" in mail:
        # bezpieczne połączenie
        server.starttls()
        # EHLO polecenie + uwierzytelnianie
        server.login(mail["user"], mail["password"])

   # tworzenie wiadomości wieloczęściowej – to właśnie ta wiadomość wieloczęściowa zostanie wysłana
    msg = MIMEText(mail["message"])
    msg['from'] = mail["from"]
    msg['to'] = mail["to"]
    msg['date'] = formatdate(localtime=True)
    msg['subject'] = mail["subject"]
    # wysyłamy komunikat
    server.send_message(msg)
    # zamykanie połączenia
    server.quit()


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

# informacje są pobierane z pliku konfiguracyjnego zawierającego następujące dane dla każdego serwera

# opis: opis wysyłanej wiadomości e-mail
# serwer SMTP: serwer SMTP
# smtp-port: port serwera SMTP
# from: nadawca
# do: odbiorca
# subject: temat wiadomości
# content-type: kodowanie wiadomości
# treść: treść wiadomości e-mail


# konfiguracja aplikacji
import config
config = config.configure()

# wiadomości e-mail są przetwarzane pojedynczo
for mail in config['mails']:
    try:
        # logi
        print("----------------------------------")
        print(f"Envoi du message [{mail['description']}]")
        # wysyłanie wiadomości w trybie szczegółowym
        sendmail(mail, True)
        # koniec
        print("Message envoyé...")
    except BaseException as erreur:
        # wyświetlanie błędu
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
    # następna wiadomość e-mail

Komentarze

  • wiersze 8–35: wykorzystywana jest wyłącznie funkcja [sendmail]. Od tej pory będzie ona korzystać z modułu [smtplib] (wiersz 2);
  • wiersz 16: połączenie z serwerem SMTP;
  • wiersz 18: jeśli [verbose=True], komunikacja między klientem a serwerem będzie wyświetlana na konsoli;
  • wiersze 20–24: przeprowadzane jest ewentualne uwierzytelnianie, jeśli wymaga tego serwer SMTP;
  • wiersz 22: uwierzytelnianie odbywa się za pośrednictwem bezpiecznego połączenia;
  • wiersz 24: uwierzytelnianie;
  • wiersze 26–33: wysyłanie komunikatu. Następnie rozpocznie się dialog ze skryptem [smtp/01/main]. Jeśli nastąpiło uwierzytelnienie, odbędzie się ono w ramach bezpiecznego połączenia;
  • wiersz 35: zakończenie dialogu klient–serwer;

Przed uruchomieniem skryptu [smtp/02/main] należy zmodyfikować konfigurację konta Gmail [pymailparlexemple@gmail.com]:

  • zaloguj się na konto Gmail o nazwie [pymailparlexemple@gmail.com];
  • zmień następujące ustawienia: Image
  • w [2] zezwól mniej bezpiecznym aplikacjom na dostęp do konta;

Zrób to samo z drugim kontem Gmail [pymail2parlexemple@gmail.com].

Wyniki

Po uruchomieniu skryptu [smtp/02/main] otrzymujemy następujące wyniki w konsoli:


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
  • wiersz 40: klient [smtplib] rozpoczyna dialog w celu nawiązania szyfrowanego połączenia z serwerem SMTP, czego nie udało się osiągnąć w skrypcie [smtp/main/01];
  • poza tym widzimy znane polecenia protokołu SMTP;

Jeśli zajrzymy na konto Gmail użytkownika [pymail2parlexemple], zobaczymy następujące informacje:

Image

21.5.8. skrypty [smtp/03]: obsługa załączników

Uzupełniamy skrypt [smtp/02/main], aby wysyłana wiadomość mogła zawierać załączniki.

Image

Skrypt [smtp/03/main] jest konfigurowany przez następujący skrypt [smtp/03/config]:


import os


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

    return {
        # opis: opis wysłanej wiadomości e-mail
        # serwer SMTP: serwer SMTP
        # smtp-port: port serwera SMTP
        # from: nadawca
        # do: odbiorca
        # subject: temat wiadomości e-mail
        # treść: treść wiadomości 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",
                # testujemy znaki z akcentami
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP z uwierzytelnianiem
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # tutaj należy podać bezwzględne ścieżki do załączonych plików
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                ]
            }
        ]
    }

Plik [smtp/03/config] różni się od używanego wcześniej pliku [smtp/02/config] jedynie opcjonalną obecnością listy [attachments] (wiersze 30–32), która określa listę plików do załączenia do wysyłanej wiadomości.

Skrypt [smtp/03/main] ma następującą postać:


# importy
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):
    # wysyła wiadomość mail[message] na serwer SMTP mail[smtp-server] w imieniu mail[from]
    # dla mail[to]. Jeśli verbose=True, śledzi wymianę danych między klientem a serwerem

    # używana jest biblioteka smtplib
    # pozwala na przekazywanie wyjątków
    #
    # serwer SMTP
    server = smtplib.SMTP(mail["smtp-server"])
    # tryb verbose
    server.set_debuglevel(verbose)
    # połączenie zabezpieczone?
    if "user" in mail:
        server.starttls()
        server.login(mail["user"], mail["password"])

    # tworzenie komunikatu wieloczęściowego – to właśnie ten komunikat zostanie wysłany
    # źródło: 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"]
    # dołączamy wiadomość tekstową w formacie MIMEText
    msg.attach(MIMEText(mail["message"]))
    # przeglądamy załączniki
    for path in mail["attachments"]:
        # ścieżka musi być ścieżką bezwzględną
        # odgaduje się typ załączonego pliku
        ctype, encoding = mimetypes.guess_type(path)
        # jeśli nie udało się odgadnąć
        if ctype is None or encoding is not None:
            # Nie udało się odgadnąć typu pliku lub plik jest zakodowany (skompresowany), więc
            # używa się ogólnego typu „bag-of-bits”.
            ctype = 'application/octet-stream'
        # typ dzieli się na typ główny i podtyp
        maintype, subtype = ctype.split('/', 1)
        # rozpatrujemy różne przypadki
        if maintype == 'text':
            with open(path) as fp:
                # Uwaga: należy zająć się obliczaniem zestawu znaków
                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)
        # przypadek typu wiadomość / rfc822
        elif maintype == 'message':
            with open(path, 'rb') as fp:
                part = MIMEMessage(email.message_from_bytes(fp.read()))
        else:
            # inne przypadki
            with open(path, 'rb') as fp:
                part = MIMEBase(maintype, subtype)
                part.set_payload(fp.read())
            # Zakoduj ładunek przy użyciu Base64
            encoders.encode_base64(part)
        # Ustaw parametr nazwy pliku
        basename = os.path.basename(path)
        part.add_header('Content-Disposition', 'attachment', filename=basename)
        # dołączamy plik do wysyłanej wiadomości
        msg.attach(part)
    # wszystkie załączniki zostały dodane – wysyłamy wiadomość jako ciąg znaków
    server.send_message(msg)


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

..

Komentarze

  • wiersze 18–32: funkcja [sendmail] pozostaje taka sama, jak w przypadku braku załączników;
  • wiersz 35: poniższy kod pochodzi z oficjalnej dokumentacji języka Python;
  • wiersz 36: wiadomość, która zostanie wysłana, będzie składać się z kilku części: tekstu i załączników. Nazywa się to wiadomością [Multipart];
  • wiersze 37–40: w wiadomości [Multipart] znajdują się standardowe pola każdej wiadomości e-mail;
  • wiersz 42: różne części wiadomości [Multipart] [msg] są dołączane do wiadomości za pomocą metody [msg.attach] (wiersz 81). Załączone części mogą mieć dowolny charakter. Są one charakteryzowane przez typ MIME. Typem zwykłego tekstu o typie MIME jest typ [MIMEText];
  • wiersze 44–81: do wiadomości o typie [msg Multipart] dołączymy wszystkie załączniki wiadomości, która ma zostać wysłana (wiersz 81);
  • wiersz 44: [path] oznacza bezwzględną ścieżkę do pliku, który ma zostać załączony;
  • wiersz 47: aby znaleźć typ MIME, który ma zostać użyty dla załączanej części, wykorzystamy rozszerzenie (.docx, .php…) pliku, który ma zostać załączony. Zadanie to realizuje metoda [mimetypes.guess_type]. Zwraca ona dwie informacje:
    • [ctype]: typ pliku MIME;
    • [encoding]: informację o jego kodowaniu;
  • wiersze 49–52: w przypadku, gdy nie można określić typu pliku MIME, uznaje się go za plik binarny (wiersz 52);
  • wiersz 54: typ pliku MIME dzieli się na typ główny i typ drugorzędny, na przykład [application/pdf]. Te dwa elementy są rozdzielane;
  • wiersze 56–76: rozpatruje się różne przypadki w zależności od wartości typu głównego MIME. Na przykład w przypadku typu [application/pdf] dla pliku PDF wykonuje się wiersze 70–76:
    • wiersze 56–59: przypadek, w którym załączony plik jest plikiem tekstowym. W tym przypadku tworzy się element typu [MIMEText] o zawartości [fp.read];
    • wiersze 60–62: przypadek, w którym plik zawiera obraz. W tym przypadku tworzy się element typu [MIMEImage] o zawartości [fp.read];
    • wiersze 63–65: sytuacja, w której plik jest plikiem audio. W takim przypadku tworzy się element typu [MIMEAudio] o zawartości [fp.read];
    • wiersze 66–69: przypadek, w którym plik jest wiadomością e-mail. W takim przypadku tworzy się element typu [MIMEMessage] (wiersz 69) o zawartości [email.message_from_bytes(fp.read())]. W przeciwieństwie do poprzednich przypadków, w których zawartość elementu MIME stanowiła zawartość binarną powiązanego pliku, tutaj zawartość elementu MIMEMessage jest typu [email.message.Message];
    • wiersze 70–76: pozostałe przypadki. Obejmują one na przykład pliki Worda oraz PDF z naszego przykładu;
  • wiersz 72: plik do załączenia jest otwierany w trybie binarnym (rb=read binary);
  • wiersz 74: [fp.read] odczytuje cały plik binarny;
  • wiersze 72–74: struktura [with open(…) as file] wykonuje dwie czynności:
    • otwiera plik i przypisuje mu deskryptor [file];
    • zapewnia, że po zakończeniu działania [with], niezależnie od tego, czy wystąpi błąd, czy nie, deskryptor [file] zostanie zamknięty. Jest to zatem alternatywa dla struktury [try file=open(…)/ finally];
  • wiersz 73: tworzy się nowy element [part], który ma zostać włączony do komunikatu Multipart. Wykorzystuje się tu klasę [MIMEBase] i przekazuje do konstruktora elementy [maintype, subtype] określone w wierszu 54;
  • wiersz 74: element, który ma zostać włączony do wiadomości typu Multipart, musi mieć zawartość. Można ją zainicjować za pomocą metody [set_payload];
  • wiersze 75–76: załączone pliki muszą zostać zakodowane w 7-bitowym kodowaniu. Wynika to z faktu, że historycznie niektóre serwery SMTP obsługiwały wyłącznie znaki zakodowane w 7 bitach. W tym przypadku stosowane jest kodowanie o nazwie „Base64”;
  • wiersz 77: od tego wiersza przetwarzanie dotyczy wszystkich typów MIME, które utworzyliśmy w wierszach 56–76 [MIMEMessage, MIMEImage, MIMEAudio, MIMEBase, MIMEText];
  • wiersz 79: element, który ma zostać dodany do wiadomości typu Multipart, posiada nagłówek go opisujący. W tym miejscu wskazujemy, że dodawany element odpowiada załącznikowi. Nazwa tego pliku jest trzecim parametrem przekazywanym do metody [add_header]. Nazwa tego pliku jest często wykorzystywana przez programy pocztowe do zapisania załączonego pliku pod tą nazwą w systemie plików użytkownika. Do tej pory pracowaliśmy z bezwzględną nazwą załączonego pliku. Tutaj przekazujemy po prostu jego nazwę bez ścieżki (wiersz 78);
  • wiersz 81: plik binarny jest wbudowywany w wiadomość [msg Multipart];
  • wiersz 83: gdy wszystkie części wiadomości zostaną dołączone do pliku [msg Multipart], plik ten jest wysyłany;

Wyniki

Jeśli uruchomimy skrypt [smtp/03/main] z plikiem [smtp/02/config], który już przedstawiliśmy, konto [pymail2parlexemple@gmail.com] otrzyma następującą wiadomość:

Image

W pliku [4, 9-11] widoczne są załączniki.

Przedstawmy teraz przykład z załącznikiem w wiadomości e-mail. Zapiszemy otrzymaną wiadomość e-mail w powyższym pliku [3]:

Image

Zapisujemy wiadomość e-mail pod nazwą [mail attaché 1.eml] w folderze [smtp/03/attachments].

Teraz modyfikujemy plik [smtp/03/config] w następujący sposób:


import os


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

    return {
        # opis: opis wysłanej wiadomości e-mail
        # serwer SMTP: serwer SMTP
        # smtp-port: port serwera SMTP
        # from: nadawca
        # do: odbiorca
        # subject: temat wiadomości e-mail
        # treść: treść wiadomości 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",
                # testujemy znaki z akcentami
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP z uwierzytelnianiem
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # w tym miejscu należy podać bezwzględne ścieżki do załączonych plików
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                    f"{script_dir}/attachments/mail attaché 1.eml",
                ]
            }
        ]
    }
  • w wierszu 33 dodaliśmy załącznik;

Teraz ponownie uruchamiamy skrypt [smtp/03/main]. Daje to następujący wynik w skrzynce pocztowej użytkownika [pymail2parlexemple@gmail.com]:

Image

  • w [1] – otrzymana wiadomość e-mail;
  • w [2]: treść wiadomości;
  • w [3]: tekst wiadomości e-mail w załączniku;
  • w pliku [4]: program Thunderbird wykrył 5 załączników:
    • [fichier attaché.docx];
    • [fichier attaché.pdf];
    • [mail attaché 1.eml]. Ten załącznik jest sam w sobie wiadomością e-mail zawierającą dwa załączniki:
      • [fichier attaché.docx];
      • [fichier attaché.pdf];

21.6. Protokół POP3

21.6.1. Wprowadzenie

Do odczytywania wiadomości e-mail przechowywanych na serwerze pocztowym służą dwa protokoły:

  • protokół POP3 (Post Office Protocol) – historycznie pierwszy protokół, ale obecnie rzadko stosowany;
  • protokół IMAP (Internet Message Access Protocol) – nowszy od POP3 i obecnie najczęściej stosowany;

Aby zapoznać się z protokołem POP3, wykorzystamy następującą architekturę:

Image

  • [Serveur B] będzie, w zależności od sytuacji:
    • lokalnym serwerem POP3, zaimplementowanym przez serwer pocztowy [hMailServer];
    • serwerem [pop.gmail.com], który jest serwerem POP3 menedżera poczty [gmail.com];
  • [Client A] będzie klientem POP3 w różnych postaciach:
    • klient [RawTcpClient] służący do rozpoznawania protokołu POP3;
    • skrypt w języku Python odtwarzający protokół POP3 klienta [RawTcpClient];
    • skrypt w języku Python wykorzystujący moduły Python do obsługi załączników oraz korzystania z szyfrowanego i uwierzytelnionego połączenia, gdy wymaga tego serwer POP3;

21.6.2. Omówienie protokołu POP3

Podobnie jak w przypadku protokołu SMTP, przeanalizujemy protokół POP3 na podstawie logów serwera pocztowego [hMailServer]. W tym celu należy uruchomić ten serwer.

Korzystając z programu Thunderbird, wykonamy następujące czynności:

  • wyślemy wiadomość e-mail do użytkownika [guest@localhost.com];
  • przeczytamy skrzynkę pocztową tego użytkownika;

Image

Image

W powyższym przykładzie [3-6] przedstawia wiadomość otrzymaną przez użytkownika [guest@localhost.com].

Teraz sprawdzamy logi serwera [hMailServer]. W tym celu korzystamy z narzędzia administracyjnego [hMailServer Administrator]:

Image

Logi serwera POP3 przedstawiają się następująco (ostatnie wiersze w dzisiejszym pliku logów):


"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..."
  • wiersz 1: serwer POP3 wysyła wiadomość powitalną do klienta (Thunderbird), który właśnie się połączył;
  • wiersz 2: klient wysyła polecenie [CAPA] (capabilities), aby uzyskać listę poleceń, z których może korzystać;
  • wiersz 3: serwer odpowiada, że klient może używać poleceń [USER, UIDL, TOP]. Serwer POP rozpoczyna swoje odpowiedzi od [+OK] lub [-ERR], aby wskazać, czy wykonanie polecenia klienta zakończyło się powodzeniem, czy nie;
  • wiersz 4: klient wysyła polecenie [USER guest], aby wskazać, że chce przejrzeć skrzynkę pocztową użytkownika [guest];
  • wiersz 5: serwer odpowiada mu [+OK] i prosi o hasło użytkownika [guest];
  • wiersz 6: klient wysyła polecenie [PASS password], aby przesłać hasło użytkownika [guest]. W tym przypadku hasło jest przesyłane w postaci niezaszyfrowanej, ponieważ serwer POP3 nie wymusił bezpiecznego połączenia. Zobaczymy, że w przypadku serwera POP3 należącego do Gmaila będzie inaczej;
  • wiersz 7: serwer zweryfikował kombinację nazwy użytkownika i hasła. Informuje, że blokuje skrzynkę pocztową użytkownika [guest];
  • wiersz 8: klient wysyła do niego polecenie [STAT], które żąda informacji o skrzynce pocztowej;
  • wiersz 9: serwer odpowiada, że znajduje się tam wiadomość o rozmiarze 612 bajtów. Zasadniczo serwer informuje, że znajduje się tam N wiadomości i podaje ich łączny rozmiar;
  • wiersz 10: klient wysyła polecenie [LIST]. Polecenie to służy do uzyskania listy wiadomości;
  • wiersz 11: serwer wysyła mu listę wiadomości w następującej formie:
    • wiersz podsumowujący z liczbą komunikatów i ich łączną wielkością;
    • po jednym wierszu na każdą wiadomość, zawierającym numer wiadomości i jej rozmiar;
  • wiersz 13: klient wysyła polecenie [UIDL], które żąda listy wiadomości wraz z ich identyfikatorami. Każda wiadomość jest bowiem oznaczona unikalnym numerem w ramach serwisu pocztowego;
  • wiersz 14: odpowiedź serwera. Widzimy zatem, że wiadomość nr 1 na liście ma identyfikator 42;
  • wiersz 15: klient wysyła polecenie [RETR 1], prosząc o przesłanie mu wiadomości nr 1 z listy;
  • wiersz 16: serwer POP3 wykonuje to polecenie;
  • wiersz 17: klient wysyła polecenie [QUIT], aby poinformować, że zamierza rozłączyć się z serwerem POP3;
  • wiersz 18: serwer również zamknie połączenie z klientem, ale wcześniej wysyła mu wiadomość pożegnalną;

Teraz odtworzymy elementy powyższego dialogu, korzystając z klienta [RawTcpClient] uruchomionego w oknie PyCharm:

Image

Dialog wygląda następująco:


(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
  • wiersz 1: nawiązujemy połączenie z portem 110 maszyny [localhost]. To właśnie tam działa usługa POP3 z [hMailServer];
  • w wierszach 5, 7, 9, 13, 34 używamy poleceń [USER, PASS, LIST, RETR, QUIT];
  • wiersz 4: komunikat powitalny serwera POP3;
  • wiersz 5: wskazujemy, że chcemy uzyskać dostęp do skrzynki pocztowej użytkownika [guest];
  • wiersz 7: wysyłamy hasło użytkownika [guest] w postaci niezaszyfrowanej;
  • wiersz 9: żądamy listy wiadomości ze skrzynki pocztowej;
  • wiersz 13: żądamy wiadomości nr 1;
  • wiersze 14–33: serwer POP3 wysyła wiadomość nr 1;
  • wiersz 34: kończy się sesja;

Oto podsumowanie kilku popularnych poleceń obsługiwanych przez serwer POP3:

  • polecenie [USER] służy do określenia użytkownika, którego skrzynkę pocztową chcemy odczytać;
  • polecenie [PASS] służy do określenia hasła;
  • polecenie [LIST] wyświetla listę wiadomości znajdujących się w skrzynce pocztowej użytkownika;
  • polecenie [RETR] wyświetla wiadomość o podanym numerze;
  • polecenie [DELE] powoduje usunięcie wiadomości o podanym numerze;
  • polecenie [QUIT] informuje serwer o zakończeniu operacji;

Odpowiedź serwera może przybrać kilka form:

  • pojedynczy wiersz zaczynający się od [+OK], wskazujący, że poprzednie polecenie klienta zakończyło się powodzeniem;
  • pojedynczy wiersz zaczynający się od [-ERR], wskazujący, że poprzednie polecenie klienta zakończyło się niepowodzeniem;
  • kilka wierszy, gdzie:
    • pierwszy wiersz zaczyna się od [+OK];
    • ostatni wiersz składa się z pojedynczej kropki;

21.6.3. skrypty [pop3/01]: podstawowy klient POP3

Image

Ponieważ protokół POP3 ma taką samą strukturę jak protokół SMTP, skrypt [pop3/01/main.py] jest adaptacją skryptu [smtp/01/main.py]. Będzie on posiadał następujący plik konfiguracyjny [pop3/01/config.py]:


def configure() -> dict:
    # skrzynki pocztowe, z których pobierane są wiadomości
    mailboxes = [
        # serwer: serwer POP3
        # port: port serwera POP3
        # użytkownik: użytkownik, którego wiadomości chcemy przeczytać
        # hasło: jego hasło
        # maxmails: maksymalna liczba wiadomości do pobrania
        # timeout: maksymalny czas oczekiwania na odpowiedź serwera
        # encoding: kodowanie otrzymanych wiadomości e-mail
        # delete: jeśli wartość wynosi „True”, wiadomości są usuwane ze skrzynki pocztowej
        # po ich pobraniu na komputer

        {
            "server": "localhost",
            "port": "110",
            "user": "guest",
            "password": "guest",
            "maxmails": 10,
            "timeout": 1.0,
            "encoding": "utf-8",
            "delete": False
        }
    ]
    # przywracamy konfigurację
    return {
        "mailboxes": mailboxes
    }
  • wiersze 3–24: lista skrzynek pocztowych do sprawdzenia. W tym przypadku jest tylko jedna;
  • wiersze 4–12: znaczenia elementów słownika definiujących każdą ze skrzynek pocztowych;
  • wiersz 15: serwer POP3, do którego kierowane jest zapytanie, to lokalny serwer [hMailServer];
  • wiersze 17–18: chcemy odczytać skrzynkę pocztową użytkownika [guest@localhost];
  • wiersz 19: odczytanych zostanie maksymalnie 10 wiadomości e-mail;
  • wiersz 20: klient będzie czekał na odpowiedź serwera maksymalnie 1 sekundę;
  • wiersz 21: typ kodowania odczytywanych wiadomości;
  • wiersz 22: pobrane wiadomości nie będą usuwane;

Skrypt [pop3/01/main.py] wygląda następująco:


# importów
import re
import socket


# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
    # odczytuje skrzynkę pocztową opisaną przez słownik [mailbox]
    # jeśli verbose=True, śledzi wymianę danych między klientem a serwerem



# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
    # wysyła polecenie do kanału połączenia
    # tryb szczegółowy, jeśli verbose=True
    # jeśli with_rclf=True, dodaje sekwencję rclf do wymiany
    # zwraca pierwszy wiersz odpowiedzi



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


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

# klient POP3 (Post Office Protocol) umożliwiający odczytywanie wiadomości ze skrzynki pocztowej
# protokół komunikacyjny POP3 klient-serwer
# -> klient łączy się z serwerem SMTP na porcie 110
# <- serwer wysyła mu wiadomość powitalną
# -> klient wysyła polecenie USER użytkownik
# <- serwer odpowiada OK lub nie
# -> klient wysyła polecenie PASS mot_de_passe
# <- serwer odpowiada OK lub nie
# -> klient wysyła polecenie LIST
# <- serwer odpowiada OK lub nie
# -> klient wysyła polecenie RETR z numerem dla każdej wiadomości e-mail
# <- serwer odpowiada OK lub nie. Jeśli OK, wysyła treść żądanej wiadomości e-mail
# -> serwer wysyła wszystkie wiersze wiadomości e-mail i kończy wierszem zawierającym
# jedyny znak.
# -> klient wysyła polecenie DELE z numerem, aby usunąć wiadomość e-mail
# <- serwer odpowiada OK lub nie
# # -> klient wysyła polecenie QUIT w celu zakończenia dialogu z serwerem
# <- serwer odpowiada OK lub nie
# odpowiedzi serwera mają postać +OK tekst lub -ERR tekst
# Odpowiedź może składać się z kilku wierszy. W takim przypadku ostatni wiersz składa się z pojedynczego kropki
# wymieniane wiersze tekstu muszą kończyć się znakami RC(#13) i LF(#10)
# 

# pobieramy konfigurację aplikacji
import config
config = config.configure()

# przetwarzamy skrzynki pocztowe jedna po drugiej
for mailbox in config['mailboxes']:
    try:
        # wyświetlanie konsoli
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
        # odczyt skrzynki pocztowej w trybie szczegółowym
        readmails(mailbox, True)
        # koniec
        print("Lecture terminée...")
    except BaseException as erreur:
        # wyświetla błąd
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass

Komentarze

Jak już wspomnieliśmy, [pop3/01/main.py] jest adaptacją skryptu [smtp/01/main.py], który już omówiliśmy. Omówimy jedynie główne różnice:

  • wiersz 64: funkcja [readmails] odpowiada za odczytywanie wiadomości e-mail ze skrzynki pocztowej. Dane potrzebne do połączenia się z tą skrzynką znajdują się w słowniku [mailbox]. Drugim parametrem [True] jest parametr [Verbose], który w tym przypadku wymaga monitorowania wymiany danych między klientem a serwerem;

Funkcja [readmails] ma następujący opis:


# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
    # odczytuje wiadomości e-mail ze skrzynki opisanej w słowniku [mailbox]
    # jeśli verbose=True, śledzi komunikację między klientem a serwerem

    # wyodrębnia parametry skrzynki pocztowej
    # zakłada się, że słownik [mailbox] jest poprawny
    server = mailbox['server']
    port = int(mailbox['port'])
    user = mailbox['user']
    password = mailbox['password']
    maxmails = mailbox['maxmails']
    delete = mailbox['delete']
    timeout = mailbox['timeout']

    # zezwala się na zgłaszanie błędów systemowych
    connexion = None
    try:
        # nawiązywanie połączenia na porcie [port] serwera [server] z limitem czasu wynoszącym jedną sekundę
        connexion = socket.create_connection((server, port), timeout=timeout)

        # połączenie stanowi dwukierunkowy strumień komunikacji
        # między klientem (tym programem) a serwerem POP3, z którym nawiązano połączenie
        # kanał ten służy do wymiany poleceń i informacji

        # odczyt wiadomości powitalnej
        send_command(mailbox, connexion, "", verbose, True)
        # polecenie USER
        send_command(mailbox, connexion, f"USER {user}", verbose, True)
        # polecenie PASS
        send_command(mailbox, connexion, f"PASS {password}", verbose, True)
        # polecenie LIST
        première_ligne = send_command(mailbox, connexion, "LIST", verbose, True)
        # analiza pierwszego wiersza w celu ustalenia liczby komunikatów
        match = re.match(r"^\+OK (\d+)", première_ligne)
        nbmessages = int(match.groups()[0])
        # przechodzimy przez pętlę po wiadomościach
        imessage = 0
        while imessage < nbmessages and imessage < maxmails:
            # polecenie RETR
            send_command(mailbox, connexion, f"RETR {imessage + 1}", verbose, True)
            # polecenie DELE
            if delete:
                send_command(mailbox, connexion, f"DELE {imessage + 1}", verbose, True)
            # następna wiadomość
            imessage += 1
        # polecenie QUIT
        send_command(mailbox, connexion, "QUIT", verbose, True)
        # koniec
    finally:
        # zamknięcie połączenia
        if connexion:
            connexion.close()

Komentarze

  • wiersze 8–14: pobierane są informacje konfiguracyjne skrzynki pocztowej, która ma zostać sprawdzona;
  • wiersze 19–20: nawiązywanie połączenia z serwerem POP3;
  • wiersze 26–27: odczyt wiadomości powitalnej wysłanej przez serwer;
  • wiersze 28–29: wysyłamy polecenie [USER] w celu zidentyfikowania użytkownika, którego wiadomości e-mail chcemy pobrać;
  • wiersze 30–31: wysyłamy polecenie [PASS] w celu podania hasła tego użytkownika;
  • wiersze 32–33: wysyłamy polecenie [LIST], aby sprawdzić, ile wiadomości znajduje się w skrzynce pocztowej tego użytkownika. Funkcja [sendCommand] zwraca pierwszy wiersz odpowiedzi serwera. W tym wierszu serwer podaje liczbę wiadomości w skrzynce pocztowej;
  • wiersze 34–36: pobieramy liczbę wiadomości z pierwszego wiersza odpowiedzi;
  • wiersze 39–46: przechodzimy cyklicznie przez każdą wiadomość. Dla każdej z nich wysyłamy dwa polecenia:
    • RETR i: w celu pobrania wiadomości nr i (wiersze 40–41);
    • DELE i: w celu usunięcia wiadomości, jeśli konfiguracja wymaga, aby przeczytane wiadomości były usuwane z serwera (wiersze 43–44);
  • wiersze 47–48: wysyłamy polecenie [QUIT], aby poinformować serwer o zakończeniu operacji;

Funkcja [send_command] wygląda następująco:


# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
    # wysyła polecenie do kanału połączenia
    # tryb szczegółowy, jeśli verbose=True
    # jeśli with_rclf=True, dodaje sekwencję rclf do wymiany
    # zwraca pierwszy wiersz odpowiedzi

    # znak końca linii
    if with_rclf:
        rclf = "\r\n"
    else:
        rclf = ""
    # wysyła polecenie, jeśli nie jest puste
    if commande:
        connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
        # ewentualne echo
        if verbose:
            affiche(commande, 1)
    # odczyt gniazda tak, jakby było plikiem tekstowym
    encoding = f"{mailbox['encoding']}" if mailbox['encoding'] else None
    file = connexion.makefile(encoding=encoding)
    # plik ten jest przetwarzany wiersz po wierszu
    # odczyt pierwszego wiersza
    première_ligne = réponse = file.readline().strip()
    # tryb szczegółowy?
    if verbose:
        affiche(première_ligne, 2)
    # pobieranie kodu błędu
    code_erreur = réponse[0]
    if code_erreur == "-":
        # wystąpił błąd
        raise BaseException(réponse[5:])
    # szczególny przypadek odpowiedzi wielowierszowych LIST, RETR
    cmd = commande.lower()[0:4]
    if cmd == "list" or cmd == "retr":
        # ostatni wiersz odpowiedzi?
        dernière_ligne = False
        while not dernière_ligne:
            # odczyt kolejnego wiersza
            ligne_suivante = file.readline().strip()
            # tryb szczegółowy?
            if verbose:
                affiche(ligne_suivante, 2)
            # ostatni wiersz?
            dernière_ligne = ligne_suivante == "."
    # koniec – zwracamy pierwszy wiersz
    return première_ligne

Komentarze

  • wiersze 13–18: polecenie [command] jest wysyłane do serwera POP3 tylko wtedy, gdy nie jest puste. Jest to konieczne, aby odczytać komunikat powitalny serwera POP3, który wysyła go nawet wtedy, gdy klient nie wysłał jeszcze żadnych poleceń;
  • wiersze 19–21: odczytujemy gniazdo tak, jakby było plikiem tekstowym. Pozwoli nam to skorzystać z metody [readline] (wiersz 24) i w ten sposób odczytywać komunikat wiersz po wierszu. Używamy klucza [encoding] ze słownika [mailbox], aby określić kodowanie wierszy, które będą odczytywane;
  • wiersz 24: odczytujemy pierwszy wiersz odpowiedzi;
  • wiersze 28–32: obsługujemy przypadek ewentualnego błędu. Są one typu [-ERR invalid password, -ERR mailbox unknown, -ERR unable to lock mailbox…];
  • wiersz 32: generowany jest wyjątek z komunikatem o błędzie;
  • wiersz 35: tylko polecenia typu [list, retr] mogą mieć odpowiedzi składające się z wielu wierszy;
  • wiersze 36–45: w przypadku odpowiedzi wielowierszowej wyświetlane są wszystkie otrzymane wiersze (wiersze 42–43) aż do otrzymania ostatniego wiersza (wiersz 45);
  • wiersz 46: zwracamy pierwszy odczytany wiersz, ponieważ w przypadku polecenia [LIST] zawiera on liczbę wiadomości znajdujących się w skrzynce pocztowej;

Wyniki

Weźmy poprzedni przykład. Za pomocą programu Thunderbird wysłaliśmy następującą wiadomość do użytkownika [guest@localhost] (serwer hMailServer musi być uruchomiony):

Image

Po uruchomieniu otrzymujemy następujące wyniki:


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
  • wiersze 15–31: poprawnie odzyskujemy wiadomość wysłaną do [guest@localhost].

Mamy tu do czynienia z podstawowym klientem POP3, któremu brakuje pewnych funkcji:

  1. możliwość komunikacji z zabezpieczonym serwerem POP3;
  2. możliwość odczytu załączników do wiadomości;

Zaimplementujemy te dwie możliwości za pomocą nowego skryptu, który tym razem będzie bardziej złożony.

21.6.4. skrypty [pop3/02]: klient POP3 z modułami [poplib] i [email]

Napiszemy klienta POP3, który pozwoli zarządzać załącznikami oraz komunikacją z zabezpieczonymi serwerami. Ponadto będziemy zapisywać wiadomości wraz z załącznikami w plikach.

Wykorzystamy dwa moduły w języku Python:

  • [poplib]: który będzie obsługiwał protokół POP3;
  • [email]: obejmuje wiele podmodułów, które pozwolą nam analizować otrzymane komunikaty. Każdy komunikat to ustrukturyzowany ciąg znaków, w którym można znaleźć:
    • nagłówki wiadomości [From, To, Subject, Return-Path…];
    • treść wiadomości w wersji tekstowej oraz ewentualnie w formacie HTML;
    • załączniki;

Image

Skrypt [inet/pop3/02/main] [1] jest konfigurowany przez plik [inet/pop3/02/config] [2] i wykorzystuje moduł [inet/shared/mail_parser] [3].

Plik [pop3/02/config] ma następującą treść:


import os


def configure() -> dict:
    # konfiguracja aplikacji
    config = {
        # lista skrzynek pocztowych do zarządzania
        "mailboxes": [
            # serwer: serwer POP3
            # port: port serwera POP3
            # użytkownik: użytkownik, którego wiadomości chcemy przeczytać
            # hasło: jego hasło
            # maxmails: maksymalna liczba wiadomości do pobrania
            # timeout: maksymalny czas oczekiwania na odpowiedź serwera
            # delete: ustawić na „true”, jeśli pobrane wiadomości mają zostać usunięte z serwera
            # ssl: wartość „true”, jeśli odczyt wiadomości odbywa się za pośrednictwem bezpiecznego połączenia
            # output: folder, w którym będą zapisywane pobrane wiadomości

            {
                "server": "pop.gmail.com",
                "port": "995",
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                "maxmails": 10,
                "delete": False,
                "ssl": True,
                "timeout": 2.0,
                "output": "output"
            }
        ]
    }
    # absolutna ścieżka do folderu skryptu
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # absolutne ścieżki do folderów, które mają zostać uwzględnione w syspath
    absolute_dependencies = [
        # folder lokalny
        f"{script_dir}/../../shared",
   ]

    # konfiguracja syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # ustawiamy konfigurację
    return config

Plik ten definiuje listę skrzynek pocztowych, które mają być sprawdzane, oraz ustala ścieżkę Python Path aplikacji.

Znajduje się tu tylko jedna skrzynka pocztowa:

  • wiersze 22–23: użytkownik, którego wiadomości e-mail mają być odczytywane;
  • wiersze 20–21: nazwa i port serwera POP3, na którym przechowywane są wiadomości e-mail tego użytkownika;
  • wiersz 24: maksymalna liczba wiadomości do pobrania. Jeśli wypróbujesz ten skrypt na swojej skrzynce pocztowej, prawdopodobnie nie będziesz chciał pobierać setek wiadomości, które się w niej znajdują;
  • wiersz 25: wartość logiczna wskazująca, czy po odczytaniu wiadomości należy ją usunąć (delete=True);
  • wiersz 26: ustawienie atrybutu [ssl] na True oznacza, że serwer POP3 zdefiniowany w wierszach 20–21 korzysta z szyfrowanego połączenia;
  • wiersz 27: maksymalny czas oczekiwania na odpowiedzi serwera wyrażony w sekundach;
  • wiersz 28: folder, w którym mają być przechowywane przeczytane wiadomości e-mail. Zostanie on utworzony, jeśli jeszcze nie istnieje. Podano tutaj nazwę względną. Podczas wykonywania skryptu będzie ona odnosić się do folderu, z którego uruchamiasz skrypt. W przypadku skryptu [Pycharm] będzie to folder skryptu [pop3/02];

Skrypt [pop3/02/main] wygląda następująco:


# importy
import email
import os
import poplib
import shutil


# odczyt skrzynki pocztowej
def readmails(mailbox: dict, verbose: bool):
    # odczytuje skrzynkę pocztową opisaną przez słownik [mailbox]
    # jeśli verbose=True, śledzi wymianę danych między klientem a serwerem


# main ----------------------------------------------------------------
#  klient POP3 (Post Office Protocol) umożliwiający odczyt wiadomości e-mail

# pobieramy konfigurację aplikacji
import config
config = config.configure()

# przetwarzamy skrzynki pocztowe jedna po drugiej
for mailbox in config['mailboxes']:
    try:
        # wyświetlanie konsoli
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
        # odczyt skrzynki pocztowej w trybie szczegółowym
        readmails(mailbox, True)
        # koniec
        print("Lecture terminée...")
    except BaseException as erreur:
        # wyświetlanie błędu
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
  • wiersze 17–36: część skryptu o nazwie [main] jest analogiczna do tej w skrypcie [pop3/01];

Funkcja [readmails] ma następującą postać:


# odczyt skrzynki pocztowej
def readmails(mailbox: dict, verbose: bool):
    # odczytuje skrzynkę pocztową opisaną w słowniku [mailbox]
    # jeśli verbose=True, śledzi wymianę danych między klientem a serwerem

    # import z mail_parser
    from mail_parser import save_message

    # wyodrębnia parametry skrzynki pocztowej
    # zakłada się, że słownik [mailbox] jest poprawny
    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']

    # zezwala się na zgłaszanie błędów systemowych
    pop3 = None
    try:
        # tworzy się foldery do przechowywania danych, jeśli jeszcze nie istnieją
        if not os.path.isdir(output):
            os.mkdir(output)
        # użytkownik
        dir2 = f"{output}/{user}"
        # usuwamy folder [dir2], jeśli istnieje, a następnie tworzymy go ponownie
        if os.path.isdir(dir2):
            # usunięcie
            shutil.rmtree(dir2)
        # utworzenie
        os.mkdir(dir2)
        # nawiązanie połączenia na porcie [port] z [server]
        if ssl:
            pop3 = poplib.POP3_SSL(server, port, timeout=timeout)
        else:
            pop3 = poplib.POP3(server, port, timeout=timeout)

        # połączenie stanowi dwukierunkowy strumień komunikacji
        # między klientem (tym programem) a serwerem POP3, z którym nawiązano połączenie
        # kanał ten służy do wymiany poleceń i informacji

        # tryb szczegółowy
        pop3.set_debuglevel(2 if verbose else 0)
        # odczyt wiadomości powitalnej
        pop3.getwelcome(    )
        # polecenie USER
        réponse = pop3.user(user)
        # polecenie PASS
        réponse = pop3.pass_(password)
        # polecenie LIST
        liste = pop3.list()
        # wiadomości e-mail znajdują się na liście [1]
        imail = 0
        nb_mails = len(liste[1])
        fini = imail == maxmails or imail == nb_mails
        éléments = liste[1]
        while not fini:
            # bieżący element
            élément = éléments[imail]
            # element to lista bajtów, którą dekoduje się na ciąg znaków
            desc = élément.decode()
            # mamy ciąg znaków rozdzielony spacjami
            # pierwszy element to numer wiadomości
            num = desc.split()[0]
            # pobieramy wiadomość
            message = pop3.retr(int(num))
            # wiersze wiadomości znajdują się w zmiennej „message” o wartości [1]
            str_message = ""
            for ligne in message[1]:
                # wiersz to ciąg bajtów, który dekodujemy jako ciąg znaków
                str_message += f"{ligne.decode()}\r\n"
            # folder wiadomości
            dir3 = f"{dir2}/message_{num}"
            # jeśli folder nie istnieje, tworzymy go
            if not os.path.isdir(dir3):
                os.mkdir(dir3)
            # temat email.message.Message
            save_message(dir3, email.message_from_string(str_message), 0)
            # kolejny e-mail
            imail += 1
            # czy osiągnięto limit?
            fini = imail == maxmails or imail == nb_mails

        # zamówienie QUIT
        pop3.quit()
    finally:
        # zamknięcie połączenia
        if pop3:
            pop3.close()

Komentarze

  • wiersze 6–7: importowana jest funkcja [mail_parser.save_message] wykorzystywana w wierszu 80;
  • kod funkcji jest zamknięty w bloku try (wiersz 22) / finally (wiersz 88). Dzięki temu wszystkie wyjątki są przekazywane do kodu głównego, który je zatrzymuje i wyświetla;
  • wiersze 11–18: pobierane są informacje konfiguracyjne skrzynki pocztowej;
  • wiersze 23–33: wszystkie wiadomości będą przechowywane w folderze [output/user], gdzie [output] i [user] są zdefiniowane w konfiguracji. W związku z tym kolejno tworzymy foldery [output], a następnie [output/user]. Aby utworzyć ten ostatni, należy najpierw usunąć go z wiersza 31. [shutil] to moduł, który należy zaimportować. [shutil.rmtree(dir)] usuwa folder [dir] wraz z całą jego zawartością;
  • do wszystkich operacji na plikach systemowych używa się modułu [os], który również należy zaimportować;
  • wiersze 34–38: nawiązuje się połączenie z serwerem POP3. Jeśli serwer jest zabezpieczony, stosuje się klasę [poplib.POP3_SSL], w przeciwnym razie klasę [poplib.POP3]. Atrybut [ssl] użyty w wierszu 35 pochodzi z konfiguracji skrzynki pocztowej;
  • wiersz 45: ustawia się poziom logowania:
    • 0: brak logów;
    • 1: polecenia wysyłane przez klienta POP3 są rejestrowane;
    • 2: szczegółowe logi. Widoczne jest również to, co odbiera klient POP3;
  • wiersz 47: po nawiązaniu połączenia serwer POP3 wysyła wiadomość powitalną. Można ją przeczytać;
  • wiersze 48–49: polecenie USER protokołu POP3;
  • wiersze 50–51: polecenie PASS protokołu POP3;
  • wiersze 52–53: polecenie LIST protokołu POP3. Odpowiedź ma postać krotki (response, ['mesg_num octets'…], bajty), na przykład lista=(b'+OK 3 wiadomości (3859 bajtów)', [b'1 584', b'2 550', b'3 2725'], 22). Widać, że dwa pierwsze elementy krotki to bajty (prefiks b). lista[1] to tablica, w której każdy element jest ciągiem bajtów zawierającym dwie informacje: numer wiadomości i jej rozmiar w bajtach;
  • wiersz 56: z powyższego wynika, że liczbę wiadomości w skrzynce pocztowej można uzyskać za pomocą [email.message_from_bytes(data2[0][1])];
  • wiersze 59–84: przeprowadzana jest pętla dla każdej wiadomości. Pętla kończy się, gdy wszystkie wiadomości zostaną odczytane lub gdy osiągnięta zostanie maksymalna liczba wiadomości określona w konfiguracji;
  • wiersz 61: bieżący element tablicy liste[1], a więc coś w rodzaju „b'1 584'”, czyli ciąg bajtów;
  • wiersz 63: przekształcamy ciąg bajtów na ciąg znaków. Mamy teraz ciąg znaków '1 584';
  • wiersz 66: pobieramy numer wiadomości, w tym przypadku ciąg znaków „1”;
  • wiersz 68: wysyłamy polecenie POP3 RETR num. Otrzymujemy odpowiedź w stylu:

[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)]
  • (ciąg dalszy)
    • komunikat jest krotką składającą się z trzech elementów;
    • message[1] to tablica wierszy. Każdy wiersz to ciąg bajtów (prefiks b). Cała wiadomość składa się z tego zbioru wierszy;
    • [Return-Path, Received, To, Subject, Message-ID, Content-Type, Content-Transfer-Encoding, Content-Language] to nagłówki wiadomości. Każdy z nich zawiera informacje o otrzymanej wiadomości. Informacje te pozwolą na odzyskanie treści wiadomości (przedostatni element tablicy message[1]);
  • wiersze 71–73: tworzymy ciąg znaków [strMessage], składający się ze wszystkich wierszy wiadomości. Mamy teraz wiadomość w postaci ciągu znaków. Wiadomość ta może zawierać inne wiadomości, a także załączniki. Załączniki są bowiem dołączane w postaci ciągu znaków. Należy więc pamiętać, że wiadomość e-mail jest początkowo ciągiem znaków i to właśnie ten ciąg znaków należy przeanalizować, aby wyodrębnić załączniki, ewentualne inne zawarte w niej wiadomości oraz oczywiście treść wiadomości, czyli to, co napisał nadawca;
  • wiersze 74–78: umieścimy treść wiadomości i załączniki w folderze [dir3];
  • wiersze 79–80: analizę wiadomości zlecimy funkcji [save_message]:
    • pierwszym parametrem jest [dir3], czyli folder, w którym należy zapisać treść wiadomości;
    • drugim parametrem jest typ [email.message.Message]. Obiekt ten posiada metody służące do pobierania różnych części wiadomości (treść, załączniki) oraz wszystkich jej nagłówków. Aby uzyskać dostęp do tego obiektu, należy zaimportować moduł [email]. Funkcja [email.message_from_string] umożliwia utworzenie obiektu [email.message.Message] na podstawie ciągu znaków wiadomości;

Funkcja [save_message] jest częścią modułu [mail_parser]:

Image

Moduł [mail_parser] został zaimportowany do wierszy 6–7 funkcji [readmails];

W [mail_parser.py] funkcja [save_message] ma następującą postać:


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


# zapisanie wiadomości typu email.message.Message
# funkcję tę można wywoływać rekurencyjnie
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # wynik: folder do zapisywania wiadomości
    # email_message: wiadomość do zapisania
    # irfc822: bieżący numer w numeracji załączonych wiadomości e-mail
    #
    # część wiadomości
    part = email_message
    # nagłówki [From, To, Subject] znajdują się w jednej z części wieloczęściowych
    # lub w części [text/*], jeśli nie ma części [multipart]
    keys = part.keys()
    # Pole „From” musi znajdować się wśród nagłówków, w przeciwnym razie część ta nie zawiera poszukiwanych nagłówków
    if "From" in keys:
        # pobieramy niektóre nagłówki
        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'))}"]
        # zapis nagłówków w pliku tekstowym
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # zapis do pliku
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # typ pliku [part]
    main_type = part.get_content_maintype()

Uwagi

  • wiersz 12: funkcja przyjmuje maksymalnie trzy parametry:
  • [output]: folder, w którym ma zostać zapisana wiadomość (drugi parametr);
  • [email_message]: wiadomość typu [email.message.Message]. Ten typ jest typem strukturalnym. Zawiera tekst wiadomości e-mail oraz wszystkie załączone pliki i udostępnia metody do pobierania poszczególnych elementów;
  • [irfc822]: ten parametr służy do numerowania wiadomości e-mail zawartych w [email_message];
  • wiersz 18: obiekt [email_message] jest umieszczany w [part]. Typ [email.message.Message] zawiera części [part] (treść wiadomości, załączniki, wiadomości e-mail zawarte w obiekcie), które również mają typ [email.message.Message]. Każda część typu [part] może zawierać podczęści. W ten sposób typ [email.message.Message] stanowi drzewo elementów typu [email.message.Message]:
    • [part.ismultipart()] jest równe [True], jeśli część [part] zawiera podczęści. Są one wówczas dostępne poprzez [part.get_payload()];
    • gdy [part.ismultipart()] jest równe [False], oznacza to, że dotarliśmy do liścia drzewa wiadomości początkowej: może to być:
      • treści wiadomości w postaci zwykłego tekstu;
      • treści wiadomości w postaci tekstu HTML;
      • załącznika (z wyjątkiem wiadomości enkapsulowanej, dla której [part.ismultipart()] jest równe [True]);
  • ze względu na drzewiastą strukturę parametru [email.message.Message] funkcja [save_message] będzie wywoływana rekurencyjnie. Rekurencja kończy się po dotarciu do liści drzewa, tj. do części [part], dla której [part.ismultipart()] ma wartość [False];
  • wiersz 21: żądamy wyświetlenia kluczy (lub nagłówków) aktualnie analizowanej wiadomości (która ze względu na rekurencję może stanowić podczęść wiadomości początkowej);
  • wiersze 23–35: chcemy zapisać nagłówki:
    • [From]: nadawca wiadomości;
    • [To]: odbiorca wiadomości;
    • [Subject]: temat wiadomości;
    • [Return-Path]: adresat, do którego należy wysłać odpowiedź, jeśli chcemy na wiadomość odpowiedzieć. Informacja ta nie zawsze znajduje się bowiem w polu [From];
    • [User-Agent]: klient POP3, który komunikuje się z serwerem POP3;
    • [Date]: data wysłania wiadomości e-mail;
  • wiersz 23: tylko jedna z części wiadomości zawiera te nagłówki. W przypadku pozostałych części kod z wierszy 23–35 zostanie pominięty;
  • wiersze 25–30: tworzymy listę zawierającą sześć nagłówków;
  • wiersz 25: przeanalizujmy pierwszy nagłówek:
    • [part.get(key)] pozwala uzyskać nagłówek powiązany z kluczem [key];
    • ten nagłówek może być zakodowany. Jeśli kodowanie to nie jest utf-8, nagłówek jest dekodowany w celu ponownego zakodowania go w formacie utf-8 za pomocą funkcji [decode_header];
    • pierwszy nagłówek będzie miał postać [From: pymail2lexemple@gmail.com];
  • wiersze 31–35: nagłówki zapisuje się w pliku [output/headers.txt];

Funkcja [decode_header] wygląda następująco (nadal w pliku [mail_parser.py]):


# dekodowanie nagłówków
def decode_header(header: object) -> str:
    # dekodowanie nagłówka
    header = email.header.decode_header(f"{header}")
    # wynikiem jest tablica – w tym przypadku będzie zawierała tylko jeden element typu (nagłówek, kodowanie)
    # jeśli encoding==None, to header jest ciągiem znaków
    # w przeciwnym razie jest to lista bajtów zakodowanych zgodnie z encoding
    header, encoding = header[0]
    if not encoding:
        # jeśli nie ma kodowania
        return header
    else:
        # jeśli istnieje kodowanie, to następuje dekodowanie
        return header.decode(encoding)

Komentarze

  • wiersz 4: dekodujemy nagłówek:
    • należy zaimportować moduł [email.header];
    • otrzymujemy listę krotek [(header1,encoding1) , (header2, encoding2)…];
    • w przypadku nagłówków [From, To, Subject, Return-Path, Date] lista będzie zawierała tylko jeden element;
    • wiersz 8: pobieramy pojedynczy nagłówek i jego kodowanie:
      • jeśli [encoding==None], to [header] jest nagłówkiem w postaci ciągu znaków;
      • w przeciwnym razie [header] jest ciągiem bajtów reprezentującym zakodowany nagłówek;
  • wiersze 10–11: jeśli nie było kodowania, to zwracamy nagłówek;
  • wiersze 12–14: jeśli zastosowano kodowanie, to odkodowujemy uzyskany ciąg bajtów do postaci ciągu znaków i zwracamy go;

Wróćmy do funkcji [save_message]:


# zapisanie wiadomości typu email.message.Message
# funkcja ta może być wywoływana rekurencyjnie
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # wynik: folder do zapisywania wiadomości
    # email_message: wiadomość do zapisania
    # irfc822: bieżący numer w numeracji załączonych wiadomości e-mail
    #
    # część wiadomości
    part = email_message
    # nagłówki [From, To, Subject] znajdują się w jednej z części wieloczęściowych
    # lub w części [text/*], gdy nie ma części [multipart]
    keys = part.keys()
    # „From” musi znajdować się wśród nagłówków, w przeciwnym razie część ta nie zawiera poszukiwanych nagłówków
    if "From" in keys:
        # pobieramy niektóre nagłówki
        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'))}"]
        # zapis nagłówków w pliku tekstowym
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # zapis do pliku
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # typ części [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # jeśli wiadomość jest typu text/plain
    if type_of_part == "text/plain":
        # wiadomość tekstowa
        save_textmessage(output, part, 0)

    # jeśli wiadomość jest typu text/html
    elif type_of_part == "text/html":
        # wiadomość HTML
        save_textmessage(output, part, 1)

    #, jeśli wiadomość jest kontenerem części
    elif part.is_multipart():
        
    else:
        
    # pozostałe części są ignorowane (nie text/plain, nie text/html, nie załącznik)
    # zwracamy aktualną wartość irfc822 (numeracja załączonych wiadomości e-mail umieszczonych w folderze wyjściowym)
    return irfc822

Komentarze

  • wiersze 1–26: przetworzono nagłówki pierwotnej wiadomości;
  • wiersze 28–31: części wiadomości typu [email.message.Message] mają typ główny i podtyp. Pobieramy je;
  • wiersze 32–35: jeśli przetwarzana część ma typ [text/plain], oznacza to, że dotarliśmy do liścia drzewa wiadomości początkowej. Jest to tekst wpisany przez nadawcę w wiadomości;
  • wiersz 35: ten tekst jest zapisany w pliku:
    • pierwszy parametr [output] to folder, w którym tekst ma zostać zapisany;
    • drugi parametr to część wiadomości zawierająca tekst do zapisania;
    • trzeci parametr ma wartość 0 w przypadku zapisywania zwykłego tekstu, a 1 w przypadku tekstu typu HTML;
  • wiersze 37–40: jeśli część ma typ [text/html], oznacza to, że dotarliśmy również do liścia drzewa wiadomości początkowej. Jest to tekst, który nadawca napisał w swojej wiadomości, tym razem w formacie HTML. Nie wszystkie programy pocztowe obsługują ten format;

Funkcja [save_textmessage] działa w następujący sposób:


# zapis wiadomości tekstowej
def save_textmessage(output: str, part: email.message.Message, type_of_text: int):
    # nagłówki
    headers = []
    # zestaw znaków wiadomości
    charset = part.get_content_charset()
    if charset is not None:
        charset = part.get_content_charset().lower()
        headers.append(f"Charset: {charset}")
    # tryb kodowania treści
    content_transfer_encoding = part.get("Content-Transfer-Encoding")
    if content_transfer_encoding is not None:
        headers.append(f"Transfer-Content-Encoding: {content_transfer_encoding}")
    # tryb 8-bitowy spowodował problem
    if content_transfer_encoding == "8bit":
        # pobieramy treść wiadomości e-mail
        msg = part.get_payload()
    else:
        # pobieramy treść wiadomości e-mail
        msg = email.contentmanager.raw_data_manager.get_content(part)
    # w zależności od typu tekstu
    filename = None
    if type_of_text == 0:
        # zapis nagłówków
        with codecs.open(f"{output}/headers.txt", "a", "utf-8") as file:
            # zapis do pliku
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")
        # plik tekstowy zawierający treść
        filename = f"{output}/mail.txt"
    elif type_of_text == 1:
        # plik HTML zawierający treść
        filename = f"{output}/mail.html"
    # zapisywanie wiadomości
    with codecs.open(filename, "w", "utf-8") as file:
        # zapis do pliku
        file.write(msg)

Uwagi

  • podobnie jak nagłówki, tekst wiadomości może być zakodowany. Mogą występować dwa rodzaje kodowania:
    • pierwotne kodowanie tekstu (utf-8, iso-8859-1…). Jest to kodowanie użyte przez program pocztowy, który wysłał wiadomość. Jest ono znane dzięki nagłówkowi [Content-Type] w otrzymanej wiadomości;
    • drugie kodowanie, któremu mógł zostać poddany powyższy tekst w celu wysłania. Jest ono rozpoznawalne dzięki nagłówkowi [Transfer-Content-Encoding] w otrzymanej wiadomości;
  • wiersz 6: początkowe kodowanie tekstu;
  • wiersz 11: drugie kodowanie, któremu poddano tekst w celu przesłania go do odbiorcy;
  • wiersze 9, 13: te dwie informacje są umieszczane na liście o nazwie [headers]. Zostaną one dodane do informacji zawartych w pliku [headers.txt], który rejestruje niektóre nagłówki wiadomości;
  • wiersz 20: plik [email.contentmanager.raw_data_manager.get_content] pozwala uzyskać wiadomość z jej pierwotnym kodowaniem 1. Pozbyto się kodowania 2. Tylko obiekt [email.contentmanager.raw_data_manager] obsługuje wyłącznie dwa typy plików [Transfer-Content-Encoding]:
    • [quoted-printable];
    • [base64];

Pozostałe są ignorowane. Jednak na przykład Thunderbird używa kodowania o nazwie „8bit” ([Transfer-Content-Encoding]). Kodowanie to jest ignorowane, a wiadomości zawierające znaki akcentowane są zniekształcane. Wiadomość można wówczas odzyskać za pomocą metody [part.get_payload()] (wiersze 15–17);

  • wiersz 21: w tym momencie mamy wiadomość pozbawioną kodowania transferowego, a więc wiadomość taką, jaką napisał nadawca;
  • wiersze 22–37: mamy do czynienia z sytuacją, w której należy zapisać wiadomość tekstową;
    • wiersze 24–28: zapisujemy dwa nagłówki utworzone w wierszach 9 i 13 do pliku [headers.txt]. Plik ten już istnieje i zawiera nagłówki. W związku z tym używamy trybu „a” (wiersz 25) do otwarcia tego pliku. „a” oznacza „append” (dodaj), a nowe nagłówki są dodawane (na końcu pliku) do istniejącej zawartości pliku [headers.txt];
    • wiersz 30: nazwa pliku, w którym ma zostać zapisana wiadomość tekstowa;
    • wiersz 33: nazwa pliku, w którym ma zostać zapisana wiadomość HTML;
    • wiersze 34–37: zapisujemy tekst w formacie UTF-8 do pliku;

Wróćmy do funkcji [save_message]:


# zapisanie wiadomości typu email.message.Message
# funkcja ta może być wywoływana rekurencyjnie
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # wynik: folder do zapisywania komunikatów
    # email_message: wiadomość do zapisania
    # irfc822: bieżący numer w numeracji załączonych wiadomości e-mail
    #
    # część wiadomości
    part = email_message
    # nagłówki [From, To, Subject] znajdują się w jednej z części wieloczęściowych
    # lub w części [text/*], gdy nie ma części [multipart]
    keys = part.keys()
    # Pole „From” musi znajdować się wśród nagłówków, w przeciwnym razie część ta nie zawiera poszukiwanych nagłówków
    if "From" in keys:
        # pobieramy niektóre nagłówki
        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'))}"]
        # zapis nagłówków w pliku tekstowym
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # zapis do pliku
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # typ części [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # jeśli wiadomość jest typu text/plain
    if type_of_part == "text/plain":
        # wiadomość tekstowa
        save_textmessage(output, part, 0)

    # jeśli wiadomość jest typu text/html
    elif type_of_part == "text/html":
        # wiadomość HTML
        save_textmessage(output, part, 1)

    #, jeśli wiadomość jest kontenerem części
    elif part.is_multipart():
        # szczególny przypadek wiadomości e-mail z załącznikiem
        if type_of_part == "message/rfc822":
            # utworzenie nowego folderu „output2” dla załączonej wiadomości e-mail
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # zapisanie podczęści komunikatu irfc822 w folderze „output2”
            for subpart in part.get_payload():
                # w nowym folderze irfc822 licznik zaczyna się od 0
                save_message(output2, subpart, 0)

        else:
            # nie mamy do czynienia z załączoną wiadomością e-mail
            # zapisanie podczęści w bieżącym folderze „output”
            # W takim przypadku należy zwiększyć wartość irfc822 dla każdej podczęści message/rfc822
            for subpart in part.get_payload():
                # save_message ustawia ostatnią wartość pola irfc822
                # zwiększoną o 1, jeśli subpart="message/rfc822", w przeciwnym razie nie jest zwiększana
                irfc822 = save_message(output, subpart, irfc822)
    else:
        # inne przypadki (nie text/plain, nie text/html, nie multipart)
        # załącznik?
        disposition = part.get('Content-Disposition')
        if disposition and disposition.startswith('attachment'):
            save_attachment(output, part)
    # pozostałe części są ignorowane (nie text/plain, nie text/html, nie attachment)
    # przywraca aktualną wartość irfc822 (numeracja wiadomości z załącznikami umieszczonych w folderze wyjściowym)
    return irfc822

Komentarze

  • wiersze 33–40: omówiliśmy dwa możliwe przypadki komunikatu znajdującego się na końcu drzewa komunikatu początkowego (bez podczęści). Pozostały nam jeszcze dwa przypadki do omówienia:
    • wiersze 43–62: przypadek, w którym analizowana część sama zawiera podczęści (part.ismultipart()==True);
    • wiersze 63–68: w pozostałych przypadkach rozpatrujemy tylko ten, w którym analizowana część jest załącznikiem;

Zajmujemy się tym ostatnim przypadkiem. Znów znajdujemy się na jednym z krańców pierwotnej wiadomości (brak podczęści). Spotkaliśmy się już z dwoma przypadkami tego rodzaju: typami text/plain i text/html. Teraz zajmujemy się przypadkiem załączonego pliku.

  • wiersz 66: załącznik jest identyfikowany przez klucz [Content-Disposition];
  • wiersz 67: jeśli ten klucz istnieje i zaczyna się od ciągu znaków [attachment], to mamy do czynienia z załącznikiem do wiadomości;
  • wiersz 68: załącznik jest zapisywany w folderze [output];

Funkcja [save_attachment] działa w następujący sposób:


# zapis załącznika
def save_attachment(output: str, part: email.message.Message):
    # nazwa załączonego pliku
    filename = os.path.basename(part.get_filename())

    # nazwa pliku może być zakodowana
    # na przykład =?utf-8?Q?Kursy-Poradniki-Serge-Tah=C3=A9-1568x268=2Ep
    filename = decode_header(filename)
    # zapisujemy załączony plik
    with open(f"{output}/{filename}", "wb") as file:
        file.write(part.get_payload(decode=True))
  • wiersz 4: jeśli [part] jest załącznikiem, to nazwa załączonego pliku jest uzyskiwana za pomocą funkcji [part.get_filename]. Zachowuje się tylko nazwę pliku, a nie jego ścieżkę;
  • wiersz 8: nazwy plików są zazwyczaj zakodowane w taki sam sposób jak nagłówki wiadomości. W związku z tym do ich dekodowania wykorzystuje się funkcję [decode_header];
  • wiersz 11: zawartość załączonego pliku to na razie ciąg znaków powstały w wyniku zakodowania (często w formacie base64) pierwotnej treści pliku. Aby uzyskać tę pierwotną treść, stosuje się funkcję [part.get_payload(decode=True)]. Parametr [decode=True] wskazuje, że zawartość załączonego pliku musi zostać zdekodowana. W ten sposób otrzymujemy ciąg bajtów;
  • wiersz 10: ten ciąg bajtów jest zapisywany w pliku [output/filename]. Tryb otwierania pliku „wb” oznacza write binary;

Wróćmy do kodu funkcji [save_message]:


def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # wynik: folder do zapisywania wiadomości
    # email_message: wiadomość do zapisania
    # irfc822: bieżący numer w numeracji załączonych wiadomości e-mail
    #
    # treść wiadomości
    part = email_message
    # nagłówki [From, To, Subject] znajdują się w jednej z części wieloczęściowych
    # lub w części [text/*], gdy nie ma części [multipart]
    keys = part.keys()
    # Pole „From” musi znajdować się wśród nagłówków, w przeciwnym razie część ta nie zawiera poszukiwanych nagłówków
    if "From" in keys:
        # pobieramy niektóre nagłówki
        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'))}"]
        # zapis nagłówków w pliku tekstowym
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # zapis do pliku
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # typ części [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # jeśli wiadomość jest typu text/plain
    if type_of_part == "text/plain":
        # wiadomość tekstowa
        save_textmessage(output, part, 0)

    # czy wiadomość jest typu text/html
    elif type_of_part == "text/html":
        # wiadomość HTML
        save_textmessage(output, part, 1)

    # jeśli wiadomość jest kontenerem części
    elif part.is_multipart():
        # szczególny przypadek wiadomości e-mail z załącznikiem
        if type_of_part == "message/rfc822":
            # utworzenie nowego folderu „output2” dla załączonej wiadomości e-mail
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # zapisanie podczęści komunikatu irfc822 w folderze „output2”
            for subpart in part.get_payload():
                # w nowym folderze irfc822 licznik zaczyna się od 0
                save_message(output2, subpart, 0)

        else:
            # nie mamy do czynienia z załączoną wiadomością e-mail
            # zapisanie podczęści w bieżącym folderze „output”
            # irfc822 musi być następnie zwiększany dla każdej podczęści message/rfc822
            for subpart in part.get_payload():
                # save_message zwraca ostatnią wartość irfc822
                # zwiększoną o 1, jeśli subpart="message/rfc822", w przeciwnym razie nie jest zwiększana
                irfc822 = save_message(output, subpart, irfc822)
    else:
        # inne przypadki (nie text/plain, nie text/html, nie multipart)
        # załącznik?
        disposition = part.get('Content-Disposition')
        if disposition and disposition.startswith('attachment'):
            save_attachment(output, part)
    # pozostałe części są ignorowane (nie text/plain, nie text/html, nie attachment)
    # przywraca aktualną wartość irfc822 (numeracja wiadomości z załącznikami umieszczonych w folderze wyjściowym)
    return irfc822

Komentarze

  • Omówiliśmy przypadki zakończeń drzewa wiadomości początkowej: części [text/plain, text/html et Content-Disposition=attachment;…]. Pozostaje nam jeszcze omówić przypadek, w którym analizowana część jest kontenerem części, tzn. zawiera podczęści [part.is_multipart()==True], wiersz 41. Aby dotrzeć do końców drzewa wiadomości, należy zatem przeanalizować te podczęści;
  • wiersz 43: w szczególny sposób traktujemy przypadek, w którym analizowana część ma typ [message/rfc822]. Jest to typ wiadomości e-mail. Jest to zatem przypadek, w którym wiadomość e-mail ma w załączniku inną wiadomość e-mail;

Kod wygląda następująco:


    # jeśli wiadomość jest kontenerem części
    elif part.is_multipart():
        # szczególny przypadek wiadomości załączonej
        if type_of_part == "message/rfc822":
            # utworzenie nowego folderu „output2” dla załączonej wiadomości e-mail
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # zapisanie podczęści wiadomości irfc822 w folderze „output2”
            for subpart in part.get_payload():
                # w nowym folderze irfc822 licznik zaczyna się od 0
                save_message(output2, subpart, 0)

        else:
            # nie mamy do czynienia z załączoną wiadomością e-mail
            # zapisanie podczęści w bieżącym folderze „output”
            # irfc822 musi być następnie zwiększany dla każdej podczęści message/rfc822
            for subpart in part.get_payload():
                # save_message zwraca ostatnią wartość irfc822
                # zwiększoną o 1, jeśli subpart="message/rfc822", w przeciwnym razie nie jest zwiększana
                irfc822 = save_message(output, subpart, irfc822)

    return irfc822
  • różnica między częścią [message/rfc822] a pozostałymi częściami wieloczęściowymi polega na tym, że zmienia się folder zapisu;
    • wiersze 6–8: w przypadku części [message/rfc822] folder zapisu staje się folderem z wiersza 7 – [output/rfc822_x], gdzie x oznacza numer załączonej wiadomości e-mail: 1 dla pierwszej, 2 dla drugiej…;
    • wiersz 21: w przypadku pozostałych części wieloczęściowych folderem zapisu pozostaje folder [output] z wiadomości początkowej. Nie zmienia się folderu;
  • wiersze 10–12: każda podczęść jest zapisywana poprzez rekurencyjne wywołanie funkcji [save_message]. Trzeci parametr to indeks numeracyjny wiadomości e-mail zawartych w [subpart]. Początkowo indeks ten wynosi 0;
  • wiersz 21: to samo wyjaśnienie co dla wiersza 12, ale zmienia się wartość trzeciego parametru funkcji [irfc822]. Jeśli w pętli obejmującej wiersze 18–21 znajduje się kilka wiadomości e-mail, muszą one zostać umieszczone w folderach o nazwie […/rfc822-1…/rfc822_2…]. Zatem trzeci parametr funkcji [save_message] musi przyjmować kolejno wartości 1, 2, 3… W tym celu funkcja [save_message] ustawia wartość funkcji [irfc822] (wiersz 21).

Weźmy przykład i załóżmy, że lista podczęści z wiersza 18 to [subpart1, subpart2, subpart3, subpart4, subpart5], a [subpart1, subpart3, subpart5] to załączone wiadomości e-mail, [subpart2] to część typu text/plain, a [subpart4] to załącznik, oraz że w wiadomości [irfc822=0] nie napotkano jeszcze żadnej wiadomości e-mail z załącznikiem. W takim przypadku:

  • (ciąg dalszy)
    • [subpart1] jest zapisywany w wierszu 21: funkcja [saveMessage] jest wykonywana z irfc822=0;
    • [subpart1] jest wiadomością e-mail z załącznikiem, więc wartość irfc822 zmienia się na 1 (wiersz 6 kodu). Tworzony jest plik [output/irfc822_1]. Wartość zwracana przez funkcję [saveMessage(ouput,subpart1,0)] wynosi zatem 1 (wiersz 23);
    • [subpart2] jest zapisywany w wierszu 21: funkcja [saveMessage] jest wykonywana z irfc822=1;
    • [subpart2] nie jest załącznikiem wiadomości e-mail. Dlatego wartość irfc822 pozostaje równa 1. Jest to wartość pobrana w wierszu 21;
    • [subpart3] jest zapisywany w wierszu 21: funkcja [save_message] jest wykonywana z irfc822=1;
    • [subpart3] jest wiadomością e-mail z załącznikiem, więc wartość irfc822 zmienia się na 2 (wiersz 6 kodu). Tworzony jest plik [output/irfc822_2]. Wartość zwracana przez [save_message(ouput,subpart1,1)] wynosi zatem 2 (wiersz 21);
    • [subpart4] jest zapisywany w wierszu 21: funkcja [save_message] jest wykonywana z irfc822=2;
    • [subpart4] nie jest załącznikiem wiadomości e-mail. Dlatego wartość irfc822 pozostaje równa 2. Jest to wartość pobrana w wierszu 21;
    • [subpart5] jest zapisywany w wierszu 21: funkcja [save_message] jest wykonywana z irfc822=2;
    • [subpart5] to wiadomość e-mail z załącznikiem, więc wartość irfc822 przyjmuje wartość 3 (wiersz 6 kodu). Tworzony jest folder o nazwie [output/irfc822_3]. Wartość zwracana przez [save_message(ouput,subpart1,2)] wynosi zatem 3 (wiersz 21);

Przykłady wykonania

Wysyłamy 4 wiadomości e-mail do [pymail2parlexemple@gmail.com] z: [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/];

Wszystkie wiadomości e-mail będą miały temat [hélène va au marché], a treść [acheter des légumes]. Chcemy sprawdzić, w jaki sposób odczytywane są znaki akcentowane.

Odczytujemy je za pomocą skryptu [pop3/02/main] skonfigurowanego przy użyciu następującego pliku [pop3/02/config]:


import os


def configure() -> dict:
    # konfiguracja aplikacji
    config = {
        # lista skrzynek pocztowych do zarządzania
        "mailboxes": [
            # serwer: serwer POP3
            # port: port serwera POP3
            # użytkownik: użytkownik, którego wiadomości chcemy przeczytać
            # hasło: jego hasło
            # maxmails: maksymalna liczba wiadomości do pobrania
            # timeout: maksymalny czas oczekiwania na odpowiedź serwera
            # delete: ustawić na „true”, jeśli pobrane wiadomości mają zostać usunięte z serwera
            # ssl: wartość „true”, jeśli odczyt wiadomości odbywa się za pośrednictwem bezpiecznego połączenia
            # output: folder, w którym będą zapisywane pobrane wiadomości

            {
                "server": "pop.gmail.com",
                "port": "995",
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prD&@1QZ3TG",
                "maxmails": 10,
                "delete": False,
                "ssl": True,
                "timeout": 2.0,
                "output": "output"
            }
        ]
    }
    # absolutna ścieżka do folderu skryptu
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # absolutne ścieżki do folderów, które mają zostać uwzględnione w syspath
    absolute_dependencies = [
        # folder lokalny
        f"{script_dir}/../../shared",
    ]

    # konfiguracja syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # konfiguracja jest generowana
    return config

Wynik jest następujący:

Image

Wiadomość nr 1 to ta wysłana przez Thunderbirda:

Image

  • w [5] program Thunderbird [3] wykorzystuje plik [Transfer-Content-Encoding] typu [8bit];
  • w [4]: wiadomość jest zakodowana w formacie UTF-8;

Komunikat 2 to ten wysłany przez klienta e-mail:

Image

Image

Warto zauważyć, że [em Client] koduje teksty w formacie utf-8 [4] i przekazuje je w formacie [quoted-printable] [5]. Wysłał również kopię wiadomości w formacie HTML i [7-8]. Wszystkie testowane tutaj programy pocztowe potrafią to zrobić. Jest to ustawienie konfiguracyjne.

Wiadomość nr 3 to ta wysłana przez Gmaila:

Image

Warto zauważyć, że Gmail koduje teksty w formacie utf-8 ([3]) i przesyła je w formacie [quoted-printable] oraz [4]. W formacie [6] – wersja wiadomości HTML.

Wiadomość nr 4 to ta wysłana przez program Outlook:

Image

Warto zauważyć, że program Outlook koduje teksty w standardzie iso-8859-1 ([3]) i przekształca je na format [quoted-printable] oraz [4].

Powyższe przykłady pokazują dwie rzeczy:

  • nasz klient [pop3/02] działał poprawnie;
  • programy pocztowe mają różne sposoby wysyłania wiadomości e-mail;

Przyjrzyjmy się teraz załącznikom. Za pomocą programu Thunderbird opróżniamy skrzynkę pocztową użytkownika [pymail2parlexemple@gmail.com]. Następnie używamy skryptu [smtp/03/main] do wysłania wiadomości e-mail z następującą konfiguracją [smtp/03/config]:


import os


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

    return {
        # opis: opis wysłanej wiadomości e-mail
        # serwer SMTP: serwer SMTP
        # smtp-port: port serwera SMTP
        # from: nadawca
        # do: odbiorca
        # subject: temat wiadomości e-mail
        # treść: treść wiadomości 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",
                # testujemy znaki z akcentami
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP z uwierzytelnianiem
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # w tym miejscu należy podać bezwzględne ścieżki do załączonych plików
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                    f"{script_dir}/attachments/mail attaché 1.eml",
                ]
            }
        ]
    }
  • wiersze 31–33: do wiadomości e-mail dołączamy:
  • plik Word;
  • plik PDF;
  • wiadomość e-mail zawierającą te same dwa załączniki;

Po wysłaniu wiadomości e-mail uruchamiamy skrypt [pop3/02] w celu odczytania skrzynki pocztowej użytkownika [pymail2parlexemple@gmail.com]. Wyniki są następujące:

Image

  • w [1]: wiadomość wraz z dwoma załącznikami;
  • w [2]: sam załączony e-mail wraz z dwoma załącznikami;

Wniosek

Moduł [mail_parser.py] jest szczególnie złożony. Wynika to ze złożoności samych wiadomości e-mail. Wykorzystamy ten moduł ponownie dla protokołu IMAP.

21.7. Protokół IMAP

21.7.1. Wprowadzenie

Do odczytu wiadomości e-mail przechowywanych na serwerze pocztowym istnieją dwa protokoły:

  • protokół POP3 (Post Office Protocol) – historycznie pierwszy protokół, ale obecnie rzadko stosowany;
  • protokół IMAP (Internet Message Access Protocol) – nowszy od POP3 i obecnie najczęściej stosowany;

Aby zapoznać się z protokołem IMAP, wykorzystamy następującą architekturę:

Image

  • [Serveur B] będzie, w zależności od sytuacji:
    • lokalnym serwerem IMAP, zaimplementowanym przez serwer pocztowy [hMailServer];
    • serwer [imap.gmail.com:993], który jest serwerem IMAP menedżera poczty [Gmail];
  • [Client A] będzie skryptem w języku Python wykorzystującym moduły Pythona umożliwiające zarządzanie załącznikami, a także korzystanie z szyfrowanego i uwierzytelnionego połączenia, gdy wymaga tego serwer IMAP;

Protokół IMAP wykracza poza zakres protokołu POP3:

  • wiadomości e-mail są przechowywane na serwerze IMAP i mogą być porządkowane w folderach;
  • klient IMAP może wysyłać polecenia tworzenia, modyfikacji i usuwania tych folderów;

Spójrzmy na przykład z wykorzystaniem programu Thunderbird. W następującej architekturze:

Image

  • Thunderbird jest klientem A;
  • [imap.gmail.com] to serwer B (Gmail);

Utwórzmy folder w wiadomościach użytkownika [pymail2parlexemple@gmail.com] za pomocą programu Thunderbird:

Image

  • w [1-6] tworzymy folder [dossier1];

Image

  • do [7-8], przenosimy (za pomocą myszki) wszystkie pliki z folderu [Courrier entrant] do folderu [dossier1];

Teraz logujemy się na stronie Gmaila i logujemy się jako użytkownik [pymail2parlexemple@gmail.com]:

Image

  • w [2-3] skrzynka odbiorcza jest pusta;
  • w [1] znajduje się utworzony folder [dossier1];

Image

  • w [4-6]: wiadomości e-mail, które zostały przeniesione do folderu [dossier1];

Mamy tu do czynienia z następującą architekturą:

Image

  • Klient A to aplikacja Thunderbird;
  • Klient C to aplikacja internetowa Gmail;
  • Serwer B to serwer Gmaila o nazwie IMAP;

Drzewo folderów użytkownika jest zarządzane przez serwer IMAP. Następnie wszystkie klienty IMAP synchronizują się z nim, aby wyświetlić użytkownikowi foldery jego konta. W tym przypadku Thunderbird wysłał kilka poleceń w celu:

  • utworzenie folderu [dossier1];
  • przeniesienie wiadomości do tego folderu;

21.7.2. skrypt [imap/main]: klient IMAP z modułem [imaplib]

Image

Skrypt [imap/main] jest konfigurowany przez następujący skrypt [imap/config]:


import os


def configure() -> dict:
    # konfiguracja aplikacji
    config = {
        # lista skrzynek pocztowych do zarządzania
        "mailboxes": [
            # serwer: serwer IMAP
            # port: port serwera IMAP
            # użytkownik: użytkownik, którego wiadomości chcesz przeczytać
            # hasło: jego hasło
            # maxmails: maksymalna liczba wiadomości do pobrania
            # timeout: maksymalny czas oczekiwania na odpowiedź serwera
            # delete: ustawić na „true”, jeśli pobrane wiadomości mają zostać usunięte z serwera
            # ssl: wartość „true”, jeśli odczyt wiadomości odbywa się za pośrednictwem bezpiecznego połączenia
            # output: folder, w którym będą zapisywane pobrane wiadomości

            {
                "server": "imap.gmail.com",
                "port": "993",
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                "maxmails": 10,
                "ssl": True,
                "timeout": 2.0,
                "output": "output"
            }
        ]
    }
    # absolutna ścieżka do folderu skryptu
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # absolutne ścieżki do folderów, które mają zostać uwzględnione w syspath
    absolute_dependencies = [
        # folder lokalny
        f"{script_dir}/../shared",
    ]

    # konfiguracja syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # ustawiamy konfigurację
    return config

Komentarze

  • wiersze 8–29: klucz [mailboxes] jest powiązany z listą skrzynek pocztowych do sprawdzenia;
  • wiersz 20: serwer IMAP;
  • wiersz 21: jego port usługowy;
  • wiersze 22–23: użytkownik, którego wiadomości e-mail chcemy przeczytać;
  • wiersz 24: maksymalna liczba wiadomości e-mail, które chcemy przeczytać;
  • wiersz 25: określa, czy należy nawiązać bezpieczne połączenie z serwerem IMAP (True), czy nie (False);
  • wiersz 26: maksymalny czas oczekiwania na odpowiedź serwera;
  • wiersz 27: folder, w którym będą zapisywane przeczytane wiadomości e-mail;

Skrypt [imap/main] wygląda następująco:


# importy
import email
import imaplib
import os
import shutil


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

def readmails(mailbox: dict):
    


# main ----------------------------------------------------------------
#  klient IMAP umożliwiający odczytywanie wiadomości e-mail

# pobieramy konfigurację aplikacji
import config
config = config.configure()

# przetwarzanie skrzynek pocztowych jedna po drugiej
for mailbox in config['mailboxes']:
    try:
        # wyświetlanie konsoli
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']} / {mailbox['server']}:{mailbox['port']}")
        # odczyt skrzynki pocztowej
        readmails(mailbox)
        # koniec
        print("Lecture terminée...")
    # z wyjątkiem BaseException jako błąd:
    #     # wyświetlamy błąd
    #     print(f"Wystąpił następujący błąd: {błąd}")
    finally:
        pass

Komentarze

  • wiersze 14–36: mamy tu do czynienia z podejściem znanym już ze skryptu |pop3/02/main|;

Funkcja [readmails] wygląda następująco:


def readmails(mailbox: dict):
    # pozwalamy na przekazywanie wyjątków
    #
    # moduł parsera wiadomości e-mail
    from mail_parser import save_message

    # pobieranie informacji konfiguracyjnych
    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']
    #
    # zaczynamy
    imap_resource = None
    try:
        # tworzymy katalogi do przechowywania danych, jeśli jeszcze nie istnieją
        if not os.path.isdir(output):
            os.mkdir(output)
        # użytkownik
        dir2 = f"{output}/{user}"
        # usuwamy folder [dir2], jeśli istnieje, a następnie tworzymy go ponownie
        if os.path.isdir(dir2):
            # usunięcie
            shutil.rmtree(dir2)
        # utworzenie
        os.mkdir(dir2)
        # połączenie z serwerem IMAP
        if ssl:
            imap_resource = imaplib.IMAP4_SSL(server, port)
        else:
            imap_resource = imaplib.IMAP4(server, port)
        # przekroczenie limitu czasu komunikacji klienta
        sock = imap_resource.socket()
        sock.settimeout(timeout)
        # uwierzytelnianie
        imap_resource.login(user, password)
        # wybieramy folder INBOX (poczta przychodząca)
        imap_resource.select('INBOX')
        # pobierane są wszystkie wiadomości z tego folderu: kryterium ALL
        # brak konkretnego kodowania: None
        typ1, data1 = imap_resource.search(None, 'ALL')
        # print(f"typ={typ1}, data={data1}")

        # data1[0] to tablica bajtów zawierająca numery wszystkich wiadomości, oddzielone spacją
        nums = data1[0].split()
        imail = 0
        fini = imail >= maxmails or imail >= len(nums)
        # wiadomości są odczytywane jedna po drugiej
        while not fini:
            # num to numer wiadomości w formacie binarnym
            num = nums[imail]
            # print(f"wiadomość nr {num}")

            # pobieramy wiadomość o numerze num
            typ2, data2 = imap_resource.fetch(num, '(RFC822)')
            # print(f"type={typ2}, data={data2}")

            # data to lista zawierająca krotki, w tym przypadku tylko jedną
            # data[0] to krotka, dataQZXW2HTMLBWzBdZQXQZXW2HTMLBWzFdZQX to drugi element tej krotki
            # dataQZXW2HTMLBWzBdZQXQZXW2HTMLBWzFdZQX zawiera ciąg bajtów reprezentujący wszystkie wiersze wiadomości
            # przez „wiadomość” należy rozumieć treść wiadomości wraz ze wszystkimi załącznikami

            # odzyskujemy wiadomość jako typ email.message.Message
            message = email.message_from_bytes(data2[0][1])
            # folder wiadomości
            dir3 = f"{dir2}/message_{int(num)}"
            # jeśli folder nie istnieje, tworzy się go
            if not os.path.isdir(dir3):
                os.mkdir(dir3)
            # zapisujemy go
            save_message(dir3, message)
            # następna wiadomość
            imail += 1
            fini = imail >= maxmails or imail >= len(nums)
    finally:
        if imap_resource:
            # zamyka się połączenie ze skrzynką pocztową
            imap_resource.close()
            # odłączamy się od serwera IMAP
            imap_resource.logout()

Komentarze

  • wiersze 7–15: pobierane są elementy konfiguracji;
  • wiersze 19, 79: kod jest kontrolowany przez blok try / finally. Nie przechwytujemy zatem wyjątków (brak klauzuli except), które są następnie przekazywane do kodu wywołującego, który je zatrzymuje i wyświetla;
  • wiersze 23–30: tworzy się folder do zapisywania wiadomości e-mail;
  • wiersze 31–35: nawiązuje się połączenie z serwerem IMAP. Wybór klasy zależy od tego, czy mamy do czynienia z zabezpieczonym serwerem IMAP (IMAP4_SSL), czy też nie (IMAP4);
  • wiersze 36–38: ustawia się limit czasu komunikacji klient–serwer;
  • wiersze 39–40: uwierzytelnianie się na serwerze IMAP;
  • wiersze 41–42: widzieliśmy już, że skrzynka pocztowa użytkownika o identyfikatorze IMAP może być podzielona na foldery. Folder [INBOX] to folder poczty przychodzącej. Aby wybrać folder [dossier1], należy wpisać [imapResource.select('dossier1')];
  • wiersze 43–45: żądamy listy wszystkich wiadomości znalezionych w folderze [INBOX]:
    • pierwszy parametr [imapResource.search] określa typ kodowania. [None] oznacza „brak filtrowania według kodowania”;
    • drugi parametr to kryterium. Istnieją różne sposoby jego określenia. Kryterium [ALL] oznacza, że chcemy wszystkie wiadomości z folderu;

Wynik [imapResource.search] wygląda następująco:


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

[data] to lista zawierająca numery znalezionych wiadomości. Są one zapisane w systemie binarnym. W powyższym przykładzie w folderze znaleziono dwie wiadomości o numerach [INBOX];

  • wiersz 49: pobieramy numery wiadomości. Powyżej otrzymamy listę [b'1' b'2'], czyli listę numerów zakodowanych w systemie binarnym;
  • wiersze 53–78: uruchamiamy pętlę, aby odczytać wiadomości z folderu [INBOX];
  • wiersze 54–55: numer wiadomości;
  • wiersze 58–59: żądanie wiadomości o numerze [num] kierowane jest do serwera IMAP;
    • pierwszym parametrem jest numer żądanego komunikatu;
    • drugim parametrem jest ciąg znaków „(część1)(część2)…”, gdzie [parti] to nazwa części wiadomości. Nie zagłębiałem się w tę kwestię. Nazwa (RFC822) odnosi się do całej wiadomości e-mail;

Otrzymujemy coś w następującej postaci:


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')']

Element [data] jest tutaj listą zawierającą jeden element, a ten pojedynczy element jest krotką składającą się z trzech elementów:


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')'
]

Drugi element tej krotki to ciąg binarny reprezentujący całą żądaną wiadomość. W powyższym przykładzie rozpoznajemy elementy przedstawione już podczas analizy modułu [mail_parser].

data[0] reprezentuje krotkę dwuelementową. data[0][1] reprezentuje wiersze komunikatu w postaci binarnej.

  • wiersz 68: funkcja [taxpayers[slice(10,12)]] tworzy obiekt typu [email.message.Message] na podstawie wierszy wiadomości. Typ [email.message.Message] jest typem parametru modułu [mail_parser], który napisaliśmy wcześniej;
  • wiersze 69–73: tworzymy folder do zapisania komunikatu nr [num];
  • wiersz 75: wywołujemy funkcję [save_message] z modułu [mail_parser] z wiersza 5. Funkcja ta została opisana w akapicie |pop3/02/main|;
  • wiersze 76–78: następuje powrót do pętli w celu przetworzenia kolejnej wiadomości;
  • wiersze 79–84: niezależnie od tego, czy wystąpił błąd, czy nie:
    • wiersz 82: zamyka się połączenie z zapytanym folderem;
    • wiersz 84: następuje rozłączenie z serwerem IMAP;

Uzyskane wyniki są identyczne z tymi uzyskanymi za pomocą skryptu [pop3/02/main]. Jest to normalne, ponieważ wykorzystywany jest ten sam parser wiadomości e-mail [mail_parser].