Skip to content

21. Інтернет-функції

Тепер розглянемо інтернет-функції Python, які дозволяють нам здійснювати програмування TCP / IP (Transfer Control Protocol / Internet Protocol).

Image

21.1. Основи інтернет-програмування

21.1.1. Загальні відомості

Розглянемо взаємодію між двома віддаленими комп’ютерами A та B:

Image

Коли програма AppA на комп’ютері A хоче встановити зв’язок із програмою AppB на комп’ютері B в Інтернеті, їй потрібно знати кілька речей:

  • адресу IP (Інтернет-протокол) або ім’я комп’ютера B;
  • номер порту, з яким працює програма AppB. Адже комп’ютер B може підтримувати безліч програм, що працюють в Інтернеті. Коли він отримує інформацію з мережі, він повинен знати, для якої саме програми призначена ця інформація. Доступ до мережі додатки комп’ютера B отримують через «вікна», які також називаються портами зв’язку. Ця інформація міститься в пакеті, отриманому комп’ютером B, щоб його можна було доставити потрібному додатку;
  • протоколи зв’язку, які розуміє машина B. У нашому дослідженні ми використовуватимемо виключно протоколи TCP-IP;
  • протокол діалогу, який підтримує програма AppB. Адже машини A та B будуть «спілкуватися» між собою. Те, що вони скажуть, буде інкапсульовано в протоколи TCP-IP. Проте, коли на кінці ланцюга додаток AppB отримає інформацію, надіслану додатком AppA, він повинен бути здатний її інтерпретувати. Це аналогічно ситуації, коли дві особи, А і Б, спілкуються по телефону: їхній діалог передається через телефон. Мова кодується у вигляді сигналів телефоном А, передається по телефонних лініях, досягає телефону Б, де її декодують. Тоді людина Б чує слова. Саме тут виникає поняття протоколу діалогу: якщо А розмовляє французькою, а Б не розуміє цієї мови, А і Б не зможуть вести змістовний діалог;

Тому обидва додатки, що спілкуються, повинні домовитися про тип діалогу, який вони використовуватимуть. Наприклад, діалог із сервісом ftp відрізняється від діалогу із сервісом pop: ці два сервіси не приймають однакових команд. Вони мають різний протокол діалогу;

21.1.2. Характеристики протоколу TCP

Тут ми розглянемо лише мережеві комунікації, що використовують транспортний протокол TCP, основні характеристики якого наведені нижче:

  • процес, який бажає здійснити передачу, спочатку встановлює з’єднання з процесом-одержувачем інформації, яку він збирається передати. Це з’єднання встановлюється між портом машини-відправника та портом машини-одержувача. Між цими двома портами створюється віртуальний шлях, який буде зарезервований виключно для двох процесів, що встановили з’єднання;
  • усі пакети, що надсилаються вихідним процесом, проходять цим віртуальним шляхом і надходять у тому порядку, в якому вони були відправлені;
  • Інформація, що передається, має безперервний характер. Процес-відправник надсилає інформацію у своєму темпі. Ця інформація не обов’язково надсилається одразу: протокол TCP чекає, поки її накопичиться достатньо, щоб її надіслати. Вона зберігається у структурі, що називається сегментом TCP. Цей сегмент, щойно заповнений, передається на рівень IP, де він інкапсулюється в пакет IP;
  • кожен сегмент, надісланий за протоколом TCP, має свій номер. Протокол TCP одержувача перевіряє, чи сегменти надходять у правильній послідовності. За кожен правильно отриманий сегмент він надсилає підтвердження про отримання відправнику;
  • коли останній отримує його, він повідомляє про це процес-відправник. Таким чином, останній може дізнатися, що сегмент надійшов за призначенням;
  • якщо через певний час протокол TCP, який відправив сегмент, не отримає підтвердження про отримання, він повторно надсилає цей сегмент, тим самим гарантуючи якість послуги передачі інформації;
  • віртуальний канал, встановлений між двома процесами, що взаємодіють, має номер full-duplex: це означає, що інформація може передаватися в обох напрямках. Таким чином, процес-приймач може надсилати підтвердження про отримання навіть тоді, коли процес-відправник продовжує надсилати інформацію. Це дозволяє, наприклад, протоколу-відправнику TCP надсилати кілька сегментів, не чекаючи на підтвердження про отримання. Якщо через певний час він виявляє, що не отримав підтвердження про отримання певного сегмента № n, він відновить передачу сегментів з цього моменту;

21.1.3. Взаємодія «клієнт-сервер»

Часто комунікація в Інтернеті є асиметричною: машина А ініціює з’єднання, щоб звернутися до машини Б із запитом на послугу: вона вказує, що хоче встановити з’єднання зі службою SB1 на машині Б. Остання приймає або відхиляє запит. Якщо вона приймає, машина А може надсилати свої запити до служби SB1. Ці запити повинні відповідати протоколу взаємодії, який розуміє служба SB1. Таким чином між машиною А, яку називають клієнтською машиною, та машиною Б, яку називають серверною машиною, встановлюється діалог «запит-відповідь». Один із двох партнерів закриє з’єднання.

21.1.4. Архітектура клієнта

Архітектура мережевої програми, що звертається за послугами серверного додатка, буде такою:

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. Архітектура сервера

Архітектура програми, що надає послуги, буде такою:

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

Серверна програма по-різному обробляє початковий запит на з'єднання від клієнта та його подальші запити, спрямовані на отримання послуги. Програма сама не надає послугу. Якби вона це робила, то протягом часу надання послуги вона більше не прислухалася б до запитів на з'єднання, і клієнти не отримували б обслуговування. Вона діє інакше: щойно запит на з’єднання надходить на порт прослуховування та приймається, сервер створює завдання, відповідальне за надання послуги, запитуваної клієнтом. Ця послуга надається на іншому порту серверної машини, який називається портом обслуговування. Таким чином можна обслуговувати декількох клієнтів одночасно.

Завдання обслуговування матиме таку структуру:

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. Дізнайтеся про протоколи зв’язку в Інтернеті

21.2.1. Вступ

Коли клієнт підключається до сервера, між ними встановлюється діалог. Характер цього діалогу визначає так званий протокол зв’язку сервера. Серед найпоширеніших протоколів Інтернету можна виділити такі:

  • HTTP: HyperText Transfer Protocol — протокол взаємодії з веб-сервером (сервер HTTP);
  • SMTP: Simple Mail Transfer Protocol — протокол взаємодії з сервером відправлення електронної пошти (сервер SMTP);
  • POP: Post Office Protocol — протокол взаємодії з сервером зберігання електронної пошти (сервер POP). Він призначений для отримання електронних листів, а не для їх надсилання;
  • IMAP: Internet Message Access Protocol — протокол взаємодії з сервером зберігання електронної пошти (сервер IMAP). Цей протокол поступово замінив старіший протокол POP;
  • FTP: File Transfer Protocol — протокол взаємодії з сервером зберігання файлів (сервер FTP);

Всі ці протоколи мають особливість бути текстовими протоколами: клієнт і сервер обмінюються текстовими рядками. Якщо у нас є клієнт, здатний:

  • встановити з’єднання з сервером TCP;
  • відображати в консолі текстові рядки, які надсилає йому сервер;
  • надсилати на сервер текстові рядки, які користувач вводить з клавіатури;

Отже, ми можемо взаємодіяти з сервером TCP, що використовує протокол на основі текстових рядків, за умови, що знаємо правила цього протоколу.

21.2.2. Утиліти TCP

Image

У кодах, пов’язаних із цим документом, містяться дві утиліти зв’язку TCP:

  • [RawTcpClient] дозволяє підключитися до порту P сервера S;
  • [RawTcpServer] дозволяє створити сервер, який очікує клієнтів на порту P;

Це дві програми на C#, вихідні коди яких надано вам. Отже, ви можете їх змінювати.

Сервер TCP [RawTcpServer]викликається за допомогою синтаксису [RawTcpServeur port] для створення служби TCP на порту [port] локальної машини (комп’ютера, на якому ви працюєте):

  • сервер може обслуговувати декількох клієнтів одночасно;
  • сервер виконує команди, введені користувачем за допомогою клавіатури. Це такі команди:
    • list: виводить список клієнтів, які наразі підключені до сервера. Вони відображаються у форматі [id=x-nom=y]. Поле [id] слугує для ідентифікації клієнтів;
    • send x [texte]: надсилає текст клієнту № x (id=x). Квадратні дужки [] не надсилаються. Вони необхідні в команді. Вони слугують для візуального виділення тексту, що надсилається клієнту;
    • close x: закриває з’єднання з клієнтом № x;
    • quit: закриває всі з’єднання та зупиняє роботу служби;
  • рядки, надіслані клієнтом на сервер, відображаються на консолі;
  • увесь обмін даними записується у текстовий файл із назвою [machine-port.txt], де
    • [machine] — ім’я комп’ютера, на якому виконується код;
    • [port] — це порт служби, який відповідає на запити клієнта;

Клієнт TCP [RawTcpClient] викликається за допомогою синтаксису [RawTcpClient serveur port] для підключення до порту [port] сервера [serveur]:

  • рядки, введені користувачем з клавіатури, надсилаються на сервер;
  • рядки, надіслані сервером, відображаються на консолі;
  • увесь обмін даними записується у текстовий файл із назвою [serveur-port.txt];

Розглянемо приклад. Відкриваємо два термінали PyCharm і в кожному з них переходимо до папки з утилітами:

Image

В одному з вікон запускаємо сервер [RawTcpServer] на порту 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 :
  • у рядку 1 ми перебуваємо в папці утиліт;
  • у рядку 1 запускаємо сервер TCP на порту 100;
  • рядки 2–4: сервер переходить у режим очікування клієнта TCP і відображає список команд, які користувач може ввести з клавіатури;
  • у рядку 5 сервер очікує на команду, введену користувачем з клавіатури;

В іншому командному вікні запускаємо клієнт 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) :
  • у рядку 1 ми знаходимося в папці з утилітами;
  • у рядку 1 запускаємо клієнт TCP: вказуємо йому підключитися до порту 100 локальної машини (тієї, на якій виконується код [RawTcpClient]);
  • у рядку 2 клієнт успішно підключився до сервера. Вказуються координати клієнта: він знаходиться на машині [DESKTOP-30FF5FB] (у цьому прикладі — локальна машина) і використовує порт [51173] для зв’язку з сервером:
  • у рядку 3 клієнт очікує на команду, введену користувачем з клавіатури;

Повернемося до вікна сервера. Його вміст змінився:


(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...
  • у рядку 5 виявлено клієнта. Сервер присвоїв йому номер 1. Сервер правильно ідентифікував віддаленого клієнта (комп’ютер і порт);
  • рядок 6: сервер знову очікує на нового клієнта;

Повернемося до вікна клієнта та надішлемо команду на сервер:


(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
  • рядок 4 — команда, надіслана серверу;

Повернемося до вікна сервера. Його вміст змінився:


(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]
  • рядок 7, у квадратних дужках — повідомлення, отримане сервером;

Надішлімо відповідь клієнту:


(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 :
  • рядок 8, відповідь, надіслана клієнту 1. Надсилається лише текст у дужках, а не самі дужки;

Повернемося до вікна клієнта:


(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]
  • рядок 5, відповідь, отримана клієнтом. Отриманий текст — це текст у квадратних дужках;

Повернемося до вікна сервера, щоб переглянути інші команди:


(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
  • рядок 9, ми запитуємо список клієнтів;
  • рядок 10 — відповідь;
  • рядок 11, ми закриваємо з’єднання з клієнтом № 1;
  • рядок 12 — підтвердження від сервера;
  • рядок 13 — ми зупиняємо сервер;
  • рядок 14 — підтвердження від сервера;

Повернемося до вікна клієнта:


(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...
  • рядок 6, клієнт виявив завершення роботи сервера;

Було створено два файли журналів: один для сервера, інший — для клієнта:

Image

  • у [1] — журнали сервера: ім’я файлу — це ім’я клієнта у форматі [machine-port]. Це дозволяє мати різні журнали для різних клієнтів;
  • у файлі [2] — журнали клієнта: ім’я файлу відповідає імені сервера у форматі [machine-port];

Журнали сервера мають такий вигляд:


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

Журнали клієнта мають такий вигляд:


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

21.3. Отримати ім’я або адресу IP комп’ютера в Інтернеті

Image

Комп'ютери в Інтернеті ідентифікуються за адресою IP (IPv4 або IPv6) і найчастіше за іменем. Але в кінцевому підсумку лише адреса IP використовується протоколами зв’язку в Інтернеті. Тому необхідно знати адресу IP комп’ютера, ідентифікованого за його іменем.

Скрипт [ip-01.py] має такий вигляд:


# імпортується
import socket


# ------------------------------------------------
def get_ip_and_name(nom_machine: str):
    # nom_machine: ім'я машини, адресу якої потрібно отримати IP
    try:
        # nom_machine-->адреса IP
        ip = socket.gethostbyname(nom_machine)
        print(f"ip[{nom_machine}]={ip}")
    except socket.error as erreur:
        # видається помилка
        print(f"ip[{nom_machine}]={erreur}")
        return

    try:
        # адреса IP --> nom_machine
        names = socket.gethostbyaddr(ip)
        print(f"names[{ip}]={names}")
    except socket.error as erreur:
        # видається помилка
        print(f"names[{ip}]={erreur}")
        return


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

# інтернет-машини
hosts = ["istia.univ-angers.fr", "www.univ-angers.fr", "sergetahe.com", "localhost", "xx"]

# адреси IP машин HOTES
for host in hosts:
    print("-------------------------------------")
    get_ip_and_name(host)
# кінець
print("Terminé...")

Коментарі

  • рядок 2: модуль [socket] надає функції, необхідні для управління інтернет-сокетами. [socket] означає електричну розетку, мережеву розетку;
  • рядок 6: функція [get_ip_and_name] дозволяє на основі інтернет-імені комп'ютера отримати:
    • адресу IP цього комп’ютера;
    • ім’я комп’ютера, отримане на основі попередньої адреси IP;
  • рядок 10: функція [socket.gethostbyname] дозволяє отримати адресу IP комп'ютера на основі одного з цих імен (інтернет-комп'ютер може мати основне ім'я та псевдоніми);
  • рядок 12: функції, що працюють із сокетами, генерують виняток [socket.error], щойно виникає помилка;
  • рядок 19: функція [socket.gethostbyaddr] дозволяє отримати ім’я комп’ютера на основі його адреси IP. Ми побачимо, що можна отримати ім’я, відмінне від того, що було передано в рядку 6;
  • рядок 30: список імен машин. Останнє ім’я є помилковим. Ім’я [localhost] позначає машину, на якій ви працюєте і яка виконує скрипт;
  • рядки 33–35: виводиться IP цих машин;

Результати:


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. Протокол HTTP (HyperText Transfer Protocol)

21.4.1. Приклад 1

Image

Коли браузер відображає URL, він виступає клієнтом веб-сервера або, іншими словами, сервера HTTP. Саме він бере ініціативу на себе і спочатку надсилає серверу певну кількість команд. У цьому першому прикладі:

  • сервером буде утиліта [RawTcpServer];
  • клієнтом буде веб-браузер;

Спочатку запускаємо сервер на порту 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 :

Потім за допомогою браузера ми надсилаємо запит на URL [http://localhost:100], тобто ми вказуємо, що запитуваний сервер HTTP працює на порту 100 локального комп’ютера:

Image

Повернемося до вікна сервера:


(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...
  • рядок 5 — клієнт, який підключився;
  • рядки 9–22: низка текстових рядків, які він надіслав:
    • рядок 9: цей рядок має формат [GET URL HTTP/1.1]. Він запитує URL / та вимагає від сервера використовувати протокол HTTP 1.1;
    • рядок 10: цей рядок має формат [Host: serveur:port]. Регістр команди [Host] не має значення. Нагадаємо, що клієнт звертається до локального сервера, який працює на порту 100;
    • рядок 14: команда [User-Agent] вказує ідентифікатор клієнта;
    • рядок 15: команда [Accept] вказує, які типи документів приймає клієнт;
    • рядок 21: команда [Accept-Language] вказує, якою мовою бажано отримати запитувані документи, якщо вони існують кількома мовами;
    • рядок 11: команда [Connection] вказує бажаний режим з’єднання: [keep-alive] вказує, що з’єднання має підтримуватися до завершення обміну даними;
    • рядок 22: клієнт завершує свої команди порожнім рядком;

Ми завершуємо з’єднання, зупиняючи сервер:


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

21.4.2. Приклад 2

Тепер, коли ми знаємо команди, які надсилає браузер для запиту URL, ми будемо запитувати цей URL за допомогою нашого клієнта TCP [RawTcpClient]. Сервер Apache у Laragon (розділ |Встановлення Laragon|) буде нашим веб-сервером.

Запустимо Laragon, а потім веб-сервер Apache:

Image

Image

Тепер за допомогою браузера відкриємо сторінку URL [http://localhost:80]. Тут ми вказуємо лише [localhost:80], а не URL документа. У цьому випадку запитується URL /, тобто корінь веб-сервера:

Image

  • в [1], тобто запитується URL. Спочатку було введено [http://localhost:80], і браузер (у даному випадку Firefox) просто перетворив її на [localhost], оскільки протокол [http] є неявним, коли протокол не вказано, а порт [80] є неявним, коли порт не вказано;
  • у [2] — коренева сторінка / запитуваного веб-сервера;

Тепер переглянемо текст, отриманий браузером:

Image

  • клацніть правою кнопкою миші на отриманій сторінці та виберіть опцію [2]. Ви отримаєте такий вихідний код:

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

Тепер надішлімо запит на URL [http://localhost:80] за допомогою нашого клієнта 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) :
  • у рядку 1 ми підключаємося до порту 80 сервера localhost. Саме там працює веб-сервер Laragon;

Тепер вводимо команди, які ми знайшли в попередньому абзаці:


(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...
  • у рядку 4 — команду [GET]. Запитуємо кореневий каталог / веб-сервера;
  • рядок 5, команда [Host];
  • це єдині дві необхідні команди. Для інших команд веб-сервер візьме значення за замовчуванням;
  • рядок 6 — порожній рядок, який повинен завершувати команди клієнта;
  • під рядком 6 йде відповідь веб-сервера;
  • рядки 7–12: заголовки HTTP у відповіді сервера;
  • рядок 13: порожній рядок, що позначає кінець заголовків HTTP;
  • рядки 14–82 — запитуваний у рядку 4 документ HTML;

Завантажуємо файл журналу [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>]
  • рядки 11–79: отримано документ HTML. У попередньому прикладі Firefox отримав те саме;

Тепер ми маємо основу для програмування клієнта TCP, який запитуватиме URL.

21.4.3. Приклад 3

Image

Скрипт [http/01/main.py] — це клієнт HTTP, налаштований файлом [config.py]. Його вміст такий:


def configure():
    # URLs для запиту
    urls = [
        # сайт: назва сайту, до якого потрібно підключитися
        # порт: порт веб-сервісу
        # GET: запитується URL
        # заголовки: заголовки HTTP, які потрібно надіслати у запиті
        # endOfLine: позначка кінця рядка у надісланих заголовках HTTP
        # encoding: кодування відповіді сервера
        # timeout: максимальний час очікування відповіді від сервера
        {
            "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
        }
    ]
    # повертається конфігурація
    return {
        "urls": urls
    }
  • вміст файлу — це список URL, кожен елемент якого є словником. Цей словник вказує, як підключитися до сайту, позначеного ключем [site];
  • рядки 4–10: значення ключів кожного словника;

Скрипт [http/01/main.py] має такий вигляд:


# імпортує
import codecs
import socket


# -----------------------------------------------------------------------
def get_url(url: dict, suivi: bool = True):
    # зчитує URL-адресу URL сайту url["GET"] та зберігає її у файлі url[site].html
    # діалог між клієнтом і сервером відбувається за протоколом HTTP, зазначеним у словнику [url]
    # винятки передаються нагору

    sock = None
    html = None
    try:
        # підключення до [site] на порту 80 з тайм-аутом
        site = url['site']
        sock = socket.create_connection((site, int(url['port'])), float(url['timeout']))

        # з'єднання являє собою двосторонній канал зв'язку
        # між клієнтом (цією програмою) та веб-сервером, до якого встановлено з’єднання
        # цей канал використовується для обміну командами та інформацією
        # протокол зв’язку — HTTP

        # створення файлу site.html — замінюємо небажані символи на ім’я файлу
        site2 = site.replace("/", "_")
        site2 = site2.replace(".", "_")
        html_filename = f'{site2}.html'
        html = codecs.open(f"output/{html_filename}", "w", "utf-8")

        # клієнт розпочне діалог HTTP із сервером
        if suivi:
            print(f"Client : début de la communication avec le serveur [{site}]")

        # залежно від сервера, рядки клієнта повинні закінчуватися символами \n або \r\n
        end_of_line = url["endOfLine"]
        # клієнт надсилає команду GET, щоб запитати конфігурацію URL
        # синтаксис GET URL HTTP/1.1
        commande = f"GET {url['GET']} HTTP/1.1{end_of_line}"
        # відстеження?
        if suivi:
            print(f"--> {commande}", end='')
        # команда надсилається на сервер
        sock.send(bytearray(commande, 'utf-8'))
        # генерування заголовків HTTP
        for verb, value in url['headers'].items():
            # формується команда для відправлення
            commande = f"{verb}: {value}{end_of_line}"
            # відстеження?
            if suivi:
                print(f"--> {commande}", end='')
            # команда надсилається на сервер
            sock.send(bytearray(commande, 'utf-8'))
        # надсилається заголовок HTTP [Connection: close], щоб попросити веб-сервер
        # закрити з'єднання після відправлення запитуваного документа
        sock.send(bytearray(f"Connection: close{end_of_line}", 'utf-8'))
        # заголовки (headers) протоколу HTTP повинні закінчуватися порожнім рядком
        sock.send(bytearray(end_of_line, 'utf-8'))
        #
        # сервер тепер відповість через канал sock. Він надішле всі
        # свої дані, а потім закриє канал. Отже, клієнт зчитує все, що надходить з sock
        # до закриття каналу
        #
        # спочатку зчитуються заголовки HTTP, надіслані сервером
        # вони також закінчуються порожнім рядком
        if suivi:
            print(f"Réponse du serveur [{site}]")

        # зчитування сокета так, ніби це текстовий файл
        encoding = f"{url['encoding']}" if url['encoding'] else None
        if encoding:
            file = sock.makefile(encoding=encoding)
        else:
            file = sock.makefile()
        # цей файл обробляється рядок за рядком
        fini = False
        while not fini:
            # читання поточного рядка
            ligne = file.readline().strip()
            # чи є непорожній рядок?
            if ligne:
                if suivi:
                    # виводимо заголовок HTTP
                    print(f"<-- {ligne}")
            else:
                # це був порожній рядок — заголовки HTTP завершені
                fini = True
        # зчитується документ HTML, який йтиме після порожнього рядка
        # читання поточного рядка
        ligne = file.readline()
        while ligne:
            # запис у файл журналу
            html.write(str(ligne))
            # наступний рядок
            ligne = file.readline()
            # цикл завершується, коли сервер закриває з'єднання
    finally:
        # клієнт закриває з'єднання
        if sock:
            sock.close()
        # закриття HTML-файлу
        if html:
            html.close()


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

# налаштування програми
import config
config = config.configure()

# отримання URL з файлу конфігурації
for url in config['urls']:
    print("-------------------------")
    print(url['site'])
    print("-------------------------")
    try:
        # читання URL з сайту [site]
        get_url(url)
    except BaseException as erreur:
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
# кінець
print("Terminé...")

Коментарі до коду:

  • рядки 108–109: завантажується словник [config] з модуля [config.py];
  • рядки 111–122: цей словник використовується;
  • рядки 118, 7: функція [get_url(url)] запитує документ з веб-сайту url[site] і зберігає його у текстовому файлі url[site].HTML. За замовчуванням обмін даними між клієнтом і сервером реєструється в консолі (suivi=True);
  • все це здійснюється у функції [try / finally] (рядки 14–96). Клаузули [except] немає. Винятки передаються до коду, що викликає функцію, і саме він їх зупиняє та виводить на екран (рядки 119–120);
  • рядки 16–17: відкриття з’єднання з веб-сервером. Функція [socket.create_connection] приймає три параметри:
    • [param1] — ім’я комп’ютера в Інтернеті, до якого потрібно підключитися;
    • [param2] — номер порту служби, до якої потрібно підключитися;
    • [param3]: [socket.create_connection] повертає сокет, а [param3], якщо він присутній, визначає тайм-аут створеного сокета. Тайм-аут — це максимальний час очікування сокета, коли він чекає на відповідь від віддаленого комп’ютера;
  • рядки 27–28: створення файлу [site.html], у якому буде збережено отриманий документ HTML;
  • рядки 34–43: першою командою клієнта має бути команда [GET URL HTTP/1.1];
  • рядок 43: функція [sock.send] дозволяє клієнту надсилати дані на сервер. У цьому випадку надісланий рядок тексту має таке значення: «Я хочу (GET) сторінку [URL] з веб-сайту, до якого я підключений. Я працюю з протоколом HTTP версії 1.1»;
  • рядок 43: команда [sock.send(bytearray(commande, 'utf-8'))] надсилає масив байтів (bytearray). Цей масив отримується шляхом перетворення рядка [commande] у послідовність байтів, закодованих у UTF-8;
  • рядки 44–52: надсилаються інші рядки протоколу HTTP [Host, User-Agent, Accept, Accept-Language…]. Їхній порядок не має значення;
  • рядки 53–55: надсилається заголовок HTTP [Connection: close], щоб попросити сервер закрити з’єднання після надсилання запитуваного документа. За замовчуванням він цього не робить. Тому про це потрібно явно попросити. Перевага полягає в тому, що це закриття буде виявлено на стороні клієнта, і саме так він дізнається, що отримав весь запитуваний документ;
  • рядки 56–57: на сервер надсилається порожній рядок, щоб повідомити, що клієнт завершив надсилання своїх заголовків HTTP і тепер очікує на запитуваний документ;
  • рядки 68–86: сервер спочатку надішле серію заголовків HTTP, які містять різну інформацію про запитуваний документ. Ці заголовки закінчуються порожнім рядком;
  • рядки 69–73: щоб прочитати відповідь сервера рядок за рядком, використовується метод [sock.makefile(encoding=encoding)]. Необов’язковий параметр [encoding] визначає кодування очікуваного тексту. Після цієї операції потік рядків, надісланих сервером, можна буде прочитати як звичайний текстовий файл;
  • рядок 78: за допомогою методу [readline] зчитується рядок, надісланий сервером. З нього видаляються пробіли (пробіли, символи кінця рядка) на початку та в кінці рядка;
  • рядки 81–83: якщо рядок не порожній і було подано запит на відстеження, отриманий рядок виводиться на консоль;
  • рядки 84–86: якщо отримано порожній рядок, що позначає кінець заголовків HTTP, надісланих сервером, то цикл із рядка 76 зупиняється;
  • рядки 90–95: текстові рядки відповіді сервера можна зчитувати по одному за допомогою циклу while і записувати у текстовий файл [html]. Коли веб-сервер надіслав усю запитувану сторінку, він закриває з’єднання з клієнтом. На стороні клієнта це буде розпізнано як кінець файлу, і цикл у рядках 90–95 буде завершено;
  • рядки 96–102: незалежно від того, чи сталася помилка, звільняються всі ресурси, використані кодом;

Результати:

Консоль виводить такі записи журналу:


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/курси-уроки-з-програмування
<-- 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/курси-та-посібники-з-програмування/
<-- Set-Cookie: SERVERID68971=2620178|XwH/h|XwH/h; path=/
<-- X-IPLB-Instance: 17095
Terminé...

Process finished with exit code 0

Коментарі

  • рядок 12: знайдено URL [http://localhost/] (код 200);
  • рядок 29: URL [http://sergetahe.com/] не знайдено (код 302). Код 302 означає, що адреса запитуваної сторінки змінилася на URL. Нова сторінка вказана у заголовку рядка 36;
  • рядок 49: запит, надісланий на сервер [http://tahe.developpez.com], є некоректним (код 400);
  • рядок 65: URL [http://www.sergetahe.com/] не знайдено (код 301). Код 301 означає, що запитувана сторінка остаточно змінила свою адресу URL. Нова сторінка вказана у заголовку рядка 71;

Як правило, коди 3xx, 4xx та 5xx сервера HTTP є кодами помилок.

У результаті виконання було створено такі файли:

Image

Отриманий файл [output/localhost.HTML] має такий вигляд:


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

Ми дійсно отримали той самий документ, що й у браузері Firefox.

Отриманий документ [output/sergetahe_com.html] має такий вигляд:

Image

Більшість HTTP-серверів надсилають відповіді на запити частинами. Кожній надісланій частині передує рядок, що вказує кількість байтів наступної частини. Це дозволяє клієнту прочитати саме цю кількість байтів, щоб отримати частину. У цьому випадку 0 означає, що наступна частина має нуль байтів. Нагадаємо, що сервер повідомив, що документ [http://sergetahe.com/] змінився з URL. Отже, він не надіслав жодного документа.

Документ [output/tahe_developpez_com.html] має такий вигляд:


<!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>
  • рядки 1–12: сервер надіслав документ HTML, незважаючи на те, що запит був некоректним (рядок 49 результатів). Документ HTML дозволяє серверу вказати причину помилки. Вона вказана у рядках 6 та 7:
    • рядок 7: наш клієнт використовував протокол HTTP;
    • рядок 8: сервер працює з протоколом HTTPS (S = безпечний) і не підтримує протокол HTTP;

Документ [output/www_sergetahe_com.html] має такий вигляд:


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

Тут також сталася помилка (рядок 3). Проте сервер надсилає документ HTML, у якому детально описано цю помилку (рядки 1–7).

21.4.4. Приклад 4

Попередні приклади показали, що наш клієнт HTTP виявився недостатнім. Тепер ми представимо інструмент під назвою [curl], який дозволяє завантажувати веб-документи, вирішуючи згадані проблеми: протокол HTTPS, документ, що надсилається частинами, перенаправлення… Інструмент [curl] було встановлено разом із Laragon:

Image

Відкриємо термінал PyCharm [1]:

Image

  • у [1] — доступ до терміналів PyCharm;
  • у [2-3] — вже активні термінали;
  • у [4] — папка, в якій ви перебуваєте. Далі це не має значення;

У терміналі вводимо таку команду:


(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

Те, що команда [curl –help] дала результати, свідчить про те, що команда [curl] знаходиться у PATH терміналу. У Windows PATH — це сукупність папок, які переглядаються, коли користувач вводить виконувану команду, у даному випадку [curl]. Значення PATH можна визначити:


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

У рядку 2 — папки PATH, розділені крапками з комою. У цьому списку немає папок, пов’язаних із Laragon. Якщо трохи дослідити, можна виявити, що в папці [c:\windows\system32] є [curl]. Саме він і відповідав раніше.

Якщо ви хочете скористатися інструментом [curl], що входить до складу Laragon, можна вчинити так:

Image

Image

  • у [2] — термінал Laragon;
  • на [3] — ця кнопка дозволяє створювати нові термінали, кожен з яких встановлюється у окрему вкладку вищезгаданого вікна;
  • в [4] запитується PATH терміналу Laragon;
  • результат значно відрізняється від того, що було отримано в терміналі PyCharm. Цей PATH містить численні папки, створені під час інсталяції Laragon. Папка, що містить інструмент [curl], є однією з них:

Image

Далі використовуйте термінал на свій вибір. Просто майте на увазі, що якщо ви хочете скористатися інструментом, який надає Laragon, краще використовувати термінал Laragon.

Команда [curl --help] відображає всі параметри налаштування [curl]. Їх є кілька десятків. Ми будемо використовувати лише деякі з них. Щоб отримати URL, достатньо ввести команду [curl URL]. Ця команда відобразить на консолі запитуваний документ. Якщо ж нам потрібні дані обміну HTTP між клієнтом і сервером, ми введемо [curl --verbose URL]. Нарешті, щоб зберегти запитуваний документ HTML у файл, ми введемо [curl --verbose --output fichier URL].

Щоб уникнути захаращення файлової системи нашого комп’ютера, перейдемо в інше місце (я тут використовую термінал 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                                          
  • у 3-му рядку перейдемо до папки [c:\temp]. Якщо цієї папки немає, ви можете створити її або вибрати іншу;
  • у рядку 6 створюємо папку з назвою [curl];
  • у рядку 9 переходимо до цієї папки;
  • у рядку 12 переглядаємо його вміст. Він порожній (рядок 20);

Переконайтеся, що сервер Apache в Laragon запущений, і за допомогою [curl] запитуйте URL та [http://localhost/] за допомогою команди [curl –verbose –output localhost.html http://localhost/]. Отримаємо такі результати:


λ 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 до хосту localhost залишено без змін
  • рядки 10–13: рядки, надіслані [curl] на сервер [localhost]. Можна впізнати протокол HTTP;
  • рядки 14–20: рядки, надіслані у відповідь сервером;
  • рядок 14: вказує, що запитуваний документ було отримано;

Файл [localhost.html] містить запитуваний документ. Ви можете це перевірити, завантаживши файл у текстовий редактор.

Тепер запитаємо URL [https://tahe.developpez.com:443/]. Щоб отримати цей URL, клієнт HTTP повинен розуміти мову HTTPS. Це стосується клієнта [curl].

Результати консолі такі:


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 до хосту tahe.developpez.com залишено без змін
  • рядки 10–39: обмін даними між клієнтом і сервером для забезпечення безпеки з’єднання: воно буде зашифроване;
  • рядки 41–44: заголовки HTTP, надіслані клієнтом [curl] на сервер;
  • рядок 52: запитуваний документ було знайдено;
  • рядок 57: документ надсилається частинами;

[curl] коректно обробляє як захищений протокол HTTPS, так і те, що документ надсилається частинами. Надісланий документ можна знайти тут, у файлі [tahe.developpez.com.html].

Тепер запитаємо URL [http://sergetahe.com/cours-tutoriels-de-programmation]. Ми бачили, що для цього URL було перенаправлення на URL та [http://sergetahe.com/cours-tutoriels-de-programmation/] (з символом / наприкінці).

Результати виведення на консоль такі:


C:\Temp\curl
λ curl --verbose --output sergetahe.com.html --location http://sergetahe.com/курси-уроки-з-програмування
  % 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/курси-уроки-програмування/
< 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 для розміщення sergetahe.com без змін
* Issue another request to this URL: 'http://sergetahe.com/курси-уроки-програмування/'
* Found bundle for host sergetahe.com: 0x14385f8 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) з хостом 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/курси-уроки-програмування/
< 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 на хост sergetahe.com залишився без змін
* Issue another request to this URL: 'https://sergetahe.com/курси-посібники-з-програмування/'
*   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/курси-уроки-програмування/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/курси-та-посібники-з-програмування/>; 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 для розміщення sergetahe.com без змін
  • рядок 2: використовується опція [--location], щоб вказати, що потрібно слідувати перенаправленням, надісланим сервером;
  • рядок 13: сервер повідомляє, що запитуваний документ змінився на URL;
  • рядок 18: вказується нова адреса URL запитуваного документа;
  • рядок 31: [curl] надсилає новий запит, цього разу на нову адресу URL;
  • рядок 36: сервер знову відповідає, що URL змінився;
  • рядок 41: новий URL є точно таким самим, як той, що був перенаправлений, за винятком однієї деталі: змінився протокол. Він став HTTPS (рядок 41), тоді як раніше це був http (рядок 31);
  • рядок 49: надсилається новий запит до нового URL. Цей запит зашифровано. Тому розпочинається весь діалог щодо налаштування безпеки, рядки 53–91;
  • рядок 92: запитується новий URL, цього разу з протоколом HTTP/2;
  • рядок 100: документ знайдено;

Запитаний документ буде знайдено у файлі [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. Приклад 5

Python має модуль під назвою [pyccurl], який дозволяє використовувати можливості інструменту [curl] у програмі Python. Встановлюємо цей модуль:

Image

Напишемо новий скрипт [http/02/main.py]:

Image

Файл [http/02/config] має такий вигляд:


def configure():
    # список URL, які потрібно перевірити
    urls = [
        # сайт: сервер, до якого потрібно підключитися
        # timeout: максимальний час очікування відповіді від сервера
        # target: URL-адреса, до якої слід надіслати запит
        # кодування: кодування відповіді сервера
        {
            "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"
        }
    ]
    # надаємо конфігурацію
    return {
        'urls': URL-адреси
    }

Файл містить список словників, кожен з яких має таку структуру:

  • site: ім’я веб-сервера;
  • encoding: тип кодування очікуваного документа;
  • timeout: максимальний час очікування відповіді сервера, виражений у мілісекундах. Після закінчення цього часу клієнт роз'єднається;
  • url: URL запитуваного документа;

Код скрипта [http/02/main.py] такий:


# імпортує
import codecs
from io import BytesIO

import pycurl


# -----------------------------------------------------------------------
def get_url(url: dict, suivi=True):
    # зчитує URL-адресу URL і зберігає її у файлі output/url['site'].html
    # якщо [suivi=True], то здійснюється консольний моніторинг обміну даними між клієнтом і сервером
    # url[timeout] — це час очікування для клієнтських запитів;
    # URL [encoding] — це кодування запитуваного документа

    # отримуються дані конфігурації
    server = url['site']
    timeout = url['timeout']
    target = url['target']
    encoding = url['encoding']
    # відстеження
    print(f"Client : début de la communication avec le serveur [{server}]")

    # дозволяємо винятків просуватися вгору
    html = None
    curl = None
    try:
        # Ініціалізація сеансу cURL
        curl = pycurl.Curl()
        # бінарний потік
        flux = BytesIO()
        # параметри curl
        options = {
            # URL
            curl.URL: target,
            # WRITEDATA: місце, де будуть зберігатися отримані дані
            curl.WRITEDATA: flux,
            # режим детального виведення
            curl.VERBOSE: suivi,
            # нове з'єднання — без кешу
            curl.FRESH_CONNECT: True,
            # час очікування запиту (у секундах)
            curl.TIMEOUT: timeout,
            curl.CONNECTTIMEOUT: timeout,
            # не перевіряти дійсність сертифікатів SSL
            curl.SSL_VERIFYPEER: False,
            # дотримуватися перенаправлень
            curl.FOLLOWLOCATION: True
        }
        # налаштування curl
        for option, value in options.items():
            curl.setopt(option, value)
        # Виконання запиту CURL із зазначеними параметрами
        curl.perform()
        # створення файлу server.html — заміна небажаних символів у імені файлу
        server2 = server.replace("/", "_")
        server2 = server2.replace(".", "_")
        html_filename = f'{server2}.html'
        html = codecs.open(f"output/{html_filename}", "w", encoding)
        # збереження отриманого документа у файлі HTML
        html.write(flux.getvalue().decode(encoding))
    finally:
        # звільнення ресурсів
        if curl:
            curl.close()
        if html:
            html.close()


# -------------------головне
# налаштування програми
import config
config = config.configure()

# отримання URL з файлу конфігурації
for url in config['urls']:
    print("-------------------------")
    print(url['site'])
    print("-------------------------")
    try:
        # читання URL з сайту [site]
        get_url(url)
    # за винятком BaseException як помилки:
    #     print(f"Сталася така помилка: {помилка}")
    finally:
        pass
# кінець
print("Terminé...")

Коментарі

  • рядок 5: імпортуємо модуль [pycurl];
  • рядок 3: імпортується клас [BytesIO], який дозволить нам зберігати дані, отримані від сервера, у бінарному потоці;
  • рядки 70–72: отримуємо конфігурацію додатка;
  • рядки 75–85: виконується цикл по списку URL, знайдених у конфігурації;
  • рядок 81: для кожного з URL викликається функція [get_url], яка завантажить URL url[‘target’] із тайм-аутом url['timeout'];
  • рядок 9: функція [get_url] отримує конфігурацію URL, яку потрібно запитати;
  • рядки 16–19: конфігурація URL зберігається в окремих змінних;
  • рядки 26, 61: усі операції виконуються в блоці try / finally. Винятки не перехоплюються, тому вони передаються до викличного коду, який їх перехоплює;
  • рядок 28: готуємо сесію [curl]. [pycurl.Curl()] повертає ресурс [curl], який буде здійснювати транзакцію з сервером;
  • рядок 30: створення екземпляра бінарного потоку, який буде зберігати отримані дані;
  • рядки 32–48: словник [options] налаштовує з’єднання [curl] із сервером. Їхня роль вказана в коментарях;
  • рядки 49–51: параметри з’єднання передаються до ресурсу [curl];
  • рядок 53: запит на з’єднання з URL із заданими параметрами. Завдяки параметру [curl.WRITEDATA: flux] (рядок 36) функція [curl.perform()] збереже отримані дані у [flux];
  • рядки 54–60: створюється файл HTML, у якому буде збережено отриманий документ HTML;
  • рядок 60: бінарний потік [flux.getvalue()] буде збережено як рядок символів у файлі HTML. Кодування цього рядка вказано в методі [decode(encoding)]. Тому необхідно знати кодування документа, надісланого сервером. У разі помилки операція декодування бінарного потоку завершиться невдачею. Кодування вказано у файлі конфігурації URL (наприклад, у рядку 12). Цю інформацію можна було б обробляти динамічно, оскільки сервер надсилає її у своїх заголовках HTTP. Це було б кращим варіантом. Однак, щоб зберегти простоту коду, ми цього не зробили. Щоб дізнатися тип кодування документа, достатньо завантажити потрібний файл URL у браузері та переглянути заголовки HTTP, надіслані ним у режимі налагодження браузера (F12), або ж сам документ, оскільки він також вказує кодування:

Image

Image

  • рядки 61–66: виділені ресурси звільняються;

При виконанні скрипта [main.py] отримуємо такі результати у консолі:


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/курси-уроки-програмування
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 на хості sergetahe.com залишилося без змін
* Issue another request to this URL: 'http://sergetahe.com/курси-уроки-з-програмування'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) з хостом 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/курси-та-посібники-з-програмування/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 на хост sergetahe.com залишився без змін
* Issue another request to this URL: 'http://sergetahe.com/курси-уроки-з-програмування/'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) з хостом 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/курси-уроки-програмування/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 на хост sergetahe.com залишився без змін
* Issue another request to this URL: 'https://sergetahe.com/курси-та-посібники-з-програмування/'
*   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/курси-уроки-програмування/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/курси-та-посібники-з-програмування/>; rel=shortlink
< vary: Accept-Encoding
< x-iplb-instance: 17080
< set-cookie: SERVERID68971=26218|XwLIp|XwLIp; path=/
< 
* Connection #1, щоб розмістити sergetahe.com без змін
-------------------------
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, щоб розмістити tahe.developpez.com без змін
-------------------------
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 для розміщення www.polytech-angers.fr залишено без змін
* 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) з хостом 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 до хосту www.polytech-angers.fr залишився без змін
-------------------------
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 на хост localhost залишився без змін
Terminé...

Process finished with exit code 0

Коментарі

  • синім кольором — HTTP-команди, надіслані на сервер;
  • зеленим кольором — дані, отримані клієнтом у відповідь;
  • отримуємо ті самі обміни даними, що й із інструментом [curl];
    • рядок 9: запитується URL [http://sergetahe.com/];
    • рядок 15: сервер відповідає, що сторінка перемістилася. Рядок 21: новий URL;
    • рядок 32: запитується URL [http://sergetahe.com/cours-tutoriels-de-programmation];
    • рядок 38: сервер відповідає, що сторінка переміщена. Рядок 43, новий URL;
    • рядок 54: запитується URL [http://sergetahe.com/cours-tutoriels-de-programmation/];
    • рядок 60: сервер повідомляє, що сторінка переміщена. Рядок 65 — нова сторінка URL. Вона використовує захищений протокол [HTTPS];
    • рядки 71–75: встановлюється безпечний протокол із сервером;
    • рядок 76: запитується URL [https://sergetahe.com/cours-tutoriels-de-programmation/];
    • рядок 82: запитуваний документ знайдено;

21.4.6. Висновок

У цьому розділі ми ознайомилися з протоколом HTTP та написали скрипт [http/02/main.py], здатний завантажувати URL з Інтернету.

21.5. Протокол SMTP (Simple Mail Transfer Protocol)

21.5.1. Вступ

Image

У цьому розділі:

  • [Serveur B] — це локальний сервер SMTP, який ми встановимо;
  • [Client A] — це клієнт SMTP у різних формах:
    • клієнт [RawTcpClient] для виявлення протоколу SMTP;
    • скрипт на Python, що відтворює протокол SMTP клієнта [RawTcpClient];
    • скрипт на Python, що використовує модуль [smtplib], який дозволяє надсилати будь-які листи;

21.5.2. Створення адреси [gmail]

Для проведення наших тестів SMTP нам знадобиться електронна адреса, на яку можна надсилати листи. Для цього ми створимо адресу Gmail [https://www.google.com/intl/fr/gmail/about/]:

Image

Примітка: Надішліть кілька листів на створену вами адресу. Переходьте до наступного кроку лише тоді, коли переконаєтеся, що створений обліковий запис може отримувати листи.

21.5.3. Встановлення сервера SMTP

Для наших тестів ми встановимо поштовий сервер [hMailServer], який є одночасно сервером SMTP для відправлення листів, а також сервером POP3 (Post Office Protocol), що дозволяє читати листи, збережені на сервері, та сервером IMAP (Internet Message Access Protocol), який також дозволяє читати листи, збережені на сервері, але має ширші можливості. Зокрема, він дозволяє керувати зберіганням листів на сервері.

Поштовий сервер [hMailServer] доступний на URL [https://www.hmailserver.com/] (травень 2019 року).

Image

Під час встановлення вам буде запропоновано ввести певну інформацію:

Image

  • у [1-2] виберіть як поштовий сервер, так і інструменти для його адміністрування;
  • під час встановлення вас попросять ввести пароль адміністратора: запишіть його, оскільки він вам знадобиться;

[hMailServer] встановлюється як служба Windows, що запускається автоматично під час завантаження системи. Краще вибрати ручний запуск:

  • у [3] введіть [services] у поле введення на панелі стану;

Image

  • у [4-8] переведіть службу в режим [manuel] (6) та запустіть її (7);

Після запуску сервер [hMailServer] потрібно налаштувати. Сервер було встановлено разом із програмою адміністрування [hMailServer Administrator]:

Image

  • у [2] у полі введення на панелі стану введіть [hmailserver];
  • у [3] запустіть адміністратора;
  • у [4] підключіть адміністратора до сервера [hMailServer];
  • у [5] введіть пароль, який ви вказали під час встановлення [hMailServer];

Якщо ви забули пароль, виконайте такі дії:

  • зупиніть сервер [hMailServer];
  • відкрийте файл [<hmailserver>/bin/hmailserver.ini], де <hmailserver> — це папка, в яку встановлено сервер:

Image

  • у файлі [100] видаліть пароль із рядка [AdministratorPassword]. Це призведе до того, що адміністратор більше не матиме пароля. Просто введіть [Entrée], коли його буде запрошено;

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

Продовжимо налаштування сервера:

Image

  • у рядку [1-2] додайте домен (якщо його ще немає);

Image

  • у полі [3] можна вказати практично будь-що для тестів, які ми будемо проводити. Насправді слід вказати ім’я існуючого домену;

Image

Ми створимо обліковий запис користувача:

  • клацніть правою кнопкою миші на [Accounts] (7), а потім (8), щоб додати нового користувача;
  • у вкладці [General] (9) ми створюємо користувача [guest] (10) з паролем [guest] (11). Його електронна адреса — [guest@localhost] (10);
  • у [12] активовано користувача [guest];

Image

  • у [13-14] створено користувача;

Image

  • у [27] — порт служби SMTP;
  • у [28] вказано, що ця служба не вимагає автентифікації;
  • у полі [30] вкажіть вітальне повідомлення, яке сервер SMTP надсилатиме своїм клієнтам;

Image

Те саме робимо з сервером POP3:

Image

Те саме робимо для сервера IMAP:

Image

Вказуємо домен за замовчуванням для сервера [hMailServer] (їх може бути кілька) — :

Image

  • у [37] вкажіть, що доменом за замовчуванням для сервера SMTP є той, який ви створили у [38];

Після збереження цієї конфігурації ви можете перевірити її таким чином. Відкрийте термінал PyCharm у папці утиліт:

Image

Потім введіть таку команду:


(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]
  • рядок 1: встановлюється з’єднання з портом 25 машини [localhost]. Саме там працює незахищений сервер SMTP, що належить серверу [hMailServer];
  • рядок 4: отримуємо привітальне повідомлення, яке ми налаштували на попередньому кроці 30;

Отже, сервер SMTP успішно налаштований. Введіть команду [quit], щоб завершити діалог із сервером SMTP на порту 25.

Тепер зробимо те саме з портом 587, який є портом за замовчуванням для захищеної служби 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]
  • рядок 4 — відповідь сервера SMTP, що працює на порту 587;

Тепер зробимо те саме з портом 110, який є портом за замовчуванням для служби POP3, що забезпечує релейну передачу пошти:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 110
Client [DESKTOP-30FF5FB:50210] connecté au serveur [localhost-110]
Tapez vos commandes (quit pour arrêter) :
<-- [+OK Bienvenue sur le serveur POP3 localhost.com]
  • у рядку 4 ми отримали привітальне повідомлення від сервера POP3;

Тепер зробимо те саме з портом 143, який є портом за замовчуванням для служби 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]
  • у рядку 4 ми отримали привітальне повідомлення від сервера IMAP;

21.5.4. Встановлення поштового клієнта

Щоб прочитати листа, який ми збираємося надіслати, нам потрібна програма для читання електронної пошти. Для тих, у кого її немає, ми покажемо, як встановити та налаштувати програму [Thunderbird]:

  • у форматі [1]: завантажте [thunderbird], а потім встановіть його;

Image

  • запустіть поштовий сервер [hMailServer], якщо він ще не запущений;
  • у [2-3]: після запуску Thunderbird ми створимо поштовий обліковий запис для користувача [guest@localhost] на поштовому сервері [hMailServer];

Image

Image

Image

  • на [7-11]: сервер POP3, який дозволить нам читати пошту з поштового сервера [hMailServer], знаходиться за адресою [localhost] і працює на порту 110;
  • у [12-16]: сервер SMTP, який дозволить нам надсилати листи від імені користувачів поштового сервера [hMailServer], розташований за адресою [localhost] і працює на порту 25;
  • [18]: можна перевірити правильність цієї конфігурації;

Image

Image

  • на [26]: оскільки немає шифрування на SSL, Thunderbird попереджає, що наша конфігурація містить ризики;
  • в [28]: обліковий запис створено;

Щоб перевірити створений обліковий запис, за допомогою Thunderbird ми:

  • надішлемо листа користувачеві [guest@localhost.com] (протокол SMTP);
  • прочитаємо лист, отриманий цим користувачем (протокол POP3);

Image

  • у [3]: відправник;
  • у [4]: одержувач;
  • у [5]: тема листа;
  • у [6]: зміст листа;
  • у [7]: для відправлення листа;

Image

  • у [8-9]: отримуємо пошту від користувача [guest@localhost];
  • у [10-15]: отримане повідомлення;

Ми також надішлемо листа користувачеві [pymailparlexemple@gmail.com]. Створимо для нього обліковий запис у Thunderbird, щоб він міг читати отримані листи:

Image

Image

  • у [4]: вкажіть те, що бажаєте;
  • у полі [5]: адреса — [pymailparlexemple@gmail.com];
  • у полі [6]: введіть пароль, який ви призначили цьому користувачеві під час його створення;
  • у полі [7]: підтвердіть цю конфігурацію;

Image

  • у полі [8]: Thunderbird отримав наступну інформацію зі своєї бази даних;
  • у [9]: протокол читання пошти більше не POP3, а IMAP. Головна відмінність між ними полягає в тому, що [POP3] завантажує прочитані листи на локальний комп’ютер, де встановлено поштовий клієнт, і видаляє їх із віддаленого сервера, тоді як [IMAP] зберігає листи на віддаленому сервері;
  • у [10]: ідентифікатор сервера SMTP;
  • у [13]: щоб отримати додаткову інформацію про сервери IMAP та SMTP, переходимо до ручного налаштування;

Image

  • в [14-17]: характеристики сервера IMAP;
  • на [18-21]: параметри сервера SMTP;
  • у [22]: завершуємо налаштування;

Image

  • у [23-24]: новий обліковий запис Thunderbird;
  • у [26]: написання нового повідомлення;

Image

  • в [27]: відправник — [pymailparlexemple@gmail.com];
  • у [28]: одержувач — [pymailparlexemple@gmail.com];
  • у [29-30] — саме повідомлення;
  • у [31]: для відправлення;

Image

  • у [32]: завантажується пошта з різних облікових записів;

Image

  • у [33-36]: пошта, отримана користувачем [pymailparlexemple@gmail.com]

Так само ми створюємо:

  • новий обліковий запис Gmail [pymail2parlexemple@gmail.com];
  • новий обліковий запис Thunderbird [pymail2parlexemple@gmail.com] для завантаження повідомлень користувача з таким самим ім’ям:

Image

Image

Тепер ми маємо інструменти для вивчення протоколів SMTP, POP3 та IMAP. Почнемо з протоколу SMTP.

21.5.5. Протокол SMTP

Image

Ми ознайомимося з протоколом SMTP, проаналізувавши журнали сервера [hMailServer]. Для цього ми активуємо їх за допомогою команди [hmailServerAdministrator]:

Image

Image

  • у [2] журнали активовано;
  • у [3-5]: ми активуємо їх для протоколів SMTP, POP3, IMAP;
  • у [7] — запитується їх перегляд;
  • у [8] відкриває файл журналів у будь-якому текстовому редакторі;

Image

У наведеному нижче прикладі клієнтом буде [Thunderbird], а сервером — [hMailServer]. У Thunderbird слід зробити так, щоб користувач [guest@localhost.com] надіслав собі повідомлення:

Image

У цьому випадку журнали матимуть такий вигляд:


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

Наведені вище рядки описують діалог, що відбувся між клієнтом SMTP (поштовим клієнтом Thunderbird) та сервером SMTP (hMailServer). Рядки [SENT] вказують, що сервер SMTP надіслав своєму клієнту. Рядки [RECEIVED] вказують, що сервер SMTP отримав від свого клієнта.

  • рядок 1: одразу після підключення клієнта до сервера SMTP останній надсилає клієнту привітальне повідомлення;
  • рядок 2: клієнт надсилає команду [EHLO] для ідентифікації. Тут він вказує свою адресу IP [127.0.0.1], яка позначає машину [localhost], тобто машину, на якій працює клієнт SMTP;
  • рядок 3: сервер надсилає послідовність відповідей [250]. [nl] означає [newline], тобто символ \n. Відповіді мають вигляд [250-], за винятком останньої, яка має вигляд [250 ]. Таким чином клієнт SMTP розуміє, що відповідь сервера SMTP завершилася і що він може надіслати команду. Серія команд [250] мала на меті вказати клієнту SMTP набір команд, які він міг використовувати;
  • рядок 4: клієнт SMTP надсилає команду [MAIL FROM : adresse_mail_expéditeur], яка вказує, хто надсилає повідомлення;
  • рядок 5: сервер SMTP відповідає командою [250 OK], вказуючи, що він зрозумів команду;
  • рядок 6: клієнт SMTP надсилає команду [RCPT TO : adresse_mail_destinataire], щоб вказати адресу одержувача;
  • рядок 7: сервер SMTP знову підтверджує, що він зрозумів команду;
  • рядок 8: сервер SMTP надсилає команду [DATA]. Це означає, що він збирається надіслати вміст повідомлення;
  • рядок 9: сервер SMTP у відповіді [354 OK] повідомляє, що готовий прийняти повідомлення. Текст [send .] вказує, що клієнт SMTP повинен закінчити своє повідомлення рядком, що містить лише одну крапку;
  • далі не видно, що клієнт SMTP надсилає своє повідомлення. Журнали цього не відображають;
  • рядок 10: клієнт SMTP надіслав крапку, що позначає кінець повідомлення. Сервер SMTP відповідає йому, що помістив повідомлення в чергу (queued);
  • клієнт SMTP надсилає йому команду [QUIT], щоб повідомити, що він збирається закрити з’єднання;
  • рядок 12: сервер відповідає йому;

Тепер, коли ми знаємо діалог «клієнт-сервер» протоколу SMTP, спробуємо відтворити його з нашим клієнтом [RawTcpClient]. Ми використовуємо термінал PyCharm:

Image

Розглянемо новий приклад:

  • клієнтом A буде загальний клієнт TCP, [RawTcpClient];
  • сервер B — це поштовий сервер [hMailServer];
  • клієнт A попросить сервер B доставити листа, надісланого користувачем [guest@localhost.com] для себе;
  • ми перевіримо, чи одержувач дійсно отримав надісланий лист;

Запускаємо клієнт наступним чином:


(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]
  • у рядку [1] ми підключаємося до порту 25 локальної машини, де працює служба SMTP від [hMailServer]. Аргумент [--quit bye] вказує, що користувач вийде з програми, ввівши команду [bye]. Без цього аргументу командою завершення роботи програми є [quit]. Однак [quit] також є командою протоколу SMTP. Тому нам потрібно уникнути цієї неоднозначності;
  • у рядку [2] клієнт успішно підключений;
  • у рядку [3] клієнт очікує на команди, введені з клавіатури;
  • рядок [4] — сервер надсилає клієнту привітальне повідомлення;

Ми продовжуємо діалог таким чином:


(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
  • у [5] клієнт надсилає команду [EHLO nom-de-la-machine-client]. Сервер відповідає йому серією повідомлень у форматі [250-xx] (6). Код [250] вказує на успішне виконання команди, надісланої клієнтом;
  • у [10] клієнт вказує відправника повідомлення, в даному випадку [guest@localhost.com];
  • у [11] — відповідь сервера;
  • у [12] вказується одержувач повідомлення, у даному випадку користувач [guest@localhost.com];
  • у [13] — відповідь сервера;
  • у [14] команда [DATA] повідомляє серверу, що клієнт збирається надіслати вміст повідомлення;
  • у [15] — відповідь сервера;
  • у [16-22] клієнт повинен надіслати список текстових рядків, що закінчується рядком, який містить лише одну крапку. Повідомлення може містити рядки [Subject:, From:, To:] (16–18) для визначення відповідно теми повідомлення, відправника та одержувача;
  • у форматі [19] за попередніми заголовками має йти порожній рядок;
  • у [20-21] — текст повідомлення;
  • у рядку [22] — рядок, що містить лише одну крапку, яка позначає кінець повідомлення;
  • у [23], як тільки сервер отримав рядок, що містить лише одну крапку, він ставить повідомлення в чергу;
  • у [24] клієнт повідомляє серверу, що він закінчив;
  • у [25] бачимо, що сервер закрив з’єднання з клієнтом;

Тепер перевіримо за допомогою Thunderbird, чи дійсно користувач [guest@localhost.com] отримав повідомлення:

Image

  • у записі [1-6] видно, що користувач [guest@localhost.com] дійсно отримав повідомлення;

Зрештою, наш клієнт [RawTcpClient] зміг надіслати повідомлення через сервер SMTP [localhost]. Тепер скористаємося тим самим методом, щоб надіслати повідомлення користувачеві [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
  • рядок 1: використовуємо сервер SMTP від Gmail, який працює на порту 587;
  • рядок 15: ми стикаємося з перешкодою, оскільки сервер SMTP вимагає від нас встановити захищене з’єднання, чого ми не вміємо робити. На відміну від попереднього прикладу, сервер [smtp.gmail.com] (рядок 1) вимагає автентифікації. Він приймає як клієнтів лише користувачів, зареєстрованих у домені [gmail.com]. Ця автентифікація є безпечною і відбувається в рамках зашифрованого з’єднання.

Перший приклад дав нам основи для створення базового клієнта SMTP на Python. Другий показав, що деякі сервери SMTP (а насправді більшість) вимагають аутентифікації, що здійснюється через зашифроване з’єднання.

21.5.6. Скрипти [smtp/01]: простий клієнт SMTP

Ми відтворимо на Python те, що раніше дізналися про протокол SMTP.

Image

Файл [smtp/01/config] налаштовує додаток наступним чином:


def configure() -> dict:
    return {
        # опис: опис надісланого листа
        # smtp-server: сервер SMTP
        # smtp-port: порт сервера SMTP
        # from: відправник
        # to: одержувач
        # subject: тема листа
        # message: текст листа
        "mails": [
            {
                "description": "mail to localhost via localhost",
                "smtp-server": "localhost",
                "smtp-port": "25",
                "from": "guest@localhost.com",
                "to": "guest@localhost.com",
                "subject": "to localhost via localhost",
                # надсилаємо UTF-8
                "content-type": 'text/plain; charset="utf-8"',
                # перевіряємо символи з діакритичними знаками
                "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",
                # надсилаємо UTF-8
                "Content-type": 'text/plain; charset="utf-8"',
                # тестуємо символи з діакритичними знаками
                "message": "aglaë séléné\nva au marché\nacheter des fleurs"
            }
        ]
    }
  • рядки 10–35: список листів, які потрібно надіслати. Для кожного з них вказується така інформація:
    • [description]: текст, що описує електронний лист;
    • [smtp-server]: сервер SMTP, який слід використовувати;
    • [smtp-port]: його порт;
    • [from]: відправник листа;
    • [to]: одержувач електронного листа;
    • [subject]: тема листа;
    • [content-type]: кодування електронного листа;
    • [message]: текст листа;

Код [01/main] клієнта SMTP має такий вигляд:


# імпорт
import socket


# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: bool):
    # надсилає повідомлення на SMTP-сервер smtpserver від імені відправника
    # для одержувача. Якщо verbose=True, здійснюється відстеження обміну даними між клієнтом і сервером

    # дозволяє відображати системні помилки
    connexion = None
    try:
        # ім'я локального комп'ютера (необхідне для протоколу SMTP)
        client = socket.gethostbyaddr(socket.gethostbyname("localhost"))[0]
        # відкриття з'єднання на порту 25 smtpServer
        connexion = socket.create_connection((mail["smtp-server"], 25))

        # з'єднання являє собою двосторонній канал зв'язку
        # між клієнтом (цією програмою) та SMTP-сервером, до якого встановлено з’єднання
        # цей канал використовується для обміну командами та інформацією

        # після встановлення з’єднання сервер надсилає привітальне повідомлення, яке ми читаємо
        send_command(connexion, "", verbose, True)
        # команда ehlo:
        send_command(connexion, f"EHLO {client}", verbose, True)
        # команда mail from:
        send_command(connexion, f"MAIL FROM: <{mail['from']}>", verbose, True)
        # команда rcpt to:
        send_command(connexion, f"RCPT TO: <{mail['to']}>", verbose, True)
        # команда data
        send_command(connexion, "DATA", verbose, True)
        # підготовка повідомлення для відправлення
        # воно має містити такі рядки
        # From: відправник
        # To: одержувач
        # порожній рядок
        # Повідомлення
        # .
        data = f"{mail['message']}"
        # відправлення повідомлення
        send_command(connexion, data, verbose, False)
        # відправлення .
        send_command(connexion, "\r\n.\r\n", verbose, False)
        # команда виходу
        send_command(connexion, "QUIT", verbose, True)
        # кінець
    finally:
        # закриття з'єднання
        if connexion:
            connexion.close()


# --------------------------------------------------------------------------
def send_command(connexion: socket, commande: str, verbose: bool, with_rclf: bool):
    # надсилання команди в канал з'єднання
    # режим детального виведення, якщо verbose=True
    # якщо with_rclf=True, додає послідовність rclf до команди

    # дані
    rclf = "\r\n" if with_rclf else ""
    # відправлення команди, якщо команда не порожня
    if commande:
        # дозволяє передавати системні помилки
        #
        # відправлення команди
        connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
        # можливе відлуння
        if verbose:
            affiche(commande, 1)
        # читання відповіді довжиною менше 1000 символів
        reponse = str(connexion.recv(1000), 'utf-8')
        # можливе відлуння
        if verbose:
            affiche(reponse, 2)
        # отримання коду помилки
        codeErreur = int(reponse[0:3])
        # помилка, повернута сервером?
        if codeErreur >= 500:
            # генерується виняток з цією помилкою
            raise BaseException(reponse[4:])
        # повернення без помилки


# --------------------------------------------------------------------------
def affiche(echange: str, sens: int):
    # вивести обмін на екран?
    # якщо sens=1, виводиться -->обмін
    # якщо sens=2, виводить <-- обмін без двох останніх символів rclf
    if sens == 1:
        print(f"--> [{echange}]")
        return
    elif sens == 2:
        l = len(echange)
        print(f"<-- [{echange[0:l - 2]}]")
        return


# головне ----------------------------------------------------------------

# клієнт SMTP (протокол передачі SendMail), що дозволяє надсилати повідомлення
# інформація береться з конфігураційного файлу, що містить такі дані для кожного сервера

# опис: опис надісланого листа
# smtp-server: сервер SMTP
# smtp-port: порт сервера SMTP
# from: відправник
# to: одержувач
# subject: тема листа
# message: текст листа


# протокол зв’язку «клієнт-сервер» SMTP
# -> клієнт підключається до порту 25 сервера SMTP
# <- сервер надсилає йому привітальне повідомлення
# -> клієнт надсилає команду EHLO: ім’я свого комп’ютера
# <- сервер відповідає OK або ні
# -> клієнт надсилає команду mail from: <відправник>
# <- сервер відповідає OK або ні
# -> клієнт надсилає команду rcpt to: <адресат>
# <- сервер відповідає OK або ні
# -> клієнт надсилає команду data
# <- сервер відповідає OK або ні
# -> клієнт надсилає всі рядки свого повідомлення і завершує його рядком, що містить лише один символ .
# <- сервер відповідає OK або ні
# -> клієнт надсилає команду quit
# <- сервер відповідає OK або ні

# відповіді сервера мають вигляд xxx текст, де xxx — трицифрове число. Будь-яке число xxx >=500
# вказує на помилку. Відповідь може містити кілька рядків, які починаються з xxx-, за винятком останнього
# у форматі xxx (пробіл)

# обмінювані текстові рядки повинні закінчуватися символами RC(#13) та LF(#10)

# конфігурація додатка
import config
config = config.configure()

# листи обробляються по одному
for mail in config['mails']:
    try:
        # журнали
        print("----------------------------------")
        print(f"Envoi du message [{mail['description']}]")
        # підготовка повідомлення до відправлення
        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']}"
        # відправлення повідомлення в режимі детального виведення
        sendmail(mail, True)
        # кінець
        print("Message envoyé...")
    except BaseException as erreur:
        # виводиться повідомлення про помилку
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
    # наступний лист

Коментарі

  • рядки 134–136: налаштовуємо програму;
  • рядки 139–151: обробляються всі електронні листи, знайдені під час налаштування;
  • рядки 141–143: відображається інформація про те, що буде зроблено;
  • рядки 144–149: визначається повідомлення, яке потрібно надіслати. Повідомлення [message] передують заголовки [From, To, Subject, Content-type];
  • рядок 151: відправлення електронного листа забезпечується функцією [sendmail], яка приймає два параметри:
    • [mail]: словник, що містить інформацію, необхідну для відправлення електронного листа;
    • [verbose]: логічне значення, що вказує, чи слід реєструвати обмін даними між клієнтом і сервером у консолі;
  • рядки 154–156: перехоплюються всі винятки, що виникають у функції [sendmail]. Вони виводяться на екран;
  • рядок 6: [mail] — це словник, що описує електронний лист, який потрібно надіслати;
  • рядок 14: у протоколі SMTP клієнт повинен надіслати своє ім’я. Тут отримується ім’я локального комп’ютера, який буде виконувати роль клієнта;
  • рядок 16: підключення до сервера SMTP, якому буде надіслано повідомлення;
  • рядки 22–23: якщо з’єднання з сервером SMTP встановлено, він надішле привітальне повідомлення, яке ми тут читаємо;
  • потім функція [sendmail] надсилає різні команди, які повинен надіслати клієнт SMTP:
    • рядки 24–25: команда EHLO;
    • рядки 26–27: команда MAIL FROM:;
    • рядки 28–29: замовлення RCPT TO: ;
    • рядки 30–31: команда DATA;
    • рядки 32–41: відправлення повідомлення (From, To, Subject, Content-type, текст);
    • рядки 42–43: відправлення завершального символу;
    • рядки 44–457: команда QUIT, яка завершує діалог клієнта з сервером SMTP;
  • виконання [sendmail] відбувається в [try / finally], який передає всі винятки до коду, що викликає; відомо, що останній зупиняє їх усі для відображення;
  • рядки 48–50: звільнення ресурсів;
  • рядок 54: функція [send_command] відповідає за надсилання команд клієнта на сервер SMTP. Вона приймає чотири параметри:
    • [connexion]: з’єднання, що пов’язує клієнта з сервером;
    • [commande]: команда, яку потрібно надіслати;
    • [verbose]: якщо TRUE, то обмін даними між клієнтом і сервером реєструється в консолі;
    • [with_rclf]: якщо TRUE, надсилає команду, що закінчується послідовністю \r\n. Це необхідно для всіх команд протоколу SMTP, але [send_command] також використовується для відправлення повідомлення. У цьому випадку послідовність \r\n не додається;
  • рядок 62: команда надсилається лише в тому випадку, якщо вона не порожня;
  • рядки 65–66: команда надсилається на сервер у вигляді байтового рядка UTF-8;
  • рядки 70–71: зчитування всіх рядків відповіді. Припускається, що її довжина не перевищує 1000 символів. Відповідь може складатися з декількох рядків. Кожен рядок має вигляд XXX-YYY, де XXX — це числовий код, за винятком останнього рядка відповіді, який має вигляд XXX YYY (відсутність символу -);
  • рядок 76: зчитування коду помилки XXX з 1-го рядка;
  • рядки 78–80: якщо числовий код XXX перевищує 500, то сервер повернув помилку. У цьому випадку генерується виняток;

Результати

Виконання скрипта дає такі результати у консолі:


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
  • рядки 3–30: використання сервера SMTP [hMailServer] для надсилання електронного листа на [guest@localhost] проходить успішно;
  • рядки 32–46: використання серверів SMTP та [smtp.gmail.com] для надсилання електронного листа на [pymailparlexemple@gmail.com] не проходить успішно: у рядку 45 сервер SMTP надсилає код помилки 530 із повідомленням про помилку. У ньому зазначено, що клієнт SMTP повинен попередньо пройти автентифікацію через захищене з’єднання. Наш клієнт цього не зробив, тому йому відмовлено;

Результати в Thunderbird такі:

Image

21.5.7. скрипти [smtp/02]: посилання SMTP, написане з використанням бібліотеки [smtplib]

Image

Попередній клієнт має щонайменше два недоліки:

  1. він не вміє використовувати захищене з’єднання, якщо сервер цього вимагає;
  1. він не вміє додавати вкладення до повідомлення;

Ми усунемо перший недолік у скрипті [smtp/02]. У нашому новому скрипті ми будемо використовувати модуль Python [smtplib].

Скрипт [smtp/02/main] використовуватиме такий файл конфігурації jSON [smtp/02/config]:


def configure() -> dict:
    return {
        # опис: опис надісланого листа
        # smtp-server: сервер SMTP
        # smtp-port: порт сервера SMTP
        # from: відправник
        # to: одержувач
        # subject: тема листа
        # message: текст листа
        "mails": [
            {
                "description": "mail to localhost via localhost avec smtplib",
                "smtp-server": "localhost",
                "smtp-port": "25",
                "from": "guest@localhost.com",
                "to": "guest@localhost.com",
                "subject": "to localhost via localhost avec smtplib",
                # перевіряємо символи з діакритичними знаками
                "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",
                # тестуємо символи з діакритичними знаками
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP з автентифікацією
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlh@1QZ3TG",
            }
        ]
    }

Тут містяться ті самі поля, що й у файлі [smtp/01/config], а також два додаткові поля, коли сервер SMTP вимагає автентифікації:

  • рядок 31, [user]: ім’я користувача, що автентифікує з’єднання;
  • рядок 32, [password]: його пароль;

Ці два поля присутні лише в тому випадку, якщо сервер SMTP, до якого звертаються, вимагає автентифікації. У такому разі автентифікація здійснюється через захищене з’єднання.

Код скрипта [smtp/02/main.py] такий:


# імпорт
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate


# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: True):
    # надсилає повідомлення на SMTP-сервер smtpserver від імені відправника
    # для одержувача. Якщо verbose=True, здійснює відстеження обміну даними між клієнтом і сервером

    # використовується бібліотека smtplib
    # виключення передаються далі
    #
    # сервер SMTP
    server = smtplib.SMTP(mail["smtp-server"])
    # режим verbose
    server.set_debuglevel(verbose)
    # захищене з'єднання?
    if "user" in mail:
        # захищене з'єднання
        server.starttls()
        # EHLO команда + автентифікація
        server.login(mail["user"], mail["password"])

   # створення повідомлення Multipart — саме це повідомлення Multipart буде надіслано
    msg = MIMEText(mail["message"])
    msg['from'] = mail["from"]
    msg['to'] = mail["to"]
    msg['date'] = formatdate(localtime=True)
    msg['subject'] = mail["subject"]
    # відправляємо повідомлення
    server.send_message(msg)
    # вихід
    server.quit()


# головне ----------------------------------------------------------------

# інформація береться з конфігураційного файлу, що містить такі дані для кожного сервера

# опис: опис надісланого листа
# smtp-server: сервер SMTP
# smtp-port: порт сервера SMTP
# from: відправник
# to: одержувач
# subject: тема листа
# content-type: кодування листа
# message: текст листа


# конфігурація додатка
import config
config = config.configure()

# листи обробляються по черзі
for mail in config['mails']:
    try:
        # журнали
        print("----------------------------------")
        print(f"Envoi du message [{mail['description']}]")
        # відправлення повідомлення у режимі детального виведення
        sendmail(mail, True)
        # кінець
        print("Message envoyé...")
    except BaseException as erreur:
        # виведення повідомлення про помилку
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
    # наступний лист

Коментарі

  • рядки 8–35: використовується лише функція [sendmail]. Відтепер вона використовуватиме модуль [smtplib] (рядок 2);
  • рядок 16: підключення до сервера SMTP;
  • рядок 18: якщо [verbose=True], обмін даними між клієнтом і сервером відображатиметься на консолі;
  • рядки 20–24: проводиться автентифікація, якщо цього вимагає сервер SMTP;
  • рядок 22: автентифікація здійснюється через захищене з’єднання;
  • рядок 24: аутентифікація;
  • рядки 26–33: надсилання повідомлення. Після цього розпочнеться діалог зі скриптом [smtp/01/main]. Якщо відбулася автентифікація, діалог відбуватиметься через захищене з’єднання;
  • рядок 35: завершення діалогу «клієнт/сервер»;

Перед запуском скрипта [smtp/02/main] необхідно змінити налаштування облікового запису Gmail [pymailparlexemple@gmail.com]:

  • увійдіть в обліковий запис Gmail [pymailparlexemple@gmail.com];
  • змініть наступні налаштування: Image
  • у [2] дозвольте менш безпечним додаткам отримувати доступ до облікового запису;

Зробіть те саме з другим обліковим записом Gmail [pymail2parlexemple@gmail.com].

Результати

При виконанні скрипта [smtp/02/main] у консолі з’являються такі результати:


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
  • рядок 40: клієнт [smtplib] розпочинає діалог для встановлення зашифрованого з’єднання з сервером SMTP, чого не вдалося зробити у скрипті [smtp/main/01];
  • в іншому випадку ми бачимо команди, відомі за протоколом SMTP;

Якщо переглянути обліковий запис Gmail користувача [pymail2parlexemple], то можна побачити таке:

Image

21.5.8. скрипт [smtp/03]: управління вкладеними файлами

Ми доповнюємо скрипт [smtp/02/main], щоб у надісланому листі могли бути вкладені файли.

Image

Скрипт [smtp/03/main] налаштовується за допомогою наступного скрипта [smtp/03/config]:


import os


def configure() -> dict:
    # конфігурація програми
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        # опис: опис надісланого листа
        # smtp-server: сервер SMTP
        # smtp-port: порт сервера SMTP
        # from: відправник
        # to: одержувач
        # subject: тема листа
        # message: текст листа
        "mails": [
            {
                "description": "mail to gmail via gmail avec smtplib",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymail2parlexemple@gmail.com",
                "to": "pymail2parlexemple@gmail.com",
                "subject": "to gmail via gmail avec smtplib",
                # перевіряємо символи з діакритичними знаками
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP з автентифікацією
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # тут потрібно вказати абсолютні шляхи до вкладених файлів
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                ]
            }
        ]
    }

Файл [smtp/03/config] відрізняється від файлу [smtp/02/config], що використовувався раніше, лише наявністю опціонального списку [attachments] (рядки 30–32), який визначає перелік файлів, що мають бути додані до повідомлення, яке надсилається.

Скрипт [smtp/03/main] має такий вигляд:


# імпорт
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):
    # надсилає листа mail[message] на SMTP-сервер mail[smtp-server] від імені mail[from]
    # для mail[to]. Якщо verbose=True, відстежує обмін даними між клієнтом і сервером

    # використовується бібліотека smtplib
    # дозволяє передавати винятки
    #
    # сервер SMTP
    server = smtplib.SMTP(mail["smtp-server"])
    # режим verbose
    server.set_debuglevel(verbose)
    # захищене з'єднання?
    if "user" in mail:
        server.starttls()
        server.login(mail["user"], mail["password"])

    # створення багаточастинного повідомлення — саме це повідомлення буде надіслано
    # джерело: 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"]
    # додаємо текстове повідомлення у форматі MIMEText
    msg.attach(MIMEText(mail["message"]))
    # здійснюється перегляд вкладень
    for path in mail["attachments"]:
        # path має бути абсолютним шляхом
        # визначається тип вкладеного файлу
        ctype, encoding = mimetypes.guess_type(path)
        # якщо тип не вдалося визначити
        if ctype is None or encoding is not None:
            # Не вдалося визначити тип, або файл закодовано (стиснуто), тому
            # використовується загальний тип «bag-of-bits».
            ctype = 'application/octet-stream'
        # тип розкладається на основний тип та підтип
        maintype, subtype = ctype.split('/', 1)
        # розглядаємо різні випадки
        if maintype == 'text':
            with open(path) as fp:
                # Примітка: слід передбачити обчислення кодування
                part = MIMEText(fp.read(), _subtype=subtype)
        elif maintype == 'image':
            with open(path, 'rb') as fp:
                part = MIMEImage(fp.read(), _subtype=subtype)
        elif maintype == 'audio':
            with open(path, 'rb') as fp:
                part = MIMEAudio(fp.read(), _subtype=subtype)
        # випадок типу message / rfc822
        elif maintype == 'message':
            with open(path, 'rb') as fp:
                part = MIMEMessage(email.message_from_bytes(fp.read()))
        else:
            # інші випадки
            with open(path, 'rb') as fp:
                part = MIMEBase(maintype, subtype)
                part.set_payload(fp.read())
            # Кодування корисного навантаження за допомогою Base64
            encoders.encode_base64(part)
        # Встановити параметр імені файлу
        basename = os.path.basename(path)
        part.add_header('Content-Disposition', 'attachment', filename=basename)
        # додаємо файл до повідомлення, яке потрібно надіслати
        msg.attach(part)
    # усі вкладення додано — надсилаємо повідомлення у вигляді символьного рядка
    server.send_message(msg)


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

..

Коментарі

  • рядки 18–32: функція [sendmail] залишається такою, якою вона була, коли вкладень не було;
  • рядок 35: код, що йде далі, взято з офіційної документації Python;
  • рядок 36: повідомлення, яке буде надіслано, складатиметься з кількох частин: тексту та вкладених файлів. Таке повідомлення називається [Multipart];
  • рядки 37–40: у повідомленні [Multipart] містяться стандартні поля, властиві будь-якому електронному листу;
  • рядок 42: різні частини повідомлення [Multipart] [msg] додаються до повідомлення за допомогою методу [msg.attach] (рядок 81). Прикріплені частини можуть бути будь-якого типу. Вони характеризуються типом MIME. Тип MIME звичайного тексту — це тип [MIMEText];
  • рядки 44–81: до повідомлення [msg Multipart] будуть додані всі вкладення повідомлення, що надсилається (рядок 81);
  • рядок 44: [path] позначає абсолютний шлях до файлу, що додається;
  • рядок 47: щоб визначити тип MIME, який слід використовувати для частини, що додається, ми скористаємося розширенням (.docx, .php…) файлу, що додається. Цю роботу виконує метод [mimetypes.guess_type]. Він повертає дві інформації:
    • [ctype]: тип MIME файлу;
    • [encoding]: інформацію про його кодування;
  • рядки 49–52: якщо тип файлу MIME визначити неможливо, файл вважається бінарним (рядок 52);
  • рядок 54: тип файлу MIME розкладається на основний тип / вторинний тип, наприклад [application/pdf]. Ці два елементи розділяються;
  • рядки 56–76: обробляються різні випадки залежно від значення основного типу MIME. Наприклад, у випадку [application/pdf] для файлу PDF виконуються рядки 70–76:
    • рядки 56–59: випадок, коли вкладений файл є текстовим. У цьому випадку створюється елемент типу [MIMEText] із вмістом [fp.read];
    • рядки 60–62: випадок, коли файл містить зображення. У цьому випадку створюється елемент типу [MIMEImage] із вмістом [fp.read];
    • рядки 63–65: випадок, коли файл є аудіофайлом. У цьому випадку створюється елемент типу [MIMEAudio] із вмістом [fp.read];
    • рядки 66–69: випадок, коли файл є електронним листом. У цьому випадку створюється елемент типу [MIMEMessage] (рядок 69) із вмістом [email.message_from_bytes(fp.read())]. На відміну від попередніх випадків, коли вміст елемента MIME був бінарним вмістом відповідного файлу, тут вміст елемента MIMEMessage має тип [email.message.Message];
    • рядки 70–76: інші випадки. Сюди входять, наприклад, файли Word та PDF з нашого прикладу;
  • рядок 72: файл, що додається, відкривається у бінарному режимі (rb=read binary);
  • рядок 74: [fp.read] зчитує весь бінарний файл;
  • рядки 72–74: структура [with open(…) as file] виконує дві дії:
    • вона відкриває файл і присвоює йому дескриптор [file];
    • вона гарантує, що після завершення роботи [with], незалежно від того, чи сталася помилка, дескриптор [file] буде закрито. Отже, це альтернатива структурі [try file=open(…)/ finally];
  • рядок 73: створюється новий елемент [part] для включення до повідомлення Multipart. Тут використовується клас [MIMEBase], а до конструктора передаються елементи [maintype, subtype], визначені в рядку 54;
  • рядок 74: елемент, що вбудовується в повідомлення Multipart, повинен мати вміст. Його можна ініціалізувати за допомогою методу [set_payload];
  • рядки 75–76: вкладені файли мають бути закодовані у 7-бітовому форматі. Справа в тому, що раніше деякі сервери SMTP підтримували лише символи, закодовані у 7-бітовому форматі. Тут використовується кодування під назвою «Base64»;
  • рядок 77: починаючи з цього рядка, обробка здійснюється для всіх типів MIME, які ми створили в рядках 56–76 [MIMEMessage, MIMEImage, MIMEAudio, MIMEBase, MIMEText];
  • рядок 79: елемент, який потрібно додати до повідомлення Multipart, має заголовок, що його описує. Тут вказується, що доданий елемент відповідає вкладеному файлу. Ім’я цього файлу є третім параметром, що передається методу [add_header]. Ім’я цього файлу часто використовується поштовими клієнтами для збереження вкладеного файлу під цим ім’ям у файловій системі клієнта. До цього моменту ми працювали з абсолютним ім’ям вкладеного файлу. Тут ми просто передаємо його ім’я без шляху (рядок 78);
  • рядок 81: бінарний вміст файлу вбудовується у повідомлення [msg Multipart];
  • рядок 83: коли всі частини повідомлення були додані до [msg Multipart], воно надсилається;

Результати

Якщо запустити скрипт [smtp/03/main] із файлом [smtp/02/config], який вже було наведено, на рахунок [pymail2parlexemple@gmail.com] надходить таке:

Image

У файлі [4, 9-11] можна побачити вкладені файли.

Тепер розглянемо приклад із листом у вкладенні. Ми збережемо отриманий лист у файлі [3], наведеному вище:

Image

Ми зберігаємо лист під назвою [mail attaché 1.eml] у папці [smtp/03/attachments].

Тепер змінюємо файл [smtp/03/config] наступним чином:


import os


def configure() -> dict:
    # конфігурація програми
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        # опис: опис надісланого листа
        # smtp-server: сервер SMTP
        # smtp-port: порт сервера SMTP
        # from: відправник
        # to: одержувач
        # subject: тема листа
        # message: текст листа
        "mails": [
            {
                "description": "mail to gmail via gmail avec smtplib",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymail2parlexemple@gmail.com",
                "to": "pymail2parlexemple@gmail.com",
                "subject": "to gmail via gmail avec smtplib",
                # перевіряємо символи з діакритичними знаками
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP з автентифікацією
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # тут потрібно вказати абсолютні шляхи до вкладених файлів
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                    f"{script_dir}/attachments/mail attaché 1.eml",
                ]
            }
        ]
    }
  • у рядку 33 ми додали вкладення;

Тепер знову запускаємо скрипт [smtp/03/main]. У результаті у поштовій скриньці користувача [pymail2parlexemple@gmail.com] з’являється таке:

Image

  • у [1] — отриманий лист;
  • у [2]: текст повідомлення;
  • у [3] — текст листа у вкладенні;
  • у [4]: Thunderbird знайшов 5 вкладень:
    • [fichier attaché.docx];
    • [fichier attaché.pdf];
    • [mail attaché 1.eml]. Цей вкладення є, у свою чергу, електронним листом, що містить два вкладення:
      • [fichier attaché.docx];
      • [fichier attaché.pdf];

21.6. Протокол POP3

21.6.1. Вступ

Для читання листів, що зберігаються на поштовому сервері, існують два протоколи:

  • протокол POP3 (Post Office Protocol) — історично перший протокол, але зараз він майже не використовується;
  • протокол IMAP (Internet Message Access Protocol) — протокол, новіший за POP3 і найпоширеніший на сьогодні;

Щоб ознайомитися з протоколом POP3, ми використаємо таку архітектуру:

Image

  • [Serveur B] залежно від обставин буде:
    • локальним сервером POP3, реалізованим поштовим сервером [hMailServer];
    • сервером [pop.gmail.com], який є сервером POP3 поштового менеджера [gmail.com];
  • [Client A] буде клієнтом POP3 у різних формах:
    • клієнт [RawTcpClient] для виявлення протоколу POP3;
    • скрипт на Python, що відтворює протокол POP3 клієнта [RawTcpClient];
    • скрипт на Python, що використовує модулі Python для управління вкладеними файлами, а також для використання зашифрованого та автентифікованого з’єднання, коли цього вимагає сервер POP3;

21.6.2. Ознайомлення з протоколом POP3

Як і у випадку з протоколом SMTP, ми розглянемо протокол POP3 за допомогою журналів поштового сервера [hMailServer]. Для цього потрібно запустити цей сервер.

За допомогою Thunderbird ми:

  • надішлемо листа користувачеві [guest@localhost.com];
  • переглянемо поштову скриньку цього користувача;

Image

Image

У [3-6] вище — повідомлення, отримане користувачем [guest@localhost.com].

Зараз ми переглядаємо журнали сервера [hMailServer]. Для цього ми використовуємо інструмент адміністрування [hMailServer Administrator]:

Image

Журнали POP3 мають такий вигляд (останні рядки у файлі журналів за сьогодні):


"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..."
  • рядок 1: сервер POP3 надсилає привітальне повідомлення клієнту (Thunderbird), який щойно підключився;
  • рядок 2: клієнт надсилає команду [CAPA] (capabilities), щоб отримати перелік команд, якими він може користуватися;
  • рядок 3: сервер відповідає, що клієнт може використовувати команди [USER, UIDL, TOP]. Сервер POP починає свої відповіді з [+OK] або [-ERR], щоб вказати, чи вдалося йому виконати команду клієнта, чи ні;
  • рядок 4: клієнт надсилає команду [USER guest], щоб вказати, що він хоче переглянути поштову скриньку користувача [guest];
  • рядок 5: сервер відповідає йому [+OK] і запитує пароль користувача [guest];
  • рядок 6: клієнт надсилає команду [PASS password], щоб надіслати пароль користувача [guest]. Тут пароль передається у відкритому вигляді, оскільки сервер POP3 не вимагає безпечного з’єднання. Ми побачимо, що з сервером POP3 від Gmail ситуація буде іншою;
  • рядок 7: сервер підтвердив правильність комбінації логіна та пароля. Він повідомляє, що блокує поштову скриньку користувача [guest];
  • рядок 8: клієнт надсилає йому команду [STAT], яка запитує інформацію про поштову скриньку;
  • рядок 9: сервер відповідає, що є одне повідомлення розміром 612 байт. Як правило, він повідомляє, що є N повідомлень, і вказує їхній загальний розмір;
  • рядок 10: клієнт надсилає команду [LIST]. Ця команда запитує список повідомлень;
  • рядок 11: сервер надсилає йому список повідомлень у такому форматі:
    • підсумковий рядок із кількістю повідомлень та їхнім загальним розміром;
    • один рядок на кожне повідомлення, що вказує номер повідомлення та його розмір;
  • рядок 13: клієнт надсилає команду [UIDL], яка запитує список повідомлень із їхніми ідентифікаторами. Адже кожне повідомлення має унікальний номер у поштовій службі;
  • рядок 14: відповідь сервера. З неї видно, що повідомлення № 1 у списку має ідентифікатор 42;
  • рядок 15: клієнт надсилає команду [RETR 1], яка вимагає передати йому повідомлення № 1 зі списку;
  • рядок 16: сервер POP3 виконує цю команду;
  • рядок 17: клієнт надсилає команду [QUIT], щоб повідомити, що він збирається від’єднатися від сервера POP3;
  • рядок 18: сервер також закриє з’єднання з клієнтом, але перед цим надішле йому прощальне повідомлення;

Тепер ми відтворимо елементи наведеного вище діалогу, використовуючи клієнт [RawTcpClient], що виконується у вікні PyCharm:

Image

Діалог виглядає так:


(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
  • рядок 1: встановлюється з’єднання з портом 110 машини [localhost]. Саме там працює служба POP3 системи [hMailServer];
  • у рядках 5, 7, 9, 13, 34 ми використовуємо команди [USER, PASS, LIST, RETR, QUIT];
  • рядок 4: привітальне повідомлення сервера POP3;
  • рядок 5: вказуємо, що хочемо отримати доступ до поштової скриньки користувача [guest];
  • рядок 7: надсилаємо пароль користувача [guest] у відкритому вигляді;
  • рядок 9: запитується список повідомлень у поштовій скриньці;
  • рядок 13: запитується повідомлення № 1;
  • рядки 14–33: сервер POP3 надсилає повідомлення № 1;
  • рядок 34: завершується сесія;

Ось короткий огляд деяких поширених команд, які підтримує сервер POP3:

  • команда [USER] служить для визначення користувача, поштову скриньку якого потрібно переглянути;
  • команда [PASS] служить для введення пароля;
  • команда [LIST] запитує список повідомлень, що містяться у поштовій скриньці користувача;
  • команда [RETR] запитує повідомлення, номер якого передається;
  • команда [DELE] вимагає видалення повідомлення, номер якого передається;
  • команда [QUIT] повідомляє серверу про завершення операції;

Відповідь сервера може мати кілька форм:

  • один рядок, що починається з [+OK], щоб вказати, що попередня команда клієнта виконана успішно;
  • один рядок, що починається з [-ERR], який вказує, що попередня команда клієнта завершилася невдало;
  • кілька рядків, де:
    • перший рядок починається з [+OK];
    • останній рядок складається з однієї крапки;

21.6.3. скрипти [pop3/01]: базовий клієнт POP3

Image

Оскільки протокол POP3 має таку саму структуру, як і протокол SMTP, скрипт [pop3/01/main.py] є адаптацією скрипта [smtp/01/main.py]. Він матиме такий файл конфігурації [pop3/01/config.py]:


def configure() -> dict:
    # поштові скриньки, з яких витягуються листи
    mailboxes = [
        # сервер: сервер POP3
        # порт: порт сервера POP3
        # user: користувач, повідомлення якого потрібно прочитати
        # password: його пароль
        # maxmails: максимальна кількість листів для завантаження
        # timeout: максимальний час очікування відповіді від сервера
        # encoding: кодування отриманих листів
        # delete: якщо True, то листи видаляються зі скриньки
        # після їх завантаження на локальний комп’ютер

        {
            "server": "localhost",
            "port": "110",
            "user": "guest",
            "password": "guest",
            "maxmails": 10,
            "timeout": 1.0,
            "encoding": "utf-8",
            "delete": False
        }
    ]
    # налаштування повертається
    return {
        "mailboxes": mailboxes
    }
  • рядки 3–24: список поштових скриньок, які потрібно перевірити. Тут є лише одна;
  • рядки 4–12: значення елементів словника, що визначають кожну з поштових скриньок;
  • рядок 15: запитуваний сервер POP3 — це локальний сервер [hMailServer];
  • рядки 17–18: потрібно прочитати поштову скриньку користувача [guest@localhost];
  • рядок 19: буде прочитано не більше 10 листів;
  • рядок 20: клієнт чекатиме на відповідь сервера не більше 1 секунди;
  • рядок 21: тип кодування прочитаних повідомлень;
  • рядок 22: завантажені повідомлення не видалятимуться;

Скрипт [pop3/01/main.py] має такий вигляд:


# імпортує
import re
import socket


# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
    # зчитує поштову скриньку, описану у словнику [mailbox]
    # якщо verbose=True, відстежує обмін даними між клієнтом і сервером



# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
    # надсилає команду в канал з’єднання
    # режим детального виведення, якщо verbose=True
    # якщо with_rclf=True, додає послідовність rclf до обміну
    # повертає перший рядок відповіді



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


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

# клієнт POP3 (Post Office Protocol), що дозволяє читати повідомлення з поштової скриньки
# протокол зв’язку POP3 «клієнт-сервер»
# -> клієнт підключається до порту 110 сервера SMTP
# <- сервер надсилає йому привітальне повідомлення
# -> клієнт надсилає команду USER «користувач»
# <- сервер відповідає OK або ні
# -> клієнт надсилає команду PASS mot_de_passe
# <- сервер відповідає OK або ні
# -> клієнт надсилає команду LIST
# <- сервер відповідає OK або ні
# -> клієнт надсилає команду RETR з номером для кожного листа
# <- сервер відповідає OK або ні. Якщо OK — надсилає вміст запитуваного листа
# -> сервер надсилає всі рядки листа і завершує його рядком, що містить
# єдиний символ.
# -> клієнт надсилає команду DELE № для видалення листа
# <- сервер відповідає OK або ні
# # -> клієнт надсилає команду QUIT для завершення діалогу з сервером
# <- сервер відповідає OK або ні
# відповіді сервера мають вигляд +OK текст або -ERR текст
# Відповідь може містити кілька рядків. У такому разі останній рядок складається з однієї крапки
# обмінювані рядки тексту повинні закінчуватися символами RC(#13) та LF(#10)
# 

# отримуємо конфігурацію додатка
import config
config = config.configure()

# обробляються поштові скриньки по черзі
for mailbox in config['mailboxes']:
    try:
        # виведення інформації на консоль
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
        # читання поштової скриньки у режимі детального виведення
        readmails(mailbox, True)
        # кінець
        print("Lecture terminée...")
    except BaseException as erreur:
        # виводиться повідомлення про помилку
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass

Коментарі

Як ми вже зазначали, [pop3/01/main.py] є адаптованою версією скрипта [smtp/01/main.py], який ми вже коментували. Ми зупинимося лише на основних відмінностях:

  • рядок 64: функція [readmails] відповідає за зчитування листів із поштової скриньки. Дані для підключення до цієї поштової скриньки містяться у словнику [mailbox]. Другим параметром [True] є параметр [Verbose], який у цьому випадку вимагає відстеження обміну даними між клієнтом і сервером;

Функція [readmails] має такий вигляд:


# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
    # зчитує листи зі скриньки, описаної у словнику [mailbox]
    # якщо verbose=True, відстежує обмін даними між клієнтом і сервером

    # виокремлюються параметри поштової скриньки
    # припускається, що словник [mailbox] є дійсним
    server = mailbox['server']
    port = int(mailbox['port'])
    user = mailbox['user']
    password = mailbox['password']
    maxmails = mailbox['maxmails']
    delete = mailbox['delete']
    timeout = mailbox['timeout']

    # дозволяє відображати системні помилки
    connexion = None
    try:
        # відкриття з'єднання на порту [port] з [server] із тайм-аутом в одну секунду
        connexion = socket.create_connection((server, port), timeout=timeout)

        # з'єднання являє собою двосторонній канал зв'язку
        # між клієнтом (цією програмою) та сервером POP3, до якого встановлено з’єднання
        # цей канал використовується для обміну командами та інформацією

        # читання привітального повідомлення
        send_command(mailbox, connexion, "", verbose, True)
        # команда USER
        send_command(mailbox, connexion, f"USER {user}", verbose, True)
        # команда PASS
        send_command(mailbox, connexion, f"PASS {password}", verbose, True)
        # команда LIST
        première_ligne = send_command(mailbox, connexion, "LIST", verbose, True)
        # аналіз першого рядка для визначення кількості повідомлень
        match = re.match(r"^\+OK (\d+)", première_ligne)
        nbmessages = int(match.groups()[0])
        # цикл по повідомленнях
        imessage = 0
        while imessage < nbmessages and imessage < maxmails:
            # команда RETR
            send_command(mailbox, connexion, f"RETR {imessage + 1}", verbose, True)
            # команда DELE
            if delete:
                send_command(mailbox, connexion, f"DELE {imessage + 1}", verbose, True)
            # наступне повідомлення
            imessage += 1
        # команда QUIT
        send_command(mailbox, connexion, "QUIT", verbose, True)
        # кінець
    finally:
        # закриття з'єднання
        if connexion:
            connexion.close()

Коментарі

  • рядки 8–14: отримуємо інформацію про конфігурацію поштової скриньки, яку потрібно переглянути;
  • рядки 19–20: встановлення з’єднання з сервером POP3;
  • рядки 26–27: читання привітального повідомлення, надісланого сервером;
  • рядки 28–29: надсилання команди [USER] для ідентифікації користувача, листи якого потрібно отримати;
  • рядки 30–31: надсилання команди [PASS] для введення пароля цього користувача;
  • рядки 32–33: надсилається команда [LIST], щоб дізнатися, скільки листів є у поштовій скриньці цього користувача. Функція [sendCommand] повертає перший рядок відповіді сервера. У ньому сервер вказує, скільки повідомлень є у поштовій скриньці;
  • рядки 34–36: отримуємо кількість повідомлень із першого рядка відповіді;
  • рядки 39–46: виконується цикл по кожному з повідомлень. Для кожного з них надсилаються дві команди:
    • RETR i: для отримання повідомлення № i (рядки 40–41);
    • DELE i: для його видалення, якщо конфігурація вимагає, щоб прочитані повідомлення видалялися з сервера (рядки 43–44);
  • рядки 47–48: надсилається команда [QUIT], щоб повідомити серверу про завершення операції;

Функція [send_command] виглядає так:


# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
    # надсилає команду в канал з'єднання
    # режим детального виведення, якщо verbose=True
    # якщо with_rclf=True, додає послідовність rclf до обміну
    # повертає перший рядок відповіді

    # позначка кінця рядка
    if with_rclf:
        rclf = "\r\n"
    else:
        rclf = ""
    # надсилає команду, якщо не порожньо
    if commande:
        connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
        # можливе відлуння
        if verbose:
            affiche(commande, 1)
    # читання сокета так, ніби це текстовий файл
    encoding = f"{mailbox['encoding']}" if mailbox['encoding'] else None
    file = connexion.makefile(encoding=encoding)
    # цей файл обробляється по рядках
    # читання першого рядка
    première_ligne = réponse = file.readline().strip()
    # режим детального виведення?
    if verbose:
        affiche(première_ligne, 2)
    # отримання коду помилки
    code_erreur = réponse[0]
    if code_erreur == "-":
        # сталася помилка
        raise BaseException(réponse[5:])
    # особливий випадок багаторядкових відповідей LIST, RETR
    cmd = commande.lower()[0:4]
    if cmd == "list" or cmd == "retr":
        # останній рядок відповіді?
        dernière_ligne = False
        while not dernière_ligne:
            # читання наступного рядка
            ligne_suivante = file.readline().strip()
            # режим детального виведення?
            if verbose:
                affiche(ligne_suivante, 2)
            # останній рядок?
            dernière_ligne = ligne_suivante == "."
    # завершено — повертаємо перший рядок
    return première_ligne

Коментарі

  • рядки 13–18: команда [command] надсилається на сервер POP3 лише в тому випадку, якщо вона не є порожньою. Це потрібно для того, щоб прочитати вітальне повідомлення сервера POP3, яке він надсилає ще до того, як клієнт надіслав будь-які команди;
  • рядки 19–21: сокет зчитується так, ніби це текстовий файл. Це дозволить нам використати метод [readline] (рядок 24) і таким чином зчитувати повідомлення по рядках. Використовуємо ключ [encoding] зі словника [mailbox], щоб вказати кодування рядків, які будуть зчитуватися;
  • рядок 24: зчитується перший рядок відповіді;
  • рядки 28–32: обробляється випадок можливої помилки. Помилки мають тип [-ERR invalid password, -ERR mailbox unknown, -ERR unable to lock mailbox…];
  • рядок 32: генерується виняток із повідомленням про помилку;
  • рядок 35: лише команди типу [list, retr] можуть мати відповіді, що складаються з декількох рядків;
  • рядки 36–45: у разі багаторядкової відповіді виводимо всі отримані рядки (рядки 42–43) до отримання останнього рядка (рядок 45);
  • рядок 46: повертається перший прочитаний рядок, оскільки у випадку команди [LIST] він містить кількість повідомлень, що знаходяться у поштовій скриньці;

Результати

Візьмемо попередній приклад. За допомогою Thunderbird ми надіслали наступне повідомлення користувачеві [guest@localhost] (сервер hMailServer має бути запущений):

Image

Під час виконання отримуємо такі результати:


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
  • рядки 15–31: повідомлення, надіслане на [guest@localhost], отримано правильно.

Ми маємо тут базовий клієнт POP3, якому бракує деяких можливостей:

  1. можливість взаємодіяти із захищеним сервером POP3;
  2. можливість читати вкладення до повідомлення;

Ми реалізуємо ці дві можливості за допомогою нового скрипта, який цього разу буде складнішим.

21.6.4. Скрипт [pop3/02]: клієнт POP3 з модулями [poplib] та [email]

Ми напишемо клієнт POP3, який дозволить керувати вкладеними файлами, а також забезпечуватиме зв’язок із захищеними серверами. Крім того, ми будемо зберігати повідомлення та їхні вкладені файли у файлах.

Ми будемо використовувати два модулі Python:

  • [poplib]: який забезпечить роботу протоколу POP3;
  • [email]: який об’єднує численні підмодулі, що дозволять нам аналізувати отримані повідомлення. Кожне повідомлення — це структурований рядок символів, у якому можна знайти:
    • заголовки повідомлення [From, To, Subject, Return-Path…];
    • саме повідомлення у текстовому форматі та, можливо, у форматі HTML;
    • вкладення;

Image

Скрипт [inet/pop3/02/main] [1] налаштовується файлом [inet/pop3/02/config] [2] і використовує модуль [inet/shared/mail_parser] [3].

Файл [pop3/02/config] має такий вигляд:


import os


def configure() -> dict:
    # конфігурація програми
    config = {
        # список поштових скриньок для управління
        "mailboxes": [
            # сервер: сервер POP3
            # порт: порт сервера POP3
            # user: користувач, повідомлення якого потрібно прочитати
            # password: його пароль
            # maxmails: максимальна кількість листів для завантаження
            # timeout: максимальний час очікування відповіді від сервера
            # delete: має значення «true», якщо завантажені повідомлення потрібно видалити з сервера
            # ssl: має значення «true», якщо читання листів відбувається через захищене з’єднання
            # output: папка для зберігання завантажених повідомлень

            {
                "server": "pop.gmail.com",
                "port": "995",
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                "maxmails": 10,
                "delete": False,
                "ssl": True,
                "timeout": 2.0,
                "output": "output"
            }
        ]
    }
    # абсолютний шлях до папки зі скриптом
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # абсолютні шляхи до папок, які слід включити до syspath
    absolute_dependencies = [
        # локальна папка
        f"{script_dir}/../../shared",
   ]

    # конфігурація syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # виконується конфігурація
    return config

Файл визначає список поштових скриньок, які потрібно перевірити, та встановлює Python Path додатка.

Тут є лише одна поштова скринька:

  • рядки 22–23: користувач, листи якого потрібно переглянути;
  • рядки 20–21: ім’я та порт сервера POP3, на якому зберігаються листи цього користувача;
  • рядок 24: максимальна кількість листів, які потрібно завантажити. Адже якщо ви спробуєте запустити цей скрипт на своїй поштовій скриньці, ви, мабуть, не захочете завантажувати сотні листів, що там знаходяться;
  • рядок 25: логічне значення, яке вказує, чи слід видаляти лист після його прочитання (delete=True);
  • рядок 26: значення True для атрибута [ssl] означає, що сервер POP3, визначений у рядках 20–21, використовує зашифроване з’єднання;
  • рядок 27: максимальний час очікування відповідей від сервера, виражений у секундах;
  • рядок 28: папка, в яку слід переміщувати прочитані листи. Вона буде створена, якщо її ще немає. Тут вказано відносне ім’я. Під час виконання воно буде відносним до папки, з якої ви запускаєте скрипт. Для [Pycharm] цією папкою буде папка скрипта [pop3/02];

Скрипт [pop3/02/main] має такий вигляд:


# імпорт
import email
import os
import poplib
import shutil


# читання поштової скриньки
def readmails(mailbox: dict, verbose: bool):
    # читає поштову скриньку, описану у словнику [mailbox]
    # якщо verbose=True, відстежує обмін даними між клієнтом і сервером


# main ----------------------------------------------------------------
#   клієнт POP3 (Post Office Protocol), що дозволяє читати листи

# завантажується конфігурація програми
import config
config = config.configure()

# обробка поштових скриньок по черзі
for mailbox in config['mailboxes']:
    try:
        # виведення інформації на консоль
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
        # читання поштової скриньки у режимі детального виведення
        readmails(mailbox, True)
        # кінець
        print("Lecture terminée...")
    except BaseException as erreur:
        # виведення повідомлення про помилку
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
  • рядки 17–36: частина скрипта [main] аналогічна частині скрипта [pop3/01];

Функція [readmails] має такий вигляд:


# читання поштової скриньки
def readmails(mailbox: dict, verbose: bool):
    # читає поштову скриньку, описану у словнику [mailbox]
    # якщо verbose=True, відстежує обмін даними між клієнтом і сервером

    # імпорт з mail_parser
    from mail_parser import save_message

    # виокремлюються параметри поштової скриньки
    # припускається, що словник [mailbox] є дійсним
    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']

    # дозволяємо виводити системні помилки
    pop3 = None
    try:
        # створюються папки для зберігання, якщо їх ще немає
        if not os.path.isdir(output):
            os.mkdir(output)
        # користувач
        dir2 = f"{output}/{user}"
        # видалити папку [dir2], якщо вона існує, а потім створити її заново
        if os.path.isdir(dir2):
            # видалення
            shutil.rmtree(dir2)
        # створення
        os.mkdir(dir2)
        # відкриття з'єднання на порту [port] з [server]
        if ssl:
            pop3 = poplib.POP3_SSL(server, port, timeout=timeout)
        else:
            pop3 = poplib.POP3(server, port, timeout=timeout)

        # з'єднання являє собою двосторонній канал зв'язку
        # між клієнтом (цією програмою) та сервером POP3, до якого встановлено з’єднання
        # цей канал використовується для обміну командами та інформацією

        # режим детального виведення
        pop3.set_debuglevel(2 if verbose else 0)
        # читання привітального повідомлення
        pop3.getwelcome(    )
        # команда USER
        réponse = pop3.user(user)
        # команда PASS
        réponse = pop3.pass_(password)
        # команда LIST
        liste = pop3.list()
        # листи знаходяться у списку [1]
        imail = 0
        nb_mails = len(liste[1])
        fini = imail == maxmails or imail == nb_mails
        éléments = liste[1]
        while not fini:
            # поточний елемент
            élément = éléments[imail]
            # елемент — це список байтів, який декодується в рядок
            desc = élément.decode()
            # ми маємо рядок, розділений пробілами
            # перший елемент — номер повідомлення
            num = desc.split()[0]
            # отримуємо повідомлення
            message = pop3.retr(int(num))
            # рядки повідомлення містяться в message [1]
            str_message = ""
            for ligne in message[1]:
                # рядок — це послідовність байтів, яку декодуємо у рядок
                str_message += f"{ligne.decode()}\r\n"
            # папка повідомлення
            dir3 = f"{dir2}/message_{num}"
            # якщо папка не існує, її створюють
            if not os.path.isdir(dir3):
                os.mkdir(dir3)
            # тема email.message.Message
            save_message(dir3, email.message_from_string(str_message), 0)
            # ще один лист
            imail += 1
            # чи досягнуто максимального ліміту?
            fini = imail == maxmails or imail == nb_mails

        # замовлення QUIT
        pop3.quit()
    finally:
        # закриття сеансу
        if pop3:
            pop3.close()

Коментарі

  • рядки 6–7: імпортується функція [mail_parser.save_message], яка використовується в рядку 80;
  • код функції інкапсульовано в блоки try (рядок 22) та finally (рядок 88). Таким чином, усі винятки передаються до основного коду, який їх зупиняє та відображає;
  • рядки 11–18: отримуємо інформацію про конфігурацію поштової скриньки;
  • рядки 23–33: усі повідомлення будуть зберігатися в папці [output/user], де [output] та [user] визначені в конфігурації. Отже, послідовно створюються папки [output], а потім [output/user]. Щоб створити останню, спочатку її видаляють у рядку 31. [shutil] — це модуль, який потрібно імпортувати. [shutil.rmtree(dir)] видаляє папку [dir] та все, що в ній міститься;
  • для всіх операцій із системними файлами використовується модуль [os], який також потрібно імпортувати;
  • рядки 34–38: встановлюється з’єднання з сервером POP3. Якщо сервер захищений, використовується клас [poplib.POP3_SSL], інакше — клас [poplib.POP3]. Атрибут [ssl], використаний у рядку 35, береться з конфігурації поштової скриньки;
  • рядок 45: встановлюється рівень журналів:
    • 0: логів немає;
    • 1: команди, надіслані клієнтом POP3, записуються в журнал;
    • 2: детальні журнали. Також видно, що отримує клієнт POP3;
  • рядок 47: після підключення сервер POP3 надсилає привітальне повідомлення. Ми його читаємо;
  • рядки 48–49: команда USER протоколу POP3;
  • рядки 50–51: команда PASS протоколу POP3;
  • рядки 52–53: команда LIST протоколу POP3. Відповідь є кортежем (response, ['mesg_num octets'…], байти), наприклад: список=(b'+OK 3 повідомлення (3859 байтів)', [b'1 584', b'2 550', b'3 2725'], 22). Бачимо, що перші два елементи кортежу — це байти (префікс b). liste[1] — це масив, у якому кожен елемент є послідовністю байтів, що містить дві інформації: номер повідомлення та його розмір у байтах;
  • рядок 56: з вищесказаного випливає, що кількість повідомлень у поштовій скриньці можна отримати за допомогою [email.message_from_bytes(data2[0][1])];
  • рядки 59–84: виконується цикл по кожному з повідомлень. Цикл зупиняється, коли всі повідомлення прочитано або досягнуто максимальної кількості листів, встановленої в налаштуваннях;
  • рядок 61: поточний елемент масиву liste[1], тобто щось на зразок b'1 584', послідовність байтів;
  • рядок 63: перетворюємо послідовність байтів на рядок символів. Тепер маємо рядок '1 584';
  • рядок 66: отримуємо номер повідомлення, тут — рядок '1';
  • рядок 68: надсилаємо команду POP3 RETR num. Отримуємо відповідь такого вигляду:

[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)]
  • (продовження)
    • message — це кортеж із трьох елементів;
    • message[1] — це масив рядків. Кожен рядок — це послідовність байтів (префікс b). Повне повідомлення складається з цього набору рядків;
    • [Return-Path, Received, To, Subject, Message-ID, Content-Type, Content-Transfer-Encoding, Content-Language] — це заголовки повідомлення. Кожен із них містить інформацію про отримане повідомлення. Ця інформація дозволить отримати тіло повідомлення (передостанній елемент масиву message[1]);
  • рядки 71–73: створюємо рядок [strMessage], що складається з усіх рядків повідомлення. Тепер ми маємо повідомлення у вигляді символьного рядка. Це повідомлення може містити інші повідомлення, а також вкладені файли. Адже вкладені файли представлені у вигляді символьних рядків. Отже, слід запам’ятати, що електронний лист спочатку є рядком символів, і саме цей рядок символів потрібно проаналізувати, щоб витягти з нього вкладені файли, можливі інші вкладені повідомлення та, звісно, тіло повідомлення — те, що написав відправник;
  • рядки 74–78: ми зберемо текст повідомлення та вкладені файли у папку [dir3];
  • рядки 79–80: аналіз повідомлення буде доручено функції [save_message]:
    • перший параметр — це [dir3], папка, в яку має бути збережено вміст повідомлення;
    • другий параметр має тип [email.message.Message]. Цей об’єкт має методи для отримання різних частин повідомлення (тіло, вкладення), а також усіх його заголовків. Щоб отримати доступ до цього об’єкта, необхідно імпортувати модуль [email]. Функція [email.message_from_string] дозволяє створити об’єкт [email.message.Message] на основі символьного рядка повідомлення;

Функція [save_message] входить до складу модуля [mail_parser]:

Image

Модуль [mail_parser] було імпортовано у рядки 6–7 функції [readmails];

У [mail_parser.py] функція [save_message] має такий вигляд:


# імпорт
import codecs
import email.contentmanager
import email.header
import email.iterators
import email.message
import os


# збереження повідомлення типу email.message.Message
# цю функцію можна викликати рекурсивно
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # вихідні дані: папка для збереження повідомлень
    # email_message: повідомлення, яке потрібно зберегти
    # irfc822: поточний номер у нумерації вкладених листів
    #
    # частина повідомлення
    part = email_message
    # заголовки [From, To, Subject] знаходяться в одній із багаточастинних частин
    # або в частині [text/*], якщо немає частини [multipart]
    keys = part.keys()
    # Поле «From» має бути частиною заголовків, інакше частина не містить потрібних заголовків
    if "From" in keys:
        # витягуються певні заголовки
        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'))}"]
        # збереження заголовків у текстовому файлі
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # запис у файл
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # тип частини [part]
    main_type = part.get_content_maintype()

Коментарі

  • рядок 12: функція приймає не більше трьох параметрів:
  • [output]: папка, в яку слід зберегти повідомлення (2-й параметр);
  • [email_message]: повідомлення типу [email.message.Message]. Цей тип є структурованим. Він містить текст електронного листа, а також усі вкладені файли та надає методи для отримання його різних елементів;
  • [irfc822]: цей параметр використовується для нумерації листів, інкапсульованих у [email_message];
  • рядок 18: об’єкт [email_message] поміщається в [part]. Тип [email.message.Message] містить частини [part] (тіло повідомлення, вкладення, вкладені листи), які також мають тип [email.message.Message]. Кожна частина [part] може мати підчастини. Таким чином, тип [email.message.Message] є деревом елементів типу [email.message.Message]:
    • [part.ismultipart()] дорівнює [True], якщо частина [part] містить підчастини. До них можна отримати доступ через [part.get_payload()];
    • коли [part.ismultipart()] дорівнює [False], це означає, що ми дійшли до листка дерева початкового повідомлення: це може бути:
      • тіла повідомлення у вигляді звичайного тексту;
      • тіла повідомлення у вигляді тексту HTML;
      • вкладення (за винятком інкапсульованого повідомлення, для якого [part.ismultipart()] дорівнює [True]);
  • через деревоподібну структуру параметра [email.message.Message] функція [save_message] викликатиметься рекурсивно. Рекурсія припиняється, коли досягаються листки дерева, тобто частина [part], для якої [part.ismultipart()] дорівнює [False];
  • рядок 21: ми запитуємо ключі (або заголовки) повідомлення, що аналізується в даний момент (яке, завдяки рекурсії, може бути частиною початкового повідомлення);
  • рядки 23–35: ми хочемо записати заголовки:
    • [From]: відправник повідомлення;
    • [To]: одержувач повідомлення;
    • [Subject] — тема повідомлення;
    • [Return-Path]: адресат, якому слід відповісти, якщо потрібно відповісти. Адже ця інформація не завжди міститься в полі [From];
    • [User-Agent]: клієнт POP3, який взаємодіє з сервером POP3;
    • [Date]: дата відправлення електронного листа;
  • рядок 23: лише одна з частин повідомлення містить ці заголовки. Для інших частин код рядків 23–35 ігноруватиметься;
  • рядки 25–30: створюється список із шести заголовків;
  • рядок 25: проаналізуємо перший заголовок:
    • [part.get(key)] дозволяє отримати заголовок, пов’язаний із ключем [key];
    • цей заголовок може бути закодований. Якщо кодування не є utf-8, заголовок декодується для повторного кодування в utf-8 за допомогою функції [decode_header];
    • перший заголовок матиме вигляд [From: pymail2lexemple@gmail.com];
  • рядки 31–35: заголовки зберігаються у файлі [output/headers.txt];

Функція [decode_header] має такий вигляд (також у файлі [mail_parser.py]):


# декодування заголовків
def decode_header(header: object) -> str:
    # декодування заголовка
    header = email.header.decode_header(f"{header}")
    # результатом є масив — тут він матиме лише один елемент типу (header, encoding)
    # якщо encoding==None, то header — це рядок
    # інакше це список байтів, закодованих за допомогою encoding
    header, encoding = header[0]
    if not encoding:
        # якщо кодування відсутнє
        return header
    else:
        # якщо кодування є, то виконується декодування
        return header.decode(encoding)

Коментарі

  • рядок 4: декодується заголовок:
    • необхідно імпортувати модуль [email.header];
    • отримуємо список кортежів [(header1,encoding1) , (header2, encoding2)…];
    • для заголовків [From, To, Subject, Return-Path, Date] список міститиме лише один елемент;
    • рядок 8: отримуємо єдиний заголовок та його кодування:
      • якщо [encoding==None], то [header] — це заголовок у вигляді символьного рядка;
      • інакше [header] — це послідовність байтів, що представляє закодований заголовок;
  • рядки 10–11: якщо кодування відсутнє, то повертається заголовок;
  • рядки 12–14: якщо кодування було, то отриманий послідовність байтів декодується у рядок символів і повертається;

Повернемося до функції [save_message]:


# збереження повідомлення типу email.message.Message
# цю функцію можна викликати рекурсивно
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # вихідні дані: папка для збереження повідомлень
    # email_message: повідомлення, яке потрібно зберегти
    # irfc822: поточний номер у нумерації вкладених листів
    #
    # частина повідомлення
    part = email_message
    # заголовки [From, To, Subject] знаходяться в одній із багаточастинних частин
    # або в частині [text/*], якщо немає частини [multipart]
    keys = part.keys()
    # Поле «From» має бути частиною заголовків, інакше частина не містить потрібних заголовків
    if "From" in keys:
        # витягуються певні заголовки
        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'))}"]
        # збереження заголовків у текстовому файлі
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # запис у файл
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # тип частини [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # якщо повідомлення має тип text/plain
    if type_of_part == "text/plain":
        # текстове повідомлення
        save_textmessage(output, part, 0)

    # якщо повідомлення має тип text/html
    elif type_of_part == "text/html":
        # повідомлення HTML
        save_textmessage(output, part, 1)

    #, якщо повідомлення є контейнером частин
    elif part.is_multipart():
        
    else:
        
    # інші частини ігноруються (не text/plain, не text/html, не attachment)
    # повертається поточне значення irfc822 (нумерація листів із вкладеннями, що зберігаються у папці output)
    return irfc822

Коментарі

  • рядки 1–26: оброблено заголовки вихідного повідомлення;
  • рядки 28–31: частини повідомлення типу [email.message.Message] мають основний тип і підтип. Їх витягують;
  • рядки 32–35: якщо оброблена частина має тип [text/plain], то ми дійшли до кінцевого вузла дерева початкового повідомлення. Це текст, який відправник написав у своєму повідомленні;
  • рядок 35: цей текст записується у файл:
    • перший параметр [output] — це папка, у якій має бути збережено текст;
    • другий параметр — це частина повідомлення, що містить текст, який потрібно зберегти;
    • третій параметр дорівнює 0 для збереження звичайного тексту, 1 — для тексту типу HTML;
  • рядки 37–40: якщо частина має тип [text/html], то ми також дійшли до листа в дереві початкового повідомлення. Це текст, який відправник написав у своєму повідомленні, цього разу у форматі HTML. Не всі поштові клієнти підтримують цей формат;

Функція [save_textmessage] працює наступним чином:


# збереження текстового повідомлення
def save_textmessage(output: str, part: email.message.Message, type_of_text: int):
    # заголовки
    headers = []
    # кодування повідомлення
    charset = part.get_content_charset()
    if charset is not None:
        charset = part.get_content_charset().lower()
        headers.append(f"Charset: {charset}")
    # режим кодування вмісту
    content_transfer_encoding = part.get("Content-Transfer-Encoding")
    if content_transfer_encoding is not None:
        headers.append(f"Transfer-Content-Encoding: {content_transfer_encoding}")
    # виникли проблеми з 8-бітним режимом
    if content_transfer_encoding == "8bit":
        # отримуємо повідомлення з електронної пошти
        msg = part.get_payload()
    else:
        # отримуємо повідомлення з електронної пошти
        msg = email.contentmanager.raw_data_manager.get_content(part)
    # залежно від типів тексту
    filename = None
    if type_of_text == 0:
        # збереження заголовків
        with codecs.open(f"{output}/headers.txt", "a", "utf-8") as file:
            # запис у файл
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")
        # текстовий файл для вмісту
        filename = f"{output}/mail.txt"
    elif type_of_text == 1:
        # HTML-файл для вмісту
        filename = f"{output}/mail.html"
    # збереження повідомлення
    with codecs.open(filename, "w", "utf-8") as file:
        # запис у файл
        file.write(msg)

Коментарі

  • як і заголовки, текст повідомлення може бути закодований. Можливі два типи кодування:
    • початкове кодування тексту (utf-8, iso-8859-1…). Це кодування, яке використовує поштовий клієнт, що надіслав повідомлення. Воно визначається за заголовком [Content-Type] отриманого повідомлення;
    • друге кодування, якому міг піддатися попередній текст для відправлення. Воно визначається за заголовком [Transfer-Content-Encoding] отриманого повідомлення;
  • рядок 6: початкове кодування тексту;
  • рядок 11: друге кодування, якому піддався текст для передачі одержувачу;
  • рядки 9, 13: ці дві інформації заносяться до списку [headers]. Вони будуть додані до інформації у файлі [headers.txt], який зберігає певні заголовки повідомлення;
  • рядок 20: [email.contentmanager.raw_data_manager.get_content] дозволяє отримати повідомлення з його початковим кодуванням 1. Кодування 2 було видалено. Однак об’єкт [email.contentmanager.raw_data_manager] підтримує лише два типи [Transfer-Content-Encoding]:
    • [quoted-printable];
    • [base64];

Інші він ігнорує. Однак, наприклад, Thunderbird використовує [Transfer-Content-Encoding] під назвою «8bit». Це кодування ігнорується, і повідомлення з символами з діакритичними знаками спотворюються. Тоді повідомлення можна отримати за допомогою методу [part.get_payload()] (рядки 15–17);

  • рядок 21: на цьому етапі повідомлення вже позбавлене кодування передачі, тобто це повідомлення в тому вигляді, в якому його написав відправник;
  • рядки 22–37: ми маємо справу з випадком, коли потрібно зберегти текстове повідомлення;
    • рядки 24–28: зберігаємо два заголовки, створені в рядках 9 та 13, у файлі [headers.txt]. Цей файл уже існує і містить заголовки. Тому для відкриття цього файлу використовується режим «a» (рядок 25). «a» означає «append» (додати), і нові заголовки додаються (у кінець файлу) до існуючого вмісту файлу [headers.txt];
    • рядок 30: ім’я файлу, у якому слід зберегти текстове повідомлення;
    • рядок 33: ім’я файлу, у якому слід зберегти повідомлення HTML;
    • рядки 34–37: текст у форматі UTF-8 зберігається у файлі;

Повернемося до функції [save_message]:


# збереження повідомлення типу email.message.Message
# цю функцію можна викликати рекурсивно
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # вихідні дані: папка для збереження повідомлень
    # email_message: повідомлення, яке потрібно зберегти
    # irfc822: поточний номер у нумерації вкладених листів
    #
    # частина повідомлення
    part = email_message
    # заголовки [From, To, Subject] знаходяться в одній із багаточастинних частин
    # або в частині [text/*], якщо немає частини [multipart]
    keys = part.keys()
    # Поле «From» має бути частиною заголовків, інакше частина не містить потрібних заголовків
    if "From" in keys:
        # витягуються певні заголовки
        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'))}"]
        # збереження заголовків у текстовому файлі
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # запис у файл
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # тип частини [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # якщо повідомлення має тип text/plain
    if type_of_part == "text/plain":
        # текстове повідомлення
        save_textmessage(output, part, 0)

    # якщо повідомлення має тип text/html
    elif type_of_part == "text/html":
        # повідомлення HTML
        save_textmessage(output, part, 1)

    #, якщо повідомлення є контейнером частин
    elif part.is_multipart():
        # окремий випадок листа з вкладенням
        if type_of_part == "message/rfc822":
            # створення нової папки output2 для електронного листа з вкладенням
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # збереження підчастин повідомлення irfc822 у папці output2
            for subpart in part.get_payload():
                # у новій папці irfc822 починається з нуля
                save_message(output2, subpart, 0)

        else:
            # немає вкладеного листа
            # збереження підчастин у поточній папці output
            # Тоді значення irfc822 слід збільшувати для кожного піделемента message/rfc822
            for subpart in part.get_payload():
                # save_message повертає останнє значення irfc822
                # на 1, якщо subpart="message/rfc822", інакше не збільшується
                irfc822 = save_message(output, subpart, irfc822)
    else:
        # інші випадки (не text/plain, не text/html, не multipart)
        # вкладення?
        disposition = part.get('Content-Disposition')
        if disposition and disposition.startswith('attachment'):
            save_attachment(output, part)
    # інші частини ігноруються (не text/plain, не text/html, не attachment)
    # повертається поточне значення irfc822 (нумерація листів із вкладеннями, що зберігаються у папці output)
    return irfc822

Коментарі

  • рядки 33–40: ми розглянули два можливі випадки повідомлення на кінці дерева початкового повідомлення (без підчастин). Нам залишилося розглянути ще два випадки:
    • рядки 43–62: випадок, коли аналізована частина сама містить підчастини (part.ismultipart()==True);
    • рядки 63–68: для решти випадків ми розглядаємо лише той, коли аналізована частина є вкладенням;

Розглянемо останній випадок. Ми знову опинилися на одному з кінців вихідного повідомлення (без підчастин). Ми вже зустрічали два випадки такого типу: типи text/plain та text/html. Тепер розглянемо випадок з вкладеним файлом.

  • рядок 66: вкладення ідентифікується за ключем [Content-Disposition];
  • рядок 67: якщо цей ключ існує і починається з рядка [attachment], то ми маємо справу з вкладенням до повідомлення;
  • рядок 68: вкладення зберігається у папці [output];

Функція [save_attachment] має такий вигляд:


# збереження вкладення
def save_attachment(output: str, part: email.message.Message):
    # ім'я файлу-вкладення
    filename = os.path.basename(part.get_filename())

    # ім'я файлу може бути закодованим
    # наприклад =?utf-8?Q?Курси-Посібники-Серж-Тах=C3=A9-1568x268=2Ep
    filename = decode_header(filename)
    # зберігаємо вкладений файл
    with open(f"{output}/{filename}", "wb") as file:
        file.write(part.get_payload(decode=True))
  • рядок 4: якщо [part] є вкладенням, то ім’я вкладеного файлу отримується за допомогою [part.get_filename]. Зберігається лише ім’я файлу, а не його шлях;
  • рядок 8: імена файлів зазвичай кодуються так само, як і заголовки повідомлення. Тому для їх декодування використовується функція [decode_header];
  • рядок 11: вміст вкладеного файлу наразі є рядком символів, отриманим шляхом кодування (часто у форматі base64) початкового вмісту файлу в текст. Щоб отримати цей початковий вміст, використовується функція [part.get_payload(decode=True)]. Параметр [decode=True] вказує, що вміст вкладеного файлу має бути декодований. У результаті отримуємо послідовність байтів;
  • рядок 10: цей послідовність байтів зберігається у файлі [output/filename]. Режим відкриття файлу «wb» означає «write binary»;

Повернемося до коду функції [save_message]:


def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # результат: папка для збереження повідомлень
    # email_message: повідомлення, яке потрібно зберегти
    # irfc822: поточний номер у нумерації вкладених листів
    #
    # частина повідомлення
    part = email_message
    # заголовки [From, To, Subject] знаходяться в одній із багаточастинних частин
    # або в частині [text/*], якщо немає частини [multipart]
    keys = part.keys()
    # Поле «From» має бути частиною заголовків, інакше частина не містить потрібних заголовків
    if "From" in keys:
        # витягуються певні заголовки
        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'))}"]
        # збереження заголовків у текстовому файлі
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # запис у файл
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # тип частини [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # якщо повідомлення має тип text/plain
    if type_of_part == "text/plain":
        # текстове повідомлення
        save_textmessage(output, part, 0)

    # якщо повідомлення має тип text/html
    elif type_of_part == "text/html":
        # повідомлення HTML
        save_textmessage(output, part, 1)

    #, якщо повідомлення є контейнером частин
    elif part.is_multipart():
        # окремий випадок листа з вкладенням
        if type_of_part == "message/rfc822":
            # створення нової папки output2 для електронного листа з вкладенням
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # збереження підчастин повідомлення irfc822 у папці output2
            for subpart in part.get_payload():
                # у новій папці irfc822 починається з нуля
                save_message(output2, subpart, 0)

        else:
            # немає вкладеного листа
            # збереження підчастин у поточній папці output
            # тоді irfc822 слід збільшувати для кожного підрозділу message/rfc822
            for subpart in part.get_payload():
                # save_message повертає останнє значення irfc822
                # збільшується на 1, якщо subpart="message/rfc822", інакше не збільшується
                irfc822 = save_message(output, subpart, irfc822)
    else:
        # інші випадки (не text/plain, не text/html, не multipart)
        # вкладення?
        disposition = part.get('Content-Disposition')
        if disposition and disposition.startswith('attachment'):
            save_attachment(output, part)
    # інші частини ігноруються (не text/plain, не text/html, не attachment)
    # повертається поточне значення irfc822 (нумерація листів із вкладеннями, що зберігаються у папці output)
    return irfc822

Коментарі

  • ми розглянули випадки кінцевих вузлів дерева початкового повідомлення: частини [text/plain, text/html et Content-Disposition=attachment;…]. Залишається розглянути випадок, коли аналізована частина є контейнером частин, тобто містить підчастини [part.is_multipart()==True], рядок 41. Щоб дійти до кінцевих вузлів дерева повідомлення, потрібно проаналізувати ці підчастини;
  • рядок 43: окремо розглядаємо випадок, коли аналізована частина має тип [message/rfc822]. Це тип електронного листа. Отже, це випадок, коли електронний лист має в якості вкладення інший електронний лист;

Код такий:


    # якщо повідомлення є контейнером частин
    elif part.is_multipart():
        # окремий випадок вкладеного листа
        if type_of_part == "message/rfc822":
            # створення нової папки output2 для вкладеного листа
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # збереження підчастин повідомлення irfc822 у папці output2
            for subpart in part.get_payload():
                # у новій папці irfc822 починається з нуля
                save_message(output2, subpart, 0)

        else:
            # немає вкладеного листа
            # збереження підчастин у поточній папці output
            # irfc822 слід збільшувати для кожного піделемента message/rfc822
            for subpart in part.get_payload():
                # save_message повертає останнє значення irfc822
                # збільшується на 1, якщо subpart="message/rfc822", інакше не збільшується
                irfc822 = save_message(output, subpart, irfc822)

    return irfc822
  • відмінність частини [message/rfc822] від інших багаточастинних частин полягає в тому, що змінюється папка для збереження;
    • рядки 6–8: для частини [message/rfc822] папкою збереження стає та, що вказана в рядку 7 — [output/rfc822_x], де x — номер вкладеного листа: 1 для першого, 2 для другого…;
    • рядок 21: для інших частин багаточастинного повідомлення папкою для збереження залишається папка [output] початкового повідомлення. Папка не змінюється;
  • рядки 10–12: кожна підчастина зберігається за допомогою рекурсивного виклику [save_message]. Третій параметр — це індекс нумерації листів, інкапсульованих у [subpart]. Спочатку цей індекс дорівнює 0;
  • рядок 21: те саме пояснення, що й для рядка 12, але значення третього параметра [irfc822] змінюється. Якщо у циклі рядків 18–21 є кілька інкапсульованих листів, їх слід розмістити у папках […/rfc822-1…/rfc822_2…]. Отже, третій параметр функції [save_message] повинен послідовно приймати значення 1, 2, 3… Для цього [save_message] присвоює значення [irfc822] (рядок 21).

Візьмемо приклад і припустимо, що список підчастин рядка 18 — це [subpart1, subpart2, subpart3, subpart4, subpart5], а [subpart1, subpart3, subpart5] — це листи у вигляді вкладень, [subpart2] — це частина типу text/plain, а [subpart4] — вкладення, і що в повідомленні [irfc822=0] ми ще не зустрічали листа з вкладенням. У цьому випадку:

  • (продовження)
    • [subpart1] зберігається у рядку 21: функція [saveMessage] виконується з параметром irfc822=0;
    • [subpart1] є електронним листом із вкладенням, тому значення irfc822 змінюється на 1 (рядок 6 коду). Створюється папка [output/irfc822_1]. Отже, значення, яке повертає [saveMessage(ouput,subpart1,0)], дорівнює 1 (рядок 23);
    • [subpart2] зберігається у рядку 21: функція [saveMessage] виконується з irfc822=1;
    • [subpart2] не є вкладенням до листа. Тому значення irfc822 залишається рівним 1. Саме це значення отримується у рядку 21;
    • [subpart3] зберігається у рядку 21: функція [save_message] виконується з irfc822=1;
    • [subpart3] є електронним листом із вкладенням, тому значення irfc822 змінюється на 2 (рядок 6 коду). Створюється файл [output/irfc822_2]. Отже, значення, яке повертає [save_message(ouput,subpart1,1)], дорівнює 2 (рядок 21);
    • [subpart4] зберігається у рядку 21: функція [save_message] виконується з irfc822=2;
    • [subpart4] не є вкладенням до листа. Отже, irfc822 залишається рівним 2. Це значення, отримане у рядку 21;
    • [subpart5] зберігається у рядку 21: функція [save_message] виконується з irfc822=2;
    • [subpart5] — це електронний лист із вкладенням, тому значення irfc822 змінюється на 3 (рядок 6 коду). Створюється папка [output/irfc822_3]. Отже, значення, яке повертає [save_message(ouput,subpart1,2)], дорівнює 3 (рядок 21);

Приклади виконання

Ми надсилаємо 4 електронні листи до [pymail2parlexemple@gmail.com] з: [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/] ;

Усі листи матимуть тему [hélène va au marché], а в тексті — [acheter des légumes]. Ми хочемо перевірити, як відображаються символи з діакритичними знаками.

Ми читаємо їх за допомогою скрипта [pop3/02/main], налаштованого за допомогою такого файлу [pop3/02/config]:


import os


def configure() -> dict:
    # конфігурація додатка
    config = {
        # список поштових скриньок, якими потрібно керувати
        "mailboxes": [
            # сервер: сервер POP3
            # порт: порт сервера POP3
            # user: користувач, повідомлення якого потрібно прочитати
            # password: його пароль
            # maxmails: максимальна кількість листів для завантаження
            # timeout: максимальний час очікування відповіді від сервера
            # delete: має значення «true», якщо завантажені повідомлення потрібно видалити з сервера
            # ssl: має значення «true», якщо читання листів відбувається через захищене з’єднання
            # output: папка для зберігання завантажених повідомлень

            {
                "server": "pop.gmail.com",
                "port": "995",
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prD&@1QZ3TG",
                "maxmails": 10,
                "delete": False,
                "ssl": True,
                "timeout": 2.0,
                "output": "output"
            }
        ]
    }
    # абсолютний шлях до папки зі скриптом
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # абсолютні шляхи до папок, які слід включити до syspath
    absolute_dependencies = [
        # локальна папка
        f"{script_dir}/../../shared",
    ]

    # конфігурація syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # виконується конфігурація
    return config

Результат такий:

Image

Повідомлення 1 — це те, що надіслано Thunderbird:

Image

  • у [5] Thunderbird [3] використовує [Transfer-Content-Encoding] типу [8bit];
  • у [4]: повідомлення закодовано у UTF-8;

Повідомлення 2 — це те, що надіслано клієнтом em:

Image

Image

Слід зауважити, що [em Client] кодує тексти у форматі utf-8 [4] і передає їх у форматі [quoted-printable] [5]. Він також надіслав копію повідомлення у форматі HTML та [7-8]. Усі поштові клієнти, які тут тестувалися, можуть це робити. Це параметр налаштувань.

Повідомлення 3 — це те, що надіслав Gmail:

Image

Слід зауважити, що Gmail кодує тексти у форматі utf-8 [3] і передає їх у форматі [quoted-printable] [4]. У форматі [6] — це версія повідомлення HTML.

Повідомлення 4 — це те, що надіслано з Outlook:

Image

Слід зауважити, що Outlook кодує тексти у форматі iso-8859-1 [3] і перетворює їх у [quoted-printable] та [4].

Наведені вище приклади демонструють дві речі:

  • наш клієнт [pop3/02] працював нормально;
  • поштові клієнти по-різному відправляють листи;

Тепер розглянемо вкладені файли. За допомогою Thunderbird ми очищаємо поштову скриньку користувача [pymail2parlexemple@gmail.com]. Потім використовуємо скрипт [smtp/03/main] для відправлення листа з наступною конфігурацією [smtp/03/config]:


import os


def configure() -> dict:
    # конфігурація додатка
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        # опис: опис надісланого листа
        # smtp-server: сервер SMTP
        # smtp-port: порт сервера SMTP
        # from: відправник
        # to: одержувач
        # subject: тема листа
        # message: текст листа
        "mails": [
            {
                "description": "mail to gmail via gmail avec smtplib",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymail2parlexemple@gmail.com",
                "to": "pymail2parlexemple@gmail.com",
                "subject": "to gmail via gmail avec smtplib",
                # перевіряємо символи з діакритичними знаками
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP з автентифікацією
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # тут потрібно вказати абсолютні шляхи до вкладених файлів
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                    f"{script_dir}/attachments/mail attaché 1.eml",
                ]
            }
        ]
    }
  • рядки 31–33: ми додаємо до листа:
  • файл Word;
  • файл PDF;
  • лист із цими двома файлами у вкладенні;

Після відправлення листа ми запускаємо скрипт [pop3/02] для зчитування поштової скриньки користувача [pymail2parlexemple@gmail.com]. Отримано такі результати:

Image

  • у [1]: повідомлення з двома вкладеними файлами;
  • у [2]: сам лист із двома вкладеними файлами;

Висновок

Модуль [mail_parser.py] є особливо складним. Це пов’язано зі складністю самих листів. Ми будемо використовувати цей модуль для протоколу IMAP.

21.7. Протокол IMAP

21.7.1. Вступ

Для читання листів, що зберігаються на поштовому сервері, існують два протоколи:

  • протокол POP3 (Post Office Protocol) — історично перший протокол, але зараз він майже не використовується;
  • протокол IMAP (Internet Message Access Protocol) — новіший за POP3 і найпоширеніший на сьогодні;

Щоб ознайомитися з протоколом IMAP, ми скористаємося такою архітектурою:

Image

  • [Serveur B] залежно від обставин буде:
    • локальним сервером IMAP, реалізованим поштовим сервером [hMailServer];
    • сервером [imap.gmail.com:993], який є сервером IMAP поштового менеджера [Gmail];
  • [Client A] — це скрипт на Python, що використовує модулі Python для управління вкладеними файлами, а також для використання зашифрованого та автентифікованого з’єднання, коли цього вимагає сервер IMAP;

Протокол IMAP виходить за межі протоколу POP3:

  • листи зберігаються на сервері IMAP і можуть бути упорядковані у папки;
  • клієнт IMAP може надсилати команди на створення / редагування / видалення цих папок;

Розглянемо приклад із Thunderbird. У такій архітектурі:

Image

  • Thunderbird — це клієнт A;
  • [imap.gmail.com] — це сервер B (Gmail);

Створимо папку в листах користувача [pymail2parlexemple@gmail.com] за допомогою Thunderbird:

Image

  • у [1-6] ми створюємо папку [dossier1];

Image

  • у [7-8] ми переміщуємо (за допомогою миші) усі файли з папки [Courrier entrant] у папку [dossier1];

Тепер увійдімо на веб-сайт Gmail і авторизуємося як користувач [pymail2parlexemple@gmail.com]:

Image

  • у [2-3] папка «Вхідні» порожня;
  • у [1] — папка [dossier1], яка була створена;

Image

  • у [4-6]: листи, які було переміщено до папки [dossier1];

Ми маємо таку архітектуру:

Image

  • Клієнт A — це програма Thunderbird;
  • Клієнт C — це веб-додаток Gmail;
  • Сервер B — це сервер Gmail із ідентифікатором IMAP;

Дерево папок користувача підтримується сервером IMAP. Далі всі клієнти IMAP синхронізуються з ним, щоб відобразити користувачеві папки його облікового запису. У цьому випадку Thunderbird надіслав кілька команд для:

  • створення папки [dossier1];
  • перенести повідомлення в цю папку;

21.7.2. скрипт [imap/main]: клієнт IMAP з модулем [imaplib]

Image

Скрипт [imap/main] налаштовується за допомогою наступного скрипта [imap/config]:


import os


def configure() -> dict:
    # налаштування програми
    config = {
        # список поштових скриньок, якими потрібно керувати
        "mailboxes": [
            # сервер: сервер IMAP
            # порт: порт сервера IMAP
            # user: користувач, повідомлення якого потрібно прочитати
            # password: його пароль
            # maxmails: максимальна кількість листів для завантаження
            # timeout: максимальний час очікування відповіді від сервера
            # delete: має значення «true», якщо завантажені повідомлення потрібно видалити з сервера
            # ssl: має значення «true», якщо читання листів відбувається через захищене з’єднання
            # output: папка для зберігання завантажених повідомлень

            {
                "server": "imap.gmail.com",
                "port": "993",
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                "maxmails": 10,
                "ssl": True,
                "timeout": 2.0,
                "output": "output"
            }
        ]
    }
    # абсолютний шлях до папки, де знаходиться скрипт
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # абсолютні шляхи до папок, які слід включити до syspath
    absolute_dependencies = [
        # локальна папка
        f"{script_dir}/../shared",
    ]

    # конфігурація syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # використовуємо конфігурацію
    return config

Коментарі

  • рядки 8–29: ключ [mailboxes] пов’язаний зі списком поштових скриньок, які потрібно перевірити;
  • рядок 20: сервер IMAP;
  • рядок 21: його службовий порт;
  • рядки 22–23: користувач, листи якого потрібно переглянути;
  • рядок 24: максимальна кількість листів, які потрібно переглянути;
  • рядок 25: вказує, чи потрібно встановлювати захищене з’єднання з сервером IMAP (True) чи ні (False);
  • рядок 26: максимальний час очікування відповіді від сервера;
  • рядок 27: папка для збереження прочитаних листів;

Скрипт [imap/main] має такий вигляд:


# імпортування
import email
import imaplib
import os
import shutil


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

def readmails(mailbox: dict):
    


# main ----------------------------------------------------------------
#   клієнт IMAP для читання електронних листів

# отримуємо конфігурацію додатка
import config
config = config.configure()

# обробка поштових скриньок по черзі
for mailbox in config['mailboxes']:
    try:
        # виведення інформації на консоль
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']} / {mailbox['server']}:{mailbox['port']}")
        # читання поштової скриньки
        readmails(mailbox)
        # кінець
        print("Lecture terminée...")
    # крім BaseException як помилки:
    #     # виводимо повідомлення про помилку
    #     print(f"Сталася така помилка: {помилка}")
    finally:
        pass

Коментарі

  • рядки 14–36: тут ми бачимо підхід, який вже зустрічали у скрипті |pop3/02/main|;

Функція [readmails] має такий вигляд:


def readmails(mailbox: dict):
    # дозволяємо винятком просуватися вгору
    #
    # модуль аналізатора електронної пошти
    from mail_parser import save_message

    # отримуємо інформацію про конфігурацію
    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']
    #
    # починаємо
    imap_resource = None
    try:
        # створюються папки для зберігання, якщо їх ще немає
        if not os.path.isdir(output):
            os.mkdir(output)
        # користувач
        dir2 = f"{output}/{user}"
        # видаляємо папку [dir2], якщо вона існує, а потім створюємо її заново
        if os.path.isdir(dir2):
            # видалення
            shutil.rmtree(dir2)
        # створення
        os.mkdir(dir2)
        # підключення до сервера IMAP
        if ssl:
            imap_resource = imaplib.IMAP4_SSL(server, port)
        else:
            imap_resource = imaplib.IMAP4(server, port)
        # тайм-аут клієнтських з'єднань
        sock = imap_resource.socket()
        sock.settimeout(timeout)
        # аутентифікація
        imap_resource.login(user, password)
        # вибирається папка INBOX (вхідна пошта)
        imap_resource.select('INBOX')
        # витягуються всі повідомлення з цієї папки: критерій ALL
        # кодування не вказано: None
        typ1, data1 = imap_resource.search(None, 'ALL')
        # print(f"typ={typ1}, data={data1}")

        # data1[0] — це масив байтів, що містить номери всіх повідомлень, розділених пробілом
        nums = data1[0].split()
        imail = 0
        fini = imail >= maxmails or imail >= len(nums)
        # листи читаються по одному
        while not fini:
            # num — це номер повідомлення у двійковій системі числення
            num = nums[imail]
            # print(f"повідомлення № {num}")

            # отримуємо повідомлення № num
            typ2, data2 = imap_resource.fetch(num, '(RFC822)')
            # print(f"type={typ2}, data={data2}")

            # data — це список, що містить кортежі, у даному випадку лише один
            # data[0] — це кортеж, dataQZXW2HTMLBWzBdZQXQZXW2HTMLBWzFdZQX — другий елемент цього кортежу
            # dataQZXW2HTMLBWzBdZQXQZXW2HTMLBWzFdZQX містить послідовність байтів, що представляє всі рядки повідомлення
            # під повідомленням слід розуміти текст повідомлення + усі вкладені файли

            # повідомлення отримується як тип email.message.Message
            message = email.message_from_bytes(data2[0][1])
            # папка повідомлення
            dir3 = f"{dir2}/message_{int(num)}"
            # якщо папка не існує, її створюють
            if not os.path.isdir(dir3):
                os.mkdir(dir3)
            # зберігаємо його
            save_message(dir3, message)
            # наступне повідомлення
            imail += 1
            fini = imail >= maxmails or imail >= len(nums)
    finally:
        if imap_resource:
            # закрити з'єднання з поштовою скринькою
            imap_resource.close()
            # виходимо з сервера IMAP
            imap_resource.logout()

Коментарі

  • рядки 7–15: отримуємо елементи конфігурації;
  • рядки 19, 79: код контролюється за допомогою блоку try / finally. Отже, винятки не перехоплюються (відсутність оператора except), і вони передаються до викличного коду, який їх зупиняє та відображає;
  • рядки 23–30: створюється папка для збереження листів;
  • рядки 31–35: встановлюється з’єднання з сервером IMAP. Використовуваний клас відрізняється залежно від того, чи маємо ми справу із захищеним сервером IMAP (IMAP4_SSL), чи ні (IMAP4);
  • рядки 36–38: встановлюється тайм-аут для зв’язку між клієнтом і сервером;
  • рядки 39–40: виконується аутентифікація на сервері IMAP;
  • рядки 41–42: ми бачили, що поштова скринька користувача IMAP може бути організована у папки. Папка [INBOX] — це папка вхідної пошти. Щоб вибрати папку [dossier1], слід ввести [imapResource.select('dossier1')];
  • рядки 43–45: запитується список усіх повідомлень, знайдених у папці [INBOX]:
    • перший параметр [imapResource.search] — це тип кодування. [None] означає «без фільтрації за кодуванням»;
    • другий параметр — це критерій. Його можна виразити різними способами. Критерій [ALL] означає, що потрібно отримати всі повідомлення з папки;

Результат для [imapResource.search] виглядає так:


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

[data] — це список, що містить номери отриманих повідомлень. Вони подані у двійковій системі числення. У наведеному вище прикладі у папці було знайдено два повідомлення [INBOX];

  • рядок 49: витягуються номери повідомлень. Вище ми отримаємо список [b'1' b'2'] — список номерів, закодованих у двійковій системі числення;
  • рядки 53–78: запускаємо цикл для зчитування повідомлень із папки [INBOX];
  • рядки 54–55: номер повідомлення;
  • рядки 58–59: запит на повідомлення № [num] надсилається на сервер IMAP;
    • перший параметр — це номер потрібного повідомлення;
    • другий параметр — це рядок «(part1)(part2)…», де [parti] — це назва частини повідомлення. Я не заглиблювався в це питання. Назва (RFC822) позначає весь лист;

Ми отримуємо щось у такому вигляді:


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

Елемент [data] тут є списком з 1 елементом, і цей єдиний елемент є кортежем із трьох елементів:


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

Другий елемент цього кортежу — це бінарний рядок, що представляє весь запитуваний лист. У наведеному вище прикладі можна впізнати елементи, які вже розглядалися під час вивчення модуля [mail_parser].

data[0] являє собою кортеж із двох елементів. data[0][1] представляє рядки повідомлення у двійковій формі.

  • рядок 68: функція [taxpayers[slice(10,12)]] створює об’єкт типу [email.message.Message] на основі рядків повідомлення. Тип [email.message.Message] — це тип параметра модуля [mail_parser], який ми написали раніше;
  • рядки 69–73: ми створюємо папку для збереження повідомлення № [num];
  • рядок 75: ми викликаємо функцію [save_message] з модуля [mail_parser] з рядка 5. Ця функція була описана в розділі |pop3/02/main|;
  • рядки 76–78: виконується цикл для обробки наступного повідомлення;
  • рядки 79–84: незалежно від того, чи сталася помилка:
    • рядок 82: закривається з’єднання з запитуваною папкою;
    • рядок 84: відбувається відключення від сервера IMAP;

Отримані результати збігаються з тими, що були отримані за допомогою скрипта [pop3/02/main]. Це нормально, оскільки використовується той самий парсер пошти [mail_parser].