Skip to content

21. Funções de internet

Abordamos agora as funções de internet do Python que nos permitem programar TCP / IP (Transfer Control Protocol / Internet Protocol).

Image

21.1. Noções básicas de programação para a internet

21.1.1. Noções gerais

Consideremos a comunicação entre duas máquinas remotas A e B:

Image

Quando um aplicativo AppA da máquina A deseja se comunicar com um aplicativo AppB da máquina B na Internet, ele precisa saber algumas informações:

  • o endereço IP (Protocolo de Internet) ou o nome da máquina B;
  • o número da porta com a qual a aplicação AppB opera. De fato, a máquina B pode hospedar várias aplicações que operam na Internet. Quando recebe informações provenientes da rede, ela precisa saber a qual aplicação essas informações se destinam. Os aplicativos da máquina B têm acesso à rede por meio de interfaces, também chamadas de portas de comunicação. Essa informação está contida no pacote recebido pela máquina B para que ele seja entregue ao aplicativo correto;
  • os protocolos de comunicação compreendidos pela máquina B. Em nosso estudo, utilizaremos apenas os protocolos TCP-IP;
  • o protocolo de comunicação aceito pelo aplicativo AppB. De fato, as máquinas A e B vão “se comunicar”. O que elas vão transmitir será encapsulado nos protocolos TCP-IP. No entanto, quando, no final da cadeia, o aplicativo AppB receber a informação enviada pelo aplicativo AppA, ele precisará ser capaz de interpretá-la. Isso é semelhante à situação em que duas pessoas, A e B, se comunicam por telefone: o diálogo delas é transmitido pelo telefone. A fala será codificada na forma de sinais pelo telefone A, transmitida pelas linhas telefônicas, chegará ao telefone B para ser decodificada. A pessoa B então ouve as palavras. É aí que entra o conceito de protocolo de diálogo: se A fala francês e B não entende essa língua, A e B não poderão dialogar de forma eficaz;

Portanto, os dois aplicativos que se comunicam devem chegar a um acordo sobre o tipo de diálogo que irão adotar. Por exemplo, o diálogo com um serviço ftp não é o mesmo que com um serviço pop: esses dois serviços não aceitam os mesmos comandos. Eles possuem um protocolo de diálogo diferente;

21.1.2. As características do protocolo TCP

Aqui, abordaremos apenas as comunicações de rede que utilizam o protocolo de transporte TCP, cujas principais características são as seguintes:

  • o processo que deseja transmitir estabelece, em primeiro lugar, uma conexão com o processo destinatário das informações que irá transmitir. Essa conexão é estabelecida entre uma porta da máquina transmissora e uma porta da máquina receptora. Entre as duas portas, é criado um caminho virtual que ficará reservado exclusivamente aos dois processos que estabeleceram a conexão;
  • todos os pacotes enviados pelo processo de origem seguem esse caminho virtual e chegam na ordem em que foram enviados;
  • A informação transmitida tem um caráter contínuo. O processo emissor envia informações em seu próprio ritmo. Essas informações não são necessariamente enviadas imediatamente: o protocolo TCP aguarda até ter quantidade suficiente para enviá-las. Elas são armazenadas em uma estrutura chamada segmento TCP. Uma vez preenchido, esse segmento será transmitido para a camada IP, onde será encapsulado em um pacote IP;
  • cada segmento enviado pelo protocolo TCP é numerado. O protocolo TCP destinatário verifica se está recebendo os segmentos na sequência correta. Para cada segmento recebido corretamente, ele envia um aviso de recebimento ao remetente;
  • quando este último o recebe, ele notifica o processo emissor. Assim, este pode saber que um segmento chegou ao destino;
  • se, após um certo tempo, o protocolo TCP que enviou um segmento não receber uma confirmação de recebimento, ele retransmite o segmento em questão, garantindo assim a qualidade do serviço de encaminhamento da informação;
  • o circuito virtual estabelecido entre os dois processos que se comunicam é full-duplex: isso significa que a informação pode transitar nos dois sentidos. Assim, o processo de destino pode enviar confirmações de recebimento mesmo enquanto o processo de origem continua enviando informações. Isso permite, por exemplo, que o protocolo de origem TCP envie vários segmentos sem esperar pela confirmação de recebimento. Se, após um certo tempo, ele perceber que não recebeu a confirmação de recebimento de um determinado segmento nº n, ele retomará a transmissão dos segmentos a partir desse ponto;

21.1.3. A relação cliente-servidor

Frequentemente, a comunicação na Internet é assimétrica: a máquina A inicia uma conexão para solicitar um serviço à máquina B, especificando que deseja estabelecer uma conexão com o serviço SB1 da máquina B. Esta aceita ou recusa. Se aceitar, a máquina A pode enviar suas solicitações ao serviço SB1. Essas solicitações devem estar em conformidade com o protocolo de comunicação compreendido pelo serviço SB1. Estabelece-se, assim, um diálogo de solicitação-resposta entre a máquina A, chamada de máquina cliente, e a máquina B, chamada de máquina servidor. Um dos dois parceiros encerrará a conexão.

21.1.4. Arquitetura de um cliente

A arquitetura de um programa de rede que solicita os serviços de um aplicativo servidor será a seguinte:

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. Arquitetura de um servidor

A arquitetura de um programa que oferece serviços será a seguinte:

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

O programa servidor trata de maneira diferente o pedido de conexão inicial de um cliente e seus pedidos subsequentes destinados a obter um serviço. O programa não presta o serviço por conta própria. Se o fizesse, durante o tempo em que o serviço estivesse sendo prestado, ele não estaria mais à escuta de pedidos de conexão e, portanto, os clientes não seriam atendidos. Ele procede de outra forma: assim que uma solicitação de conexão é recebida na porta de escuta e, em seguida, aceita, o servidor cria uma tarefa encarregada de prestar o serviço solicitado pelo cliente. Esse serviço é prestado em outra porta da máquina servidora, chamada de porta de serviço. Dessa forma, é possível atender a vários clientes ao mesmo tempo.

Uma tarefa de serviço terá a seguinte estrutura:

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. Conheça os protocolos de comunicação da Internet

21.2.1. Introdução

Quando um cliente se conecta a um servidor, estabelece-se então um diálogo entre eles. A natureza desse diálogo define o que chamamos de protocolo de comunicação do servidor. Entre os protocolos mais comuns da Internet, encontram-se os seguintes:

  • HTTP: HyperText Transfer Protocol — o protocolo de comunicação com um servidor web (servidor HTTP);
  • SMTP: Simple Mail Transfer Protocol — o protocolo de comunicação com um servidor de envio de e-mails (servidor SMTP);
  • POP: Post Office Protocol — o protocolo de comunicação com um servidor de armazenamento de e-mails (servidor POP). Trata-se de recuperar os e-mails recebidos, e não de enviá-los;
  • IMAP: Internet Message Access Protocol — o protocolo de comunicação com um servidor de armazenamento de e-mails (servidor IMAP). Esse protocolo substituiu gradualmente o protocolo POP, mais antigo;
  • FTP: File Transfer Protocol — o protocolo de comunicação com um servidor de armazenamento de arquivos (servidor FTP);

Todos esses protocolos têm a particularidade de serem protocolos de linhas de texto: o cliente e o servidor trocam linhas de texto. Se tivermos um cliente capaz de:

  • estabelecer uma conexão com um servidor TCP;
  • exibir no console as linhas de texto que o servidor lhe envia;
  • enviar ao servidor as linhas de texto que um usuário digitaria no teclado;

portanto, é possível se comunicar com um servidor TCP que utilize um protocolo de linhas de texto, desde que se conheçam as regras desse protocolo.

21.2.2. Utilitários TCP

Image

Nos códigos associados a este documento, encontram-se dois utilitários de comunicação TCP:

  • [RawTcpClient] permite conectar-se à porta P de um servidor S;
  • O [RawTcpServer] permite criar um servidor que aguarda clientes na porta P;

São dois programas em C# cujos códigos-fonte estão à sua disposição. Portanto, você pode modificá-los.

O servidor TCP [RawTcpServer] éé chamado com a sintaxe [RawTcpServeur port] para criar um serviço TCP na porta [port] da máquina local (o computador no qual você está trabalhando):

  • o servidor pode atender a vários clientes simultaneamente;
  • o servidor executa os comandos digitados pelo usuário no teclado. São eles:
    • list: lista os clientes atualmente conectados ao servidor. Eles são exibidos no formato [id=x-nom=y]. O campo [id] serve para identificar os clientes;
    • send x [texte]: envia texto para o cliente nº x (id=x). Os colchetes [] não são enviados. Eles são necessários no comando e servem para delimitar visualmente o texto enviado ao cliente;
    • close x: encerra a conexão com o cliente nº x;
    • quit: encerra todas as conexões e interrompe o serviço;
  • as linhas enviadas pelo cliente ao servidor são exibidas no console;
  • todas as trocas de dados são registradas em um arquivo de texto com o nome [machine-port.txt], onde
    • [machine] é o nome da máquina na qual o código está sendo executado;
    • [port] é a porta do serviço que responde às solicitações do cliente;

O cliente TCP [RawTcpClient] é chamado com a sintaxe [RawTcpClient serveur port] para se conectar à porta [port] do servidor [serveur]:

  • as linhas digitadas pelo usuário no teclado são enviadas ao servidor;
  • as linhas enviadas pelo servidor são exibidas no console;
  • todas as trocas de dados são registradas em um arquivo de texto com o nome [serveur-port.txt];

Vejamos um exemplo. Abrimos duas janelas de terminal PyCharm e, em cada uma delas, acessamos a pasta de utilitários:

Image

Em uma das janelas, iniciamos o servidor [RawTcpServer] na porta 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 :
  • na linha 1, estamos na pasta de utilitários;
  • linha 1, iniciamos o servidor TCP na porta 100;
  • linhas 2-4: o servidor fica aguardando um cliente TCP e exibe uma lista de comandos que o usuário pode digitar no teclado;
  • linha 5, o servidor aguarda um comando digitado pelo usuário;

Na outra janela de comandos, iniciamos o cliente 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) :
  • linha 1, estamos na pasta de utilitários;
  • na linha 1, iniciamos o cliente TCP: instruímos que ele se conecte à porta 100 da máquina local (aquela na qual o código de [RawTcpClient] está sendo executado);
  • na linha 2, o cliente conseguiu se conectar ao servidor. Indicamos as coordenadas do cliente: ele está na máquina [DESKTOP-30FF5FB] (a máquina local neste exemplo) e usa a porta [51173] para se comunicar com o servidor:
  • linha 3, o cliente aguarda um comando digitado pelo usuário no teclado;

Voltemos à janela do servidor. Seu conteúdo mudou:


(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...
  • linha 5, um cliente foi detectado. O servidor atribuiu a ele o número 1. O servidor identificou corretamente o cliente remoto (máquina e porta);
  • linha 6, o servidor volta a aguardar um novo cliente;

Voltemos à janela do cliente e enviemos um comando ao servidor:


(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
  • linha 4, o comando enviado ao servidor;

Voltemos à janela do servidor. Seu conteúdo mudou:


(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]
  • linha 7, entre colchetes, a mensagem recebida pelo servidor;

Vamos enviar uma resposta ao cliente:


(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 :
  • linha 8, a resposta enviada ao cliente 1. Apenas o texto entre colchetes é enviado, não os próprios colchetes;

Voltemos à janela do cliente:


(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]
  • linha 5, a resposta recebida pelo cliente. O texto recebido é aquele entre colchetes;

Voltemos à janela do servidor para ver outros comandos:


(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
  • linha 9, solicitamos a lista de clientes;
  • linha 10, a resposta;
  • linha 11, encerramos a conexão com o cliente nº 1;
  • linha 12, a confirmação do servidor;
  • linha 13, desligamos o servidor;
  • linha 14, a confirmação do servidor;

Voltemos à janela do cliente:


(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...
  • linha 6, o cliente detectou o fim do serviço;

Dois arquivos de log foram criados, um para o servidor e outro para o cliente:

Image

  • em [1], os logs do servidor: o nome do arquivo é o nome do cliente no formato [machine-port]. Isso permite ter arquivos de log diferentes para clientes diferentes;
  • em [2], os logs do cliente: o nome do arquivo é o nome do servidor no formato [machine-port];

Os logs do servidor são os seguintes:


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

Os logs do cliente são os seguintes:


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

21.3. Obter o nome ou o endereço IP de um computador na Internet

Image

Os computadores na Internet são identificados por um endereço IP (IPv4 ou IPv6) e, na maioria das vezes, por um nome. Mas, no fim das contas, apenas o endereço IP é utilizado pelos protocolos de comunicação da Internet. Portanto, é necessário saber o endereço IP de um computador identificado pelo nome.

O script [ip-01.py] é o seguinte:


# importações
import socket


# ------------------------------------------------
def get_ip_and_name(nom_machine: str):
    # nom_machine: nome da máquina cujo endereço se deseja obter IP
    try:
        # nom_machine-->endereço IP
        ip = socket.gethostbyname(nom_machine)
        print(f"ip[{nom_machine}]={ip}")
    except socket.error as erreur:
        # é exibido o erro
        print(f"ip[{nom_machine}]={erreur}")
        return

    try:
        # endereço IP --> nom_machine
        names = socket.gethostbyaddr(ip)
        print(f"names[{ip}]={names}")
    except socket.error as erreur:
        # é exibido o erro
        print(f"names[{ip}]={erreur}")
        return


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

# as máquinas da Internet
hosts = ["istia.univ-angers.fr", "www.univ-angers.fr", "sergetahe.com", "localhost", "xx"]

# endereços IP das máquinas HOTES
for host in hosts:
    print("-------------------------------------")
    get_ip_and_name(host)
# fim
print("Terminé...")

Comentários

  • linha 2: o módulo [socket] fornece as funções necessárias para o gerenciamento de soquetes da Internet. [socket] significa tomada elétrica, tomada de rede;
  • linha 6: a função [get_ip_and_name] permite, a partir do nome de internet de uma máquina, obter:
    • o endereço IP do computador;
    • o nome do computador obtido a partir do endereço IP anterior;
  • linha 10: a função [socket.gethostbyname] permite obter o endereço IP de uma máquina a partir de um desses nomes (uma máquina na Internet pode ter um nome principal e aliases);
  • linha 12: as funções relacionadas a sockets lançam a exceção [socket.error] assim que ocorre um erro;
  • linha 19: a função [socket.gethostbyaddr] permite obter o nome de uma máquina a partir de seu endereço IP. Veremos que é possível obter um nome diferente daquele passado na linha 6;
  • linha 30: uma lista de nomes de máquinas. O último nome está incorreto. O nome [localhost] refere-se à máquina na qual você está trabalhando e que está executando o script;
  • linhas 33-35: exibimos os IP dessas máquinas;

Resultados:


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. O protocolo HTTP (Protocolo de Transferência HyperText)

21.4.1. Exemplo 1

Image

Quando um navegador exibe um URL, ele atua como cliente de um servidor web ou, em outras palavras, de um servidor HTTP. É ele quem toma a iniciativa e começa enviando uma série de comandos ao servidor. Para este primeiro exemplo:

  • o servidor será o utilitário [RawTcpServer];
  • o cliente será um navegador;

Primeiro, iniciamos o servidor na porta 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 :

Em seguida, usando um navegador, solicitamos o URL [http://localhost:100], ou seja, informamos que o servidor HTTP consultado está operando na porta 100 da máquina local:

Image

Voltemos à janela do servidor:


(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...
  • linha 5, o cliente que se conectou;
  • linhas 9 a 22: a sequência de linhas de texto que ele enviou:
    • linha 9: essa linha tem o formato [GET URL HTTP/1.1]. Ela solicita o URL / e pede ao servidor que utilize o protocolo HTTP 1.1;
    • linha 10: esta linha tem o formato [Host: serveur:port]. O uso de maiúsculas ou minúsculas no comando [Host] não faz diferença. Vale lembrar aqui que o cliente consulta um servidor local operando na porta 100;
    • linha 14: o comando [User-Agent] fornece a identidade do cliente;
    • linha 15: o comando [Accept] indica quais tipos de documentos são aceitos pelo cliente;
    • linha 21: o comando [Accept-Language] indica em que idioma os documentos solicitados devem ser fornecidos, caso existam em vários idiomas;
    • linha 11: o comando [Connection] indica o modo de conexão desejado: [keep-alive] indica que a conexão deve ser mantida até que as trocas de dados estejam concluídas;
    • linha 22: o cliente encerra seus comandos com uma linha em branco;

Encerramos a conexão ao encerrar o servidor:


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

21.4.2. Exemplo 2

Agora que conhecemos os comandos enviados por um navegador para solicitar um URL, vamos solicitar esse URL com nosso cliente TCP [RawTcpClient]. O servidor Apache do Laragon (parágrafo |Instalação do Laragon|) será nosso servidor web.

Vamos iniciar o Laragon e, em seguida, o servidor web Apache:

Image

Image

Agora, usando um navegador, vamos acessar a página URL [http://localhost:80]. Aqui, especificamos apenas o servidor [localhost:80] e nenhum documento URL. Nesse caso, é o URL / que está sendo solicitado, ou seja, a raiz do servidor web:

Image

  • em [1], o URL solicitado. Inicialmente, digitamos [http://localhost:80] e o navegador (Firefox, neste caso) simplesmente a transformou em [localhost], pois o protocolo [http] é implícito quando nenhum protocolo é mencionado e a porta [80] é implícita quando a porta não é especificada;
  • em [2], a página raiz / do servidor web consultado;

Agora, vamos visualizar o texto recebido pelo navegador:

Image

  • clicamos com o botão direito do mouse na página recebida e selecionamos a opção [2]. Obtemos o seguinte código-fonte:

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

Agora, vamos solicitar o URL [http://localhost:80] com nosso cliente 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) :
  • na linha 1, nos conectamos à porta 80 do servidor localhost. É lá que opera o servidor web do Laragon;

Agora digitamos os comandos que descobrimos no parágrafo anterior:


(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...
  • linha 4, o comando [GET]. Solicitamos a raiz / do servidor web;
  • linha 5, o comando [Host];
  • esses são os únicos dois comandos indispensáveis. Para os demais comandos, o servidor web utilizará valores padrão;
  • linha 6, a linha vazia que deve encerrar os comandos do cliente;
  • abaixo da linha 6, vem a resposta do servidor web;
  • linhas 7-12: os cabeçalhos HTTP da resposta do servidor;
  • linha 13: a linha vazia que sinaliza o fim dos cabeçalhos HTTP;
  • linhas 14 a 82: o documento HTML solicitado na linha 4;

Carregamos o arquivo de logs [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>]
  • linhas 11-79: o documento HTML foi recebido. No exemplo anterior, o Firefox havia recebido o mesmo;

Agora temos as bases para programar um cliente TCP que solicitaria um URL.

21.4.3. Exemplo 3

Image

O script [http/01/main.py] é um cliente HTTP configurado pelo arquivo [config.py]. O conteúdo deste arquivo é o seguinte:


def configure():
    # URLs a ser consultado
    urls = [
        # site: nome do site ao qual se conectar
        # porta: porta do serviço web
        # GET: URL solicitado
        # headers: cabeçalhos HTTP a serem enviados na solicitação
        # endOfLine: marcador de fim de linha nos cabeçalhos HTTP enviados
        # encoding: codificação da resposta do servidor
        # timeout: tempo máximo de espera por uma resposta do servidor
        {
            "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
        }
    ]
    # a configuração é devolvida
    return {
        "urls": urls
    }
  • o conteúdo do arquivo é uma lista de URL, sendo que cada elemento da lista é um dicionário. Esse dicionário indica como se conectar ao site designado pela chave [site];
  • linhas 4-10: o significado das chaves de cada dicionário;

O script [http/01/main.py] é o seguinte:


# importações
import codecs
import socket


# -----------------------------------------------------------------------
def get_url(url: dict, suivi: bool = True):
    # lê a URL URL do site url["GET"] e a armazena no arquivo url[site].html
    # a comunicação cliente/servidor ocorre de acordo com o protocolo HTTP indicado no dicionário [url]
    # permite que as exceções sejam propagadas

    sock = None
    html = None
    try:
        # conexão com [site] na porta 80 com um tempo limite
        site = url['site']
        sock = socket.create_connection((site, int(url['port'])), float(url['timeout']))

        # a conexão representa um fluxo de comunicação bidirecional
        # entre o cliente (este programa) e o servidor web acessado
        # esse canal é utilizado para a troca de comandos e informações
        # o protocolo de comunicação é HTTP

        # criação do arquivo site.html — os caracteres indesejáveis são substituídos por um nome de arquivo
        site2 = site.replace("/", "_")
        site2 = site2.replace(".", "_")
        html_filename = f'{site2}.html'
        html = codecs.open(f"output/{html_filename}", "w", "utf-8")

        # o cliente iniciará a comunicação HTTP com o servidor
        if suivi:
            print(f"Client : début de la communication avec le serveur [{site}]")

        # dependendo do servidor, as linhas do cliente devem terminar com \n ou \r\n
        end_of_line = url["endOfLine"]
        # o cliente envia o comando GET para solicitar a configuração URL ["GET"]
        # sintaxe GET URL HTTP/1.1
        commande = f"GET {url['GET']} HTTP/1.1{end_of_line}"
        # rastreamento?
        if suivi:
            print(f"--> {commande}", end='')
        # envia-se o comando ao servidor
        sock.send(bytearray(commande, 'utf-8'))
        # emissão dos cabeçalhos HTTP
        for verb, value in url['headers'].items():
            # construindo o comando a ser enviado
            commande = f"{verb}: {value}{end_of_line}"
            # próximo passo?
            if suivi:
                print(f"--> {commande}", end='')
            # envia-se o comando ao servidor
            sock.send(bytearray(commande, 'utf-8'))
        # envia-se o cabeçalho HTTP [Connection: close] para solicitar ao servidor web
        # que feche a conexão assim que enviar o documento solicitado
        sock.send(bytearray(f"Connection: close{end_of_line}", 'utf-8'))
        # os cabeçalhos (headers) do protocolo HTTP devem terminar com uma linha em branco
        sock.send(bytearray(end_of_line, 'utf-8'))
        #
        # o servidor agora responderá pelo canal sock. Ele enviará todos
        # seus dados e, em seguida, fechará o canal. O cliente, portanto, lê tudo o que chega pelo sock
        # até o fechamento do canal
        #
        # primeiro, lê-se os cabeçalhos HTTP enviados pelo servidor
        # elas também terminam com uma linha vazia
        if suivi:
            print(f"Réponse du serveur [{site}]")

        # leitura do soquete como se fosse um arquivo de texto
        encoding = f"{url['encoding']}" if url['encoding'] else None
        if encoding:
            file = sock.makefile(encoding=encoding)
        else:
            file = sock.makefile()
        # esse arquivo é processado linha por linha
        fini = False
        while not fini:
            # leitura da linha atual
            ligne = file.readline().strip()
            # há uma linha que não esteja vazia?
            if ligne:
                if suivi:
                    # exibimos o cabeçalho HTTP
                    print(f"<-- {ligne}")
            else:
                # essa era a linha vazia — os cabeçalhos HTTP foram concluídos
                fini = True
        # está-se lendo o documento HTML, que virá após a linha vazia
        # leitura da linha atual
        ligne = file.readline()
        while ligne:
            # registro no arquivo de logs
            html.write(str(ligne))
            # linha seguinte
            ligne = file.readline()
            # o ciclo termina quando o servidor encerra a conexão
    finally:
        # o cliente encerra a conexão
        if sock:
            sock.close()
        # fechamento do arquivo HTML
        if html:
            html.close()


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

# configuramos o aplicativo
import config
config = config.configure()

# obtendo os URL do arquivo de configuração
for url in config['urls']:
    print("-------------------------")
    print(url['site'])
    print("-------------------------")
    try:
        # leitura de URL do site [site]
        get_url(url)
    except BaseException as erreur:
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
# fim
print("Terminé...")

Comentários sobre o código:

  • linhas 108-109: o dicionário [config] do módulo [config.py] é recuperado;
  • linhas 111-122: esse dicionário é utilizado;
  • linha 118, 7: a função [get_url(url)] solicita um documento do site url[site] e o armazena no arquivo de texto url[site].HTML. Por padrão, as trocas cliente/servidor são registradas no console (monitoramento=True);
  • tudo é feito em um [try / finally] (linhas 14-96). Não há cláusula [except]. As exceções serão repassadas ao código chamador, e é este que as interrompe e as exibe (linhas 119-120);
  • linhas 16-17: abertura de uma conexão com o servidor web. A função [socket.create_connection] aceita três parâmetros:
    • [param1]: é o nome do servidor na Internet que se deseja acessar;
    • [param2]: é o número da porta do serviço ao qual se deseja se conectar;
    • [param3]: [socket.create_connection] retorna um socket e [param3], se estiver presente, indica o tempo limite do socket criado. O tempo limite é o tempo máximo de espera do socket enquanto aguarda uma resposta da máquina remota;
  • linhas 27-28: criação do arquivo [site.html], no qual será armazenado o documento HTML recebido;
  • linhas 34-43: o primeiro comando do cliente deve ser o comando [GET URL HTTP/1.1];
  • linha 43: a função [sock.send] permite que o cliente envie dados ao servidor. Aqui, a linha de texto enviada tem o seguinte significado: “Quero (GET) a página [URL] do site ao qual estou conectado. Estou trabalhando com o protocolo HTTP, versão 1.1";
  • linha 43: a instrução [sock.send(bytearray(commande, 'utf-8'))] envia uma matriz de bytes (bytearray). Essa matriz é obtida pela conversão da string [commande] em uma sequência de bytes codificados em UTF-8;
  • linhas 44-52: enviam-se as demais linhas do protocolo HTTP [Host, User-Agent, Accept, Accept-Language…]. A ordem delas não importa;
  • linhas 53-55: envia-se o cabeçalho HTTP [Connection: close] para solicitar ao servidor que feche sua conexão assim que tiver enviado o documento solicitado. Por padrão, ele não faz isso. Portanto, é preciso solicitar isso explicitamente. A vantagem é que esse encerramento será detectado pelo cliente, e é assim que ele saberá que recebeu todo o documento solicitado;
  • linhas 56-57: envia-se uma linha vazia ao servidor para indicar que o cliente concluiu o envio de seus cabeçalhos HTTP e que agora aguarda o documento solicitado;
  • linhas 68-86: o servidor enviará, em primeiro lugar, uma série de cabeçalhos HTTP que fornecerão diversas informações sobre o documento solicitado. Esses cabeçalhos terminam com uma linha vazia;
  • linhas 69-73: para poder ler a resposta do servidor, linha por linha, utiliza-se o método [sock.makefile(encoding=encoding)]. O parâmetro opcional [encoding] especifica a codificação do texto esperado. Após essa operação, o fluxo de linhas enviadas pelo servidor poderá ser lido como um arquivo de texto comum;
  • linha 78: lemos uma linha enviada pelo servidor com o método [readline]. Removemos os espaços (espaços em branco, caractere de fim de linha) do início e do fim da linha;
  • linhas 81-83: se a linha não estiver vazia e o acompanhamento tiver sido solicitado, a linha recebida é exibida no console;
  • linhas 84-86: se tiver sido recuperada a linha vazia que marca o fim dos cabeçalhos HTTP enviados pelo servidor, então o loop da linha 76 é interrompido;
  • linhas 90-95: as linhas de texto da resposta do servidor podem ser lidas linha por linha com um loop while e gravadas no arquivo de texto [html]. Quando o servidor web enviar toda a página solicitada, ele encerra sua conexão com o cliente. Do lado do cliente, isso será detectado como um fim de arquivo e sairemos do loop das linhas 90-95;
  • linhas 96-102: haja ou não erro, liberamos todos os recursos utilizados pelo código;

Resultados:

O console exibe os seguintes logs:


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/cursos-tutoriais-de-programação
<-- 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/cursos-e-tutoriais-de-programação/
<-- Set-Cookie: SERVERID68971=2620178|XwH/h|XwH/h; path=/
<-- X-IPLB-Instance: 17095
Terminé...

Process finished with exit code 0

Comentários

  • linha 12: foi encontrado o URL [http://localhost/] (código 200);
  • linha 29: o URL [http://sergetahe.com/] não foi encontrado (código 302). O código 302 significa que a página solicitada mudou de URL. A nova URL é indicada pelo cabeçalho HTTP [Location] da linha 36;
  • linha 49: a solicitação enviada ao servidor [http://tahe.developpez.com] está incorreta (código 400);
  • linha 65: o URL [http://www.sergetahe.com/] não foi encontrado (código 301). O código 301 significa que a página solicitada mudou de URL de forma definitiva. A nova URL é indicada pelo cabeçalho HTTP [Location] da linha 71;

De modo geral, os códigos 3xx, 4xx e 5xx de um servidor HTTP são códigos de erro.

A execução gerou os seguintes arquivos:

Image

O arquivo [output/localhost.HTML] recebido é o seguinte:


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

De fato, obtivemos o mesmo documento que com o navegador Firefox.

O documento [output/sergetahe_com.html] recebido é o seguinte:

Image

A maioria dos servidores HTTP envia suas respostas às solicitações em partes. Cada parte enviada é precedida por uma linha que indica o número de bytes da parte seguinte. Isso permite que o cliente leia exatamente esse número de bytes para obter a parte. Aqui, o 0 indica que a parte seguinte tem zero byte. Vale lembrar que o servidor havia indicado que o documento [http://sergetahe.com/] havia substituído o URL. Portanto, ele não enviou nenhum documento.

O documento [output/tahe_developpez_com.html] é o seguinte:


<!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>
  • linhas 1-12: o servidor enviou um documento HTML, apesar de a solicitação estar incorreta (linha 49 dos resultados). O documento HTML permite que o servidor especifique a causa do erro. Esta é indicada nas linhas 6 e 7:
    • linha 7: nosso cliente utilizou o protocolo HTTP;
    • linha 8: o servidor opera com o protocolo HTTPS (S = seguro) e não aceita o protocolo HTTP;

O documento [output/www_sergetahe_com.html] é o seguinte:


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

Também aqui ocorreu um erro (linha 3). No entanto, o servidor se encarrega de enviar um documento HTML detalhando esse erro (linhas 1-7).

21.4.4. Exemplo 4

Os exemplos anteriores nos mostraram que nosso cliente HTTP era insuficiente. Apresentaremos agora uma ferramenta chamada [curl] que permite recuperar documentos da web, lidando com as dificuldades mencionadas: protocolo HTTPS, documento enviado em partes, redirecionamentos… A ferramenta [curl] foi instalada com o Laragon:

Image

Vamos abrir um terminal PyCharm [1]:

Image

  • em [1], o acesso aos terminais de PyCharm;
  • em [2-3], os terminais já ativos;
  • em [4], a pasta em que você está. No que se segue, isso não importa;

No terminal, digitamos o seguinte comando:


(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

O fato de o comando [curl –help] ter produzido resultados mostra que o comando [curl] está no PATH do terminal. No Windows, o PATH é o conjunto de pastas exploradas quando o usuário digita um comando executável, neste caso, [curl]. O valor do PATH pode ser conhecido:


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

Na linha 2, as pastas do PATH separadas por ponto-e-vírgula. Nessa lista não aparece nenhuma pasta relacionada ao Laragon. Se investigarmos um pouco, descobrimos que há um [curl] na pasta [c:\windows\system32]. Foi esse que respondeu anteriormente.

Se quisermos usar a ferramenta [curl] fornecida com o Laragon, podemos proceder da seguinte forma:

Image

Image

  • em [2], o terminal Laragon;
  • em [3], esse botão permite criar novos terminais, cada um deles sendo instalado em uma aba da janela acima;
  • em [4], solicita-se o PATH do terminal Laragon;
  • obtém-se algo muito diferente do que havia sido obtido em um terminal PyCharm. Este PATH contém várias pastas criadas durante a instalação do Laragon. A pasta que contém a ferramenta [curl] faz parte delas:

Image

Em seguida, utilize o terminal de sua preferência. Basta saber que, quando quiser usar uma ferramenta fornecida pelo Laragon, é preferível usar o terminal do Laragon.

O comando [curl --help] exibe todas as opções de configuração do [curl]. São várias dezenas delas. Usaremos muito poucas delas. Para solicitar um URL, basta digitar o comando [curl URL]. Esse comando exibirá na console o documento solicitado. Se quisermos, além disso, as trocas de dados HTTP entre o cliente e o servidor, digitaremos [curl --verbose URL]. Por fim, para salvar o documento HTML solicitado em um arquivo, digitaremos [curl --verbose --output fichier URL].

Para evitar sobrecarregar o sistema de arquivos da nossa máquina, vamos mudar para outro local (estou usando aqui um terminal 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                                          
  • na linha 3, vamos para a pasta [c:\temp]. Se essa pasta não existir, você pode criá-la ou escolher outra;
  • na linha 6, criamos uma pasta chamada [curl];
  • na linha 9, seleciona-se essa pasta;
  • na linha 12, listamos seu conteúdo. Ele está vazio (linha 20);

Certifique-se de que o servidor Apache do Laragon esteja em execução e, com [curl], solicite os arquivos URL e [http://localhost/] usando o comando [curl –verbose –output localhost.html http://localhost/]. Obtêm-se os seguintes resultados:


λ 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 para o host localhost, mantido intacto
  • linhas 10-13: linhas enviadas por [curl] ao servidor [localhost]. Reconhece-se o protocolo HTTP;
  • linhas 14-20: linhas enviadas em resposta pelo servidor;
  • linha 14: indica que o documento solicitado foi recebido corretamente;

O arquivo [localhost.html] contém o documento solicitado. Você pode verificar isso abrindo o arquivo em um editor de texto.

Agora, vamos solicitar o URL [https://tahe.developpez.com:443/]. Para obter esse URL, o cliente HTTP precisa saber se comunicar com o HTTPS. Esse é o caso do cliente [curl].

Os resultados da console são os seguintes:


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 para o host tahe.developpez.com permaneceu intacto
  • linhas 10-39: as trocas entre cliente e servidor para proteger a conexão: esta será criptografada;
  • linhas 41-44: os cabeçalhos HTTP enviados pelo cliente [curl] ao servidor;
  • linha 52: o documento solicitado foi encontrado;
  • linha 57: o documento é enviado em partes;

[curl] lida corretamente tanto com o protocolo seguro HTTPS quanto com o fato de o documento ser enviado em partes. O documento enviado poderá ser encontrado aqui no arquivo [tahe.developpez.com.html].

Vamos agora solicitar o URL [http://sergetahe.com/cours-tutoriels-de-programmation]. Vimos que, para esse URL, havia um redirecionamento para o URL e o [http://sergetahe.com/cours-tutoriels-de-programmation/] (com um / no final).

Os resultados na console são, então, os seguintes:


C:\Temp\curl
λ curl --verbose --output sergetahe.com.html --location http://sergetahe.com/cursos-tutoriais-de-programação
  % 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/cursos-e-tutoriais-de-programação/
< 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 para hospedar sergetahe.com mantido intacto
* Issue another request to this URL: 'http://sergetahe.com/cursos-tutoriais-de-programação/'
* Found bundle for host sergetahe.com: 0x14385f8 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) com o host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation/ HTTP/1.1
> Host: sergetahe.com
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Date: Sun, 05 Jul 2020 17:44:17 GMT
< Content-Type: text/html; charset=iso-8859-1
< Content-Length: 263
< Server: Apache
< Location: https://sergetahe.com/cursos-e-tutoriais-de-programação/
< 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 para o host sergetahe.com, mantido intacto
* Issue another request to this URL: 'https://sergetahe.com/cursos-e-tutoriais-de-programação/'
*   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/cursos-e-tutoriais-de-programação/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/cursos-e-tutoriais-de-programação/>; 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 para hospedar sergetahe.com, mantido intacto
  • linha 2: utiliza-se a opção [--location] para indicar que se deseja seguir os redirecionamentos enviados pelo servidor;
  • linha 13: o servidor indica que o documento solicitado mudou para URL;
  • linha 18: ele indica o novo URL do documento solicitado;
  • linha 31: [curl] envia uma nova solicitação, desta vez para o novo URL;
  • linha 36: o servidor responde novamente que o URL foi alterado;
  • linha 41: o novo URL é exatamente o mesmo que foi redirecionado, com uma única diferença: o protocolo mudou. Passou a ser HTTPS (linha 41), enquanto antes era http (linha 31);
  • linha 49: uma nova solicitação é enviada para o novo URL. Este está criptografado. Assim, todo um diálogo de configuração de segurança é iniciado, linhas 53-91;
  • linha 92: o novo URL é solicitado, desta vez com o protocolo HTTP/2;
  • linha 100: o documento foi encontrado;

O documento solicitado será encontrado no arquivo [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. Exemplo 5

O Python possui um módulo chamado [pyccurl] que permite utilizar os recursos da ferramenta [curl] em um programa Python. Instalamos esse módulo:

Image

Vamos escrever um novo script [http/02/main.py]:

Image

O arquivo [http/02/config] é o seguinte:


def configure():
    # lista de URL a serem consultados
    urls = [
        # site: servidor ao qual se conectar
        # timeout: tempo máximo de espera por uma resposta do servidor
        # alvo: URL a ser solicitada
        # codificação: codificação da resposta do servidor
        {
            "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"
        }
    ]
    # aqui está a configuração
    return {
        ''urls': URLs
    }

O arquivo contém uma lista de dicionários, sendo que cada um deles possui a seguinte estrutura:

  • site: o nome de um servidor web;
  • encoding: o tipo de codificação do documento esperado;
  • timeout: tempo máximo de espera pela resposta do servidor, expresso em milissegundos. Passado esse tempo, o cliente se desconectará;
  • url: URL do documento solicitado;

O código do script [http/02/main.py] é o seguinte:


# importações
import codecs
from io import BytesIO

import pycurl


# -----------------------------------------------------------------------
def get_url(url: dict, suivi=True):
    # lê a URL URL e a armazena no arquivo output/url['site'].html
    # se [suivi=True], então há um acompanhamento na console da troca cliente/servidor
    # url[timeout] é o tempo limite das chamadas do cliente;
    # a URL [encoding] é a codificação do documento solicitado

    # recupera-se os dados de configuração
    server = url['site']
    timeout = url['timeout']
    target = url['target']
    encoding = url['encoding']
    # acompanhamento
    print(f"Client : début de la communication avec le serveur [{server}]")

    # permite que as exceções sejam propagadas
    html = None
    curl = None
    try:
        # Inicialização de uma sessão cURL
        curl = pycurl.Curl()
        # fluxo binário
        flux = BytesIO()
        # opções do curl
        options = {
            # URL
            curl.URL: target,
            # WRITEDATA: onde os dados recebidos serão armazenados
            curl.WRITEDATA: flux,
            # modo detalhado
            curl.VERBOSE: suivi,
            # nova conexão — sem cache
            curl.FRESH_CONNECT: True,
            # tempo limite da solicitação (em segundos)
            curl.TIMEOUT: timeout,
            curl.CONNECTTIMEOUT: timeout,
            # não verificar a validade dos certificados SSL
            curl.SSL_VERIFYPEER: False,
            # seguir redirecionamentos
            curl.FOLLOWLOCATION: True
        }
        # configuração do curl
        for option, value in options.items():
            curl.setopt(option, value)
        # Execução da solicitação CURL com essas configurações
        curl.perform()
        # criação do arquivo server.html — substituímos os caracteres indesejados por um nome de arquivo
        server2 = server.replace("/", "_")
        server2 = server2.replace(".", "_")
        html_filename = f'{server2}.html'
        html = codecs.open(f"output/{html_filename}", "w", encoding)
        # Gravação do documento recebido no arquivo HTML
        html.write(flux.getvalue().decode(encoding))
    finally:
        # liberação dos recursos
        if curl:
            curl.close()
        if html:
            html.close()


# -------------------principal
# configuração do aplicativo
import config
config = config.configure()

# obter os URL do arquivo de configuração
for url in config['urls']:
    print("-------------------------")
    print(url['site'])
    print("-------------------------")
    try:
        # lendo URL do site [site]
        get_url(url)
    # exceto BaseException como erro:
    #     print(f"Ocorreu o seguinte erro: {erro}")
    finally:
        pass
# fim
print("Terminé...")

Comentários

  • linha 5: importamos o módulo [pycurl];
  • linha 3: importa-se a classe [BytesIO], que nos permitirá armazenar os dados recebidos do servidor em um fluxo binário;
  • linhas 70-72: recuperamos a configuração do aplicativo;
  • linhas 75-85: percorremos a lista de URL encontrada na configuração;
  • linha 81: para cada um dos URL, é chamada a função [get_url], que fará o download da URL URL com um tempo limite de [‘target’];
  • linha 9: a função [get_url] recebe a configuração do URL a ser consultado;
  • linhas 16-19: a configuração do URL é recuperada em variáveis separadas;
  • linhas 26, 61: todas as operações são realizadas dentro de um bloco try / finally. As exceções não são interceptadas e, portanto, são repassadas ao código chamador, que as intercepta;
  • linha 28: prepara-se uma sessão [curl]. O [pycurl.Curl()] retorna um recurso [curl] que realizará a transação com um servidor;
  • linha 30: instanciação do fluxo binário que armazenará os dados recebidos;
  • linhas 32-48: o dicionário [options] configurará a conexão [curl] com o servidor. Suas funções estão indicadas nos comentários;
  • linhas 49-51: as opções da conexão são transmitidas ao recurso [curl];
  • linha 53: conexão com o URL solicitada com as opções definidas. Devido à opção [curl.WRITEDATA: flux] (linha 36), a função [curl.perform()] armazenará os dados recebidos em [flux];
  • linhas 54-60: cria-se o arquivo HTML, que armazenará o documento HTML recebido;
  • linha 60: o fluxo binário [flux.getvalue()] será armazenado como uma sequência de caracteres no arquivo HTML. A codificação dessa sequência é especificada no método [decode(encoding)]. Portanto, é necessário conhecer a codificação do documento enviado pelo servidor. Se houver um erro, a operação de decodificação do fluxo binário falhará. A codificação é especificada no arquivo de configuração do URL (linha 12, por exemplo). Seria possível gerenciar essa informação dinamicamente, pois o servidor a envia nos cabeçalhos HTTP. Isso teria sido preferível. Para manter o código simples, não o fizemos. Para saber o tipo de codificação do documento, basta solicitar o URL desejado com um navegador e observar os cabeçalhos HTTP enviados por ele no modo de depuração do navegador (F12) ou mesmo o próprio documento, pois este também especifica a codificação:

Image

Image

  • linhas 61-66: os recursos alocados são liberados;

Ao executar o script [main.py], obtêm-se os seguintes resultados no console:


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/cursos-tutoriais-de-programação
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 para manter o sergetahe.com intacto
* Issue another request to this URL: 'http://sergetahe.com/cursos-tutoriais-de-programação'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) com o host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation HTTP/1.1
Host: sergetahe.com
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*

* Mark bundle as not supporting multiuse
< HTTP/1.1 301 Moved Permanently
< Date: Mon, 06 Jul 2020 06:45:52 GMT
< Content-Type: text/html; charset=iso-8859-1
< Content-Length: 262
< Server: Apache
< Location: http://sergetahe.com/cursos-e-tutoriais-de-programação/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 para o host sergetahe.com, mantido intacto
* Issue another request to this URL: 'http://sergetahe.com/cursos-tutoriais-de-programação/'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) com o host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation/ HTTP/1.1
Host: sergetahe.com
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*

* Mark bundle as not supporting multiuse
< HTTP/1.1 301 Moved Permanently
< Date: Mon, 06 Jul 2020 06:45:52 GMT
< Content-Type: text/html; charset=iso-8859-1
< Content-Length: 263
< Server: Apache
< Location: https://sergetahe.com/cursos-e-tutoriais-de-programação/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 para o host sergetahe.com, mantido intacto
* Issue another request to this URL: 'https://sergetahe.com/cursos-e-tutoriais-de-programação/'
*   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/cursos-e-tutoriais-de-programação/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/cursos-e-tutoriais-de-programação/>; rel=shortlink
< vary: Accept-Encoding
< x-iplb-instance: 17080
< set-cookie: SERVERID68971=26218|XwLIp|XwLIp; path=/
< 
* Connection #1 para hospedar sergetahe.com mantido intacto
-------------------------
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 para manter o tahe.developpez.com intacto
-------------------------
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 para hospedar www.polytech-angers.fr mantido intacto
* 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) com o host www.polytech-angers.fr
* Connected to www.polytech-angers.fr (193.49.144.41) port 80 (#0)
> GET /fr/index.html HTTP/1.1
Host: www.polytech-angers.fr
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*

* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Mon, 06 Jul 2020 06:45:54 GMT
< Server: Apache/2.4.29 (Ubuntu)
< Last-Modified: Mon, 06 Jul 2020 04:50:09 GMT
< ETag: "85be-5a9be9bfcf228"
< Accept-Ranges: bytes
< Content-Length: 34238
< Cache-Control: max-age=1
< Expires: Mon, 06 Jul 2020 06:45:55 GMT
< Vary: Accept-Encoding
< Content-Type: text/html; charset=UTF-8
< Content-Language: fr
< 
* Connection #0 para o host www.polytech-angers.fr permaneceu intacto
-------------------------
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 para o host localhost permaneceu intacto
Terminé...

Process finished with exit code 0

Comentários

  • em azul, os comandos HTTP enviados ao servidor;
  • em verde, os dados recebidos em resposta pelo cliente;
  • obtêm-se as mesmas trocas de dados que com a ferramenta [curl];
    • linha 9: é solicitada a URL [http://sergetahe.com/];
    • linha 15: o servidor responde que a página foi movida. Linha 21, o novo URL;
    • linha 32: o URL [http://sergetahe.com/cours-tutoriels-de-programmation] é solicitado;
    • linha 38: o servidor responde que a página foi movida. Linha 43, o novo URL;
    • linha 54: o URL [http://sergetahe.com/cours-tutoriels-de-programmation/] é solicitado;
    • linha 60: o servidor responde que a página foi movida. Linha 65, a nova URL. Ela utiliza o protocolo seguro [HTTPS];
    • linhas 71-75: o protocolo seguro é estabelecido com o servidor;
    • linha 76: o URL [https://sergetahe.com/cours-tutoriels-de-programmation/] é solicitado;
    • linha 82: o documento solicitado foi encontrado;

21.4.6. Conclusão

Nesta seção, conhecemos o protocolo HTTP e escrevemos um script [http/02/main.py] capaz de baixar um URL da web.

21.5. O protocolo SMTP (Simple Mail Transfer Protocol)

21.5.1. Introdução

Image

Neste capítulo:

  • O [Serveur B] será um servidor SMTP local que instalaremos;
  • [Client A] será um cliente SMTP de diversas formas:
    • o cliente [RawTcpClient] para descobrir o protocolo SMTP;
    • um script em Python que simula o protocolo SMTP do cliente [RawTcpClient];
    • um script em Python que utiliza o módulo [smtplib], permitindo enviar todos os tipos de e-mails;

21.5.2. Criação de um endereço [gmail]

Para realizar nossos testes SMTP, precisaremos de um endereço de e-mail para o qual enviar mensagens. Para isso, vamos criar um endereço do Gmail [https://www.google.com/intl/fr/gmail/about/]:

Image

Observação: envie alguns e-mails para o endereço que você criou. Só prossiga quando tiver certeza de que a conta criada está apta a receber e-mails.

21.5.3. Instalação de um servidor SMTP

Para nossos testes, instalaremos o servidor de e-mail [hMailServer], que é ao mesmo tempo um servidor SMTP que permite enviar e-mails, um servidor POP3 (Post Office Protocol) que permite ler os e-mails armazenados no servidor, e um servidor IMAP (Internet Message Access Protocol) que também permite ler os e-mails armazenados no servidor, mas vai além disso. Ele permite, notadamente, gerenciar o armazenamento dos e-mails no servidor.

O servidor de e-mail [hMailServer] está disponível no URL [https://www.hmailserver.com/] (maio de 2019).

Image

Durante a instalação, serão solicitadas algumas informações:

Image

  • em [1-2], selecione tanto o servidor de e-mail quanto as ferramentas para administrá-lo;
  • durante a instalação, será solicitada a senha do administrador: anote-a, pois será necessária;

O [hMailServer] é instalado como um serviço do Windows iniciado automaticamente na inicialização do computador. É preferível escolher a inicialização manual:

  • no [3], digite [services] na área de entrada da barra de status;

Image

  • em [4-8], coloque o serviço no modo [manuel] (6) e inicie-o (7);

Depois de iniciado, o servidor [hMailServer] deve ser configurado. O servidor foi instalado com um programa de administração [hMailServer Administrator]:

Image

  • no [2], na área de entrada da barra de status, digite [hmailserver];
  • em [3], inicie o administrador;
  • em [4], conecte o administrador ao servidor [hMailServer];
  • No [5], digite a senha inserida durante a instalação do [hMailServer];

Se você esqueceu a senha, proceda da seguinte forma:

  • desligue o servidor [hMailServer];
  • abra o arquivo [<hmailserver>/bin/hmailserver.ini], onde <hmailserver> é a pasta de instalação do servidor:

Image

  • no [100], remova a senha da linha [AdministratorPassword]. Com isso, o administrador não terá mais senha. Basta digitar [Entrée] quando for solicitado;

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

Vamos continuar com a configuração do servidor:

Image

  • em [1-2], adicione um domínio (caso ainda não exista);

Image

  • em [3], pode-se inserir praticamente qualquer coisa para os testes que vamos realizar. Na prática, seria necessário inserir o nome de um domínio existente;

Image

Vamos criar uma conta de usuário:

  • clique com o botão direito do mouse em [Accounts] (7) e, em seguida, (8) para adicionar um novo usuário;
  • na aba [General] (9), definimos um usuário [guest] (10) com a senha [guest] (11). Ele terá o endereço de e-mail [guest@localhost] (10);
  • em [12], o usuário [guest] está ativado;

Image

  • em [13-14], o usuário foi criado;

Image

  • em [27], a porta do serviço SMTP;
  • em [28], esse serviço não requer autenticação;
  • em [30], insira a mensagem de boas-vindas que o servidor SMTP enviará aos seus clientes;

Image

Fazemos o mesmo com o servidor POP3:

Image

Repetimos o procedimento para o servidor IMAP:

Image

Indicamos o domínio padrão do servidor [hMailServer] (pode haver vários) :

Image

  • no [37], indique que o domínio padrão do servidor SMTP é aquele que você criou no [38];

Depois de salvar essa configuração, você pode testá-la da seguinte maneira. Abra um terminal PyCharm na pasta de utilitários:

Image

Em seguida, digite o seguinte comando:


(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]
  • linha 1: conectamos-nos à porta 25 da máquina [localhost]. É lá que funciona um servidor SMTP não seguro do servidor [hMailServer];
  • linha 4: recebemos a mensagem de boas-vindas que configuramos na etapa 30 anterior;

O servidor SMTP está, portanto, corretamente instalado. Digite o comando [quit] para encerrar a comunicação com o servidor SMTP na porta 25.

Agora, vamos fazer o mesmo com a porta 587, que é a porta padrão do serviço SMTP de recebimento seguro de e-mails:


(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]
  • linha 4, a resposta do servidor SMTP operando na porta 587;

Agora vamos fazer o mesmo com a porta 110, que é a porta padrão do serviço POP3 de retransmissão de e-mails:


(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]
  • linha 4, recebemos a mensagem de boas-vindas do servidor POP3;

Agora vamos fazer o mesmo com a porta 143, que é a porta padrão do serviço IMAP de repasse de e-mails:


(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]
  • na linha 4, recebemos a mensagem de boas-vindas do servidor IMAP;

21.5.4. Instalação de um cliente de e-mail

Para ler os e-mails que vamos enviar, precisamos de um leitor de e-mails. Para quem não tiver um, mostramos como instalar e configurar o leitor [Thunderbird]:

  • no [1]: baixe o [thunderbird] e instale-o;

Image

  • inicie o servidor de e-mail [hMailServer], caso ainda não esteja em execução;
  • em [2-3]: assim que o Thunderbird for iniciado, vamos criar uma conta de e-mail para o usuário [guest@localhost] no servidor de e-mail [hMailServer];

Image

Image

Image

  • em [7-11]: o servidor POP3, que nos permitirá ler as mensagens do servidor de e-mail [hMailServer], está no endereço [localhost] e opera na porta 110;
  • em [12-16]: o servidor SMTP, que nos permitirá enviar e-mails em nome dos usuários do servidor de e-mail [hMailServer], está localizado no endereço [localhost] e opera na porta 25;
  • [18]: é possível testar a validação dessa configuração;

Image

Image

  • em [26]: como não há criptografia em SSL, o Thunderbird nos avisa que nossa configuração apresenta riscos;
  • em [28]: a conta foi criada;

Para testar a conta criada, vamos, usando o Thunderbird:

  • enviar um e-mail para o usuário [guest@localhost.com] (protocolo SMTP);
  • ler o e-mail recebido por esse usuário (protocolo POP3);

Image

  • em [3]: o remetente;
  • em [4]: o destinatário;
  • em [5]: o assunto do e-mail;
  • em [6]: o conteúdo do e-mail;
  • em [7]: para enviar o e-mail;

Image

  • em [8-9]: a mensagem do usuário [guest@localhost] é recebida;
  • em [10-15]: a mensagem recebida;

Também vamos enviar um e-mail para o usuário [pymailparlexemple@gmail.com]. Vamos criar uma conta para ele no Thunderbird para que possa ler os e-mails que receberá:

Image

Image

  • em [4]: coloque o que quiser;
  • em [5]: o endereço é [pymailparlexemple@gmail.com];
  • em [6]: digite a senha que você atribuiu a esse usuário ao criá-lo;
  • em [7]: confirme essa configuração;

Image

  • em [8]: o Thunderbird recuperou as seguintes informações de seu banco de dados;
  • em [9]: o protocolo de leitura de e-mails não é mais POP3, mas sim IMAP. A principal diferença entre os dois é que o [POP3] transfere o e-mail lido para o computador local onde está instalado o cliente de e-mail e o exclui do servidor remoto, enquanto o [IMAP] mantém o e-mail no servidor remoto;
  • em [10]: identificação do servidor SMTP;
  • em [13]: para obter mais informações sobre os servidores IMAP e SMTP, passa-se para a configuração manual;

Image

  • em [14-17]: as características do servidor IMAP;
  • para [18-21]: as características do servidor SMTP;
  • em [22]: concluímos a configuração;

Image

  • em [23-24]: a nova conta do Thunderbird;
  • em [26]: escrevemos uma nova mensagem;

Image

  • em [27]: o remetente é [pymailparlexemple@gmail.com];
  • em [28]: o destinatário é [pymailparlexemple@gmail.com];
  • em [29-30]: a mensagem;
  • em [31]: para enviá-la;

Image

  • em [32]: verifica-se a caixa de entrada das diferentes contas;

Image

  • em [33-36]: as mensagens recebidas pelo usuário [pymailparlexemple@gmail.com]

Da mesma forma, criamos:

  • uma nova conta do Gmail [pymail2parlexemple@gmail.com];
  • uma nova conta no Thunderbird [pymail2parlexemple@gmail.com] para baixar as mensagens do usuário com o mesmo nome:

Image

Image

Agora temos as ferramentas para explorar os protocolos SMTP, POP3 e IMAP. Começamos pelo protocolo SMTP.

21.5.5. O protocolo SMTP

Image

Vamos explorar o protocolo SMTP analisando os logs do servidor [hMailServer]. Para isso, vamos ativá-los com o comando [hmailServerAdministrator]:

Image

Image

  • no [2], os logs estão ativados;
  • no [3-5]: ativamos os logs para os protocolos SMTP, POP3 e IMAP;
  • no [7], solicita-se a exibição dos logs;
  • no [8], abra o arquivo de logs com qualquer editor de texto;

Image

No exemplo a seguir, o cliente será [Thunderbird] e o servidor será [hMailServer]. No Thunderbird, faça com que o usuário [guest@localhost.com] envie uma mensagem para si mesmo:

Image

Os registros ficam assim:


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

As linhas acima descrevem o diálogo que ocorreu entre o cliente SMTP (o gerenciador de e-mails Thunderbird) e o servidor SMTP (hMailServer). As linhas [SENT] indicam o que o servidor SMTP enviou ao seu cliente. As linhas [RECEIVED] indicam o que o servidor SMTP recebeu do seu cliente.

  • linha 1: logo após a conexão do cliente ao servidor SMTP, este envia a mensagem de boas-vindas ao seu cliente;
  • linha 2: o cliente envia o comando [EHLO] para se identificar. Aqui, ele fornece seu endereço IP [127.0.0.1], que designa a máquina [localhost], ou seja, a máquina que executa o cliente SMTP;
  • linha 3: o servidor envia uma série de respostas [250]. [nl] significa [newline], ou seja, o caractere \n. As respostas têm o formato [250-], exceto a última, que tem o formato [250 ]. É assim que o cliente SMTP sabe que a resposta do servidor SMTP está concluída e que ele pode enviar um comando. A série de comandos [250] tinha como objetivo indicar ao cliente SMTP uma série de comandos que ele poderia utilizar;
  • linha 4: o cliente SMTP envia o comando [MAIL FROM : adresse_mail_expéditeur], que indica quem está enviando a mensagem;
  • linha 5: o servidor SMTP responde com [250 OK], indicando que compreendeu o comando;
  • linha 6: o cliente SMTP envia o comando [RCPT TO : adresse_mail_destinataire] para indicar o endereço do destinatário;
  • linha 7: mais uma vez, o servidor SMTP indica que compreendeu o comando;
  • linha 8: o servidor SMTP envia o comando [DATA]. Isso significa que ele enviará o conteúdo da mensagem;
  • linha 9: o servidor SMTP indica, por meio da resposta [354 OK], que está pronto para receber a mensagem. O texto [send .] indica que o cliente SMTP deve encerrar sua mensagem com uma linha contendo apenas um único ponto;
  • o que não se vê em seguida é que o cliente SMTP envia sua mensagem. Os logs não exibem isso;
  • linha 10: o cliente SMTP enviou o ponto que indica o fim da mensagem. O servidor SMTP responde que colocou a mensagem na fila (queued);
  • o cliente SMTP envia a ele o comando [QUIT] para indicar que vai encerrar a conexão;
  • linha 12: o servidor responde;

Agora que conhecemos o diálogo cliente/servidor do protocolo SMTP, vamos tentar reproduzi-lo com nosso cliente [RawTcpClient]. Usamos um terminal PyCharm:

Image

Vamos analisar um novo exemplo:

  • o cliente A será o cliente genérico TCP, [RawTcpClient];
  • o servidor B será o servidor de e-mails [hMailServer];
  • o cliente A solicitará ao servidor B que distribua um e-mail enviado pelo usuário [guest@localhost.com] para si mesmo;
  • verificaremos se o destinatário recebeu corretamente o e-mail enviado;

Iniciamos o cliente da seguinte maneira:


(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]
  • na linha [1], conectamo-nos à porta 25 da máquina local, onde opera o serviço SMTP de [hMailServer]. O argumento [--quit bye] indica que o usuário encerrará o programa digitando o comando [bye]. Sem esse argumento, o comando para encerrar o programa é [quit]. No entanto, [quit] também é um comando do protocolo SMTP. Portanto, precisamos evitar essa ambiguidade;
  • na linha [2], o cliente está conectado corretamente;
  • na linha [3], o cliente aguarda comandos digitados no teclado;
  • na linha [4], o servidor envia a ele sua mensagem de boas-vindas;

Continuamos o diálogo da seguinte maneira:


(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
  • em [5], o cliente envia o comando [EHLO nom-de-la-machine-client]. O servidor responde com uma sequência de mensagens no formato [250-xx] (6). O código [250] indica que o comando enviado pelo cliente foi bem-sucedido;
  • em [10], o cliente indica o remetente da mensagem, neste caso [guest@localhost.com];
  • em [11], a resposta do servidor;
  • em [12], indica-se o destinatário da mensagem, neste caso, o usuário [guest@localhost.com];
  • em [13], a resposta do servidor;
  • em [14], o comando [DATA] indica ao servidor que o cliente enviará o conteúdo da mensagem;
  • em [15], a resposta do servidor;
  • em [16-22], o cliente deve enviar uma lista de linhas de texto terminada por uma linha contendo apenas um único ponto. A mensagem pode conter linhas [Subject:, From:, To:] (16-18) para definir, respectivamente, o assunto da mensagem, o remetente e o destinatário;
  • em [19], os cabeçalhos anteriores devem ser seguidos por uma linha em branco;
  • em [20-21], o texto da mensagem;
  • em [22], a linha contendo apenas um único ponto, que indica o fim da mensagem;
  • em [23], assim que o servidor recebe a linha contendo apenas um único ponto, ele coloca a mensagem na fila;
  • em [24], o cliente informa ao servidor que concluiu a operação;
  • em [25], verifica-se que o servidor encerrou a conexão que o ligava ao cliente;

Agora, vamos verificar com o Thunderbird se o usuário [guest@localhost.com] recebeu a mensagem:

Image

  • em [1-6], vemos que o usuário [guest@localhost.com] recebeu a mensagem corretamente;

Por fim, nosso cliente [RawTcpClient] conseguiu enviar uma mensagem através do servidor SMTP [localhost]. Agora, vamos usar o mesmo método para enviar uma mensagem para [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
  • linha 1: usamos o servidor SMTP do Gmail, que opera na porta 587;
  • linha 15: ficamos bloqueados porque o servidor SMTP nos pede para iniciar uma conexão segura, o que não sabemos fazer. Ao contrário do exemplo anterior, o servidor [smtp.gmail.com] (linha 1) exige autenticação. Ele aceita como clientes apenas os usuários registrados no domínio [gmail.com]. Essa autenticação é segura e ocorre dentro de uma conexão criptografada.

O primeiro exemplo nos deu as bases para criar um cliente SMTP básico em Python. O segundo nos mostrou que alguns servidores SMTP (a maioria, na verdade) exigem uma autenticação realizada por meio de uma conexão criptografada.

21.5.6. Scripts [smtp/01]: um cliente SMTP básico

Vamos reproduzir em Python o que aprendemos anteriormente sobre o protocolo SMTP.

Image

O arquivo [smtp/01/config] configura o aplicativo da seguinte maneira:


def configure() -> dict:
    return {
        # descrição: descrição do e-mail enviado
        # smtp-server: servidor SMTP
        # smtp-port: porta do servidor SMTP
        # remetente: remetente
        # para: destinatário
        # assunto: assunto do e-mail
        # mensagem: mensagem do e-mail
        "mails": [
            {
                "description": "mail to localhost via localhost",
                "smtp-server": "localhost",
                "smtp-port": "25",
                "from": "guest@localhost.com",
                "to": "guest@localhost.com",
                "subject": "to localhost via localhost",
                # enviando UTF-8
                "content-type": 'text/plain; charset="utf-8"',
                # testando os caracteres acentuados
                "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",
                # estamos enviando UTF-8
                "Content-type": 'text/plain; charset="utf-8"',
                # testando caracteres acentuados
                "message": "aglaë séléné\nva au marché\nacheter des fleurs"
            }
        ]
    }
  • linhas 10-35: uma lista de e-mails a serem enviados. Para cada um deles, especificam-se as seguintes informações:
    • [description]: um texto que descreve o e-mail;
    • [smtp-server]: o servidor SMTP a ser utilizado;
    • [smtp-port]: a porta de serviço;
    • [from]: o remetente do e-mail;
    • [to]: o destinatário do e-mail;
    • [subject]: o assunto do e-mail;
    • [content-type]: a codificação do e-mail;
    • [message]: o corpo do e-mail;

O código [01/main] do cliente SMTP é o seguinte:


# importações
import socket


# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: bool):
    # envia mensagem ao servidor SMTP smtpserver em nome do remetente
    # para o destinatário. Se verbose=True, rastreia as trocas entre cliente e servidor

    # permitimos que os erros do sistema sejam relatados
    connexion = None
    try:
        # nome da máquina local (necessário para o protocolo SMTP)
        client = socket.gethostbyaddr(socket.gethostbyname("localhost"))[0]
        # abertura de uma conexão na porta 25 do smtpServer
        connexion = socket.create_connection((mail["smtp-server"], 25))

        # a conexão representa um fluxo de comunicação bidirecional
        # entre o cliente (este programa) e o servidor SMTP acessado
        # esse canal é utilizado para a troca de comandos e informações

        # após a conexão, o servidor envia uma mensagem de boas-vindas que é lida
        send_command(connexion, "", verbose, True)
        # comando ehlo:
        send_command(connexion, f"EHLO {client}", verbose, True)
        # comando mail from:
        send_command(connexion, f"MAIL FROM: <{mail['from']}>", verbose, True)
        # comando rcpt to:
        send_command(connexion, f"RCPT TO: <{mail['to']}>", verbose, True)
        # comando data
        send_command(connexion, "DATA", verbose, True)
        # preparação da mensagem a ser enviada
        # deve conter as linhas
        # De: remetente
        # Para: destinatário
        # linha em branco
        # Mensagem
        # .
        data = f"{mail['message']}"
        # envio da mensagem
        send_command(connexion, data, verbose, False)
        # envio .
        send_command(connexion, "\r\n.\r\n", verbose, False)
        # comando de saída
        send_command(connexion, "QUIT", verbose, True)
        # fim
    finally:
        # encerramento da conexão
        if connexion:
            connexion.close()


# --------------------------------------------------------------------------
def send_command(connexion: socket, commande: str, verbose: bool, with_rclf: bool):
    # envia comando no canal de conexão
    # modo detalhado se verbose=True
    # se with_rclf=True, adiciona a sequência rclf ao comando

    # dados
    rclf = "\r\n" if with_rclf else ""
    # envio do comando se o comando não estiver vazio
    if commande:
        # permite que os erros do sistema sejam reportados
        #
        # envio do comando
        connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
        # possível resposta
        if verbose:
            affiche(commande, 1)
        # leitura da resposta com menos de 1.000 caracteres
        reponse = str(connexion.recv(1000), 'utf-8')
        # possível resposta
        if verbose:
            affiche(reponse, 2)
        # recuperação do código de erro
        codeErreur = int(reponse[0:3])
        # erro retornado pelo servidor?
        if codeErreur >= 500:
            # lança-se uma exceção com o erro
            raise BaseException(reponse[4:])
        # retorno sem erro


# --------------------------------------------------------------------------
def affiche(echange: str, sens: int):
    # exibe a transação na tela
    # se sens=1, exibe -->troca
    # se sens=2, exibe <-- troca sem os dois últimos caracteres rclf
    if sens == 1:
        print(f"--> [{echange}]")
        return
    elif sens == 2:
        l = len(echange)
        print(f"<-- [{echange[0:l - 2]}]")
        return


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

# cliente SMTP (Protocolo de Transferência SendMail) que permite enviar uma mensagem
# as informações são obtidas de um arquivo de configuração que contém os seguintes dados para cada servidor

# descrição: descrição do e-mail enviado
# smtp-server: servidor SMTP
# smtp-port: porta do servidor SMTP
# remetente: remetente
# para: destinatário
# assunto: assunto do e-mail
# mensagem: mensagem do e-mail


# protocolo de comunicação cliente-servidor SMTP
# -> o cliente se conecta à porta 25 do servidor SMTP
# <- o servidor envia uma mensagem de boas-vindas
# -> o cliente envia o comando EHLO: nome do seu computador
# <- o servidor responde com OK ou não
# -> o cliente envia o comando mail from: <remetente>
# <- o servidor responde com OK ou não
# -> o cliente envia o comando rcpt to: <destinatário>
# <- o servidor responde com OK ou não
# -> o cliente envia o comando data
# <- o servidor responde com OK ou não
# -> o cliente envia todas as linhas de sua mensagem e termina com uma linha contendo apenas o caractere .
# <- o servidor responde com OK ou não
# -> o cliente envia o comando quit
# <- o servidor responde com OK ou não

# as respostas do servidor têm o formato xxx texto, em que xxx é um número de 3 dígitos. Qualquer número xxx >=500
# indica um erro. A resposta pode conter várias linhas, todas começando com xxx-, exceto a última
# no formato xxx(espaço)

# as linhas de texto trocadas devem terminar com os caracteres RC(#13) e LF(#10)

# configuração do aplicativo
import config
config = config.configure()

# os e-mails são processados um por um
for mail in config['mails']:
    try:
        # registros
        print("----------------------------------")
        print(f"Envoi du message [{mail['description']}]")
        # preparação da mensagem a ser enviada
        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']}"
        # envio da mensagem no modo detalhado
        sendmail(mail, True)
        # fim
        print("Message envoyé...")
    except BaseException as erreur:
        # exibe-se o erro
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
    # próximo e-mail

Comentários

  • linhas 134-136: configura-se o aplicativo;
  • linhas 139-151: se processam todos os e-mails encontrados na configuração;
  • linhas 141-143: exibe-se o que será feito;
  • linhas 144-149: define-se a mensagem a ser enviada. A mensagem [message] é precedida pelos cabeçalhos [From, To, Subject, Content-type];
  • linha 151: o envio do e-mail é realizado pela função [sendmail], que aceita dois parâmetros:
    • [mail]: o dicionário contendo as informações necessárias para o envio do e-mail;
    • [verbose]: um valor booleano que indica se as trocas cliente/servidor devem ou não ser registradas no console;
  • linhas 154-156: todas as exceções geradas pela função [sendmail] são interceptadas e exibidas;
  • linha 6: [mail] é o dicionário que descreve o e-mail a ser enviado;
  • linha 14: no protocolo SMTP, o cliente deve enviar seu nome. Aqui, recuperamos o nome da máquina local que servirá como cliente;
  • linha 16: conexão com o servidor SMTP, para o qual a mensagem será enviada;
  • linhas 22-23: se a conexão foi estabelecida com o servidor SMTP, este enviará uma mensagem de boas-vindas que é lida aqui;
  • a função [sendmail] envia, em seguida, os diversos comandos que um cliente SMTP deve enviar:
    • linhas 24-25: o comando EHLO;
    • linhas 26-27: o comando MAIL FROM:;
    • linhas 28-29: o comando RCPT TO: ;
    • linhas 30-31: o comando DATA;
    • linhas 32-41: envio da mensagem (From, To, Subject, Content-type, texto);
    • linhas 42-43: envio do ponto final;
    • linhas 44-457: o comando QUIT, que encerra o diálogo do cliente com o servidor SMTP;
  • a execução de [sendmail] ocorre dentro de um [try / finally], que permite que todas as exceções sejam repassadas ao código chamador. Sabe-se que este interrompe todas elas para exibi-las;
  • linhas 48-50: liberação dos recursos;
  • linha 54: a função [send_command] é responsável por enviar os comandos do cliente ao servidor SMTP. Ela aceita quatro parâmetros:
    • [connexion]: a conexão que liga o cliente ao servidor;
    • [commande]: o comando a ser enviado;
    • [verbose]: se TRUE, então as trocas entre cliente e servidor são registradas no console;
    • [with_rclf]: se TRUE, envia o comando terminado pela sequência \r\n. Isso é necessário para todos os comandos do protocolo SMTP, mas [send_command] também serve para enviar a mensagem. Nesse caso, não se adiciona a sequência \r\n;
  • linha 62: o comando só é enviado se não estiver vazio;
  • linhas 65-66: o comando é enviado ao servidor na forma de uma sequência de bytes UTF-8;
  • linhas 70-71: leitura de todas as linhas da resposta. Supõe-se que ela tenha menos de 1.000 caracteres. A resposta pode conter várias linhas. Cada linha tem o formato XXX-YYY, em que XXX é um código numérico, exceto a última linha da resposta, que tem o formato XXX YYY (ausência do caractere -);
  • linha 76: leitura do código de erro XXX da primeira linha;
  • linhas 78-80: se o código numérico XXX for maior que 500, então o servidor retornou um erro. Nesse caso, é lançada uma exceção;

Resultados

A execução do script gera os seguintes resultados no console:


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
  • linhas 3-30: o uso dos servidores SMTP e [hMailServer] para enviar um e-mail para [guest@localhost] ocorreu sem problemas;
  • linhas 32-46: o uso dos servidores SMTP e [smtp.gmail.com] para enviar um e-mail para [pymailparlexemple@gmail.com] não ocorreu corretamente: na linha 45, o servidor SMTP envia um código de erro 530 com uma mensagem de erro. Esta indica que o cliente SMTP deve primeiro se autenticar por meio de uma conexão segura. Nosso cliente não fez isso e, portanto, foi recusado;

Os resultados no Thunderbird são os seguintes:

Image

Image

O cliente anterior apresenta pelo menos duas deficiências:

  1. não sabe utilizar uma conexão segura caso o servidor a exija;
  1. não consegue anexar arquivos à mensagem;

Vamos resolver a primeira deficiência no script [smtp/02]. Em nosso novo script, utilizaremos o módulo Python [smtplib].

O script [smtp/02/main] utilizará o seguinte arquivo de configuração jSON [smtp/02/config]:


def configure() -> dict:
    return {
        # descrição: descrição do e-mail enviado
        # smtp-server: servidor SMTP
        # smtp-port: porta do servidor SMTP
        # remetente: remetente
        # para: destinatário
        # assunto: assunto do e-mail
        # mensagem: mensagem do e-mail
        "mails": [
            {
                "description": "mail to localhost via localhost avec smtplib",
                "smtp-server": "localhost",
                "smtp-port": "25",
                "from": "guest@localhost.com",
                "to": "guest@localhost.com",
                "subject": "to localhost via localhost avec smtplib",
                # testando caracteres acentuados
                "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",
                # testando caracteres acentuados
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP com autenticação
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlh@1QZ3TG",
            }
        ]
    }

Encontramos os mesmos campos do arquivo [smtp/01/config], com dois campos adicionais quando o servidor SMTP solicita autenticação:

  • linha 31, [user]: o nome do usuário que autentica a conexão;
  • linha 32, [password]: sua senha;

Esses dois campos só estão presentes se o servidor SMTP contatado exigir autenticação. Nesse caso, a autenticação é feita por meio de uma conexão segura.

O código do script [smtp/02/main.py] é o seguinte:


# importações
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate


# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: True):
    # envia mensagem ao servidor SMTP smtpserver em nome do remetente
    # para o destinatário. Se verbose=True, rastreia as trocas entre cliente e servidor

    # utiliza-se a biblioteca smtplib
    # permite que as exceções sejam propagadas
    #
    # o servidor SMTP
    server = smtplib.SMTP(mail["smtp-server"])
    # modo detalhado
    server.set_debuglevel(verbose)
    # conexão segura?
    if "user" in mail:
        # conexão segura
        server.starttls()
        # EHLO comando + autenticação
        server.login(mail["user"], mail["password"])

   # construção de uma mensagem Multipart — é essa mensagem Multipart que será enviada
    msg = MIMEText(mail["message"])
    msg['from'] = mail["from"]
    msg['to'] = mail["to"]
    msg['date'] = formatdate(localtime=True)
    msg['subject'] = mail["subject"]
    # envio da mensagem
    server.send_message(msg)
    # saída
    server.quit()


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

# as informações são obtidas de um arquivo de configuração que contém os seguintes dados para cada servidor

# descrição: descrição do e-mail enviado
# smtp-server: servidor SMTP
# smtp-port: porta do servidor SMTP
# remetente: remetente
# para: destinatário
# assunto: assunto do e-mail
# content-type: codificação do e-mail
# mensagem: mensagem do e-mail


# configuração do aplicativo
import config
config = config.configure()

# processamos os e-mails um por um
for mail in config['mails']:
    try:
        # registros
        print("----------------------------------")
        print(f"Envoi du message [{mail['description']}]")
        # envio da mensagem no modo detalhado
        sendmail(mail, True)
        # fim
        print("Message envoyé...")
    except BaseException as erreur:
        # exibição do erro
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
    # próximo e-mail

Comentários

  • linhas 8-35: apenas a função [sendmail] é utilizada. A partir de agora, ela utilizará o módulo [smtplib] (linha 2);
  • linha 16: conexão com o servidor SMTP;
  • linha 18: se for [verbose=True], as trocas cliente/servidor serão exibidas no console;
  • linhas 20-24: realiza-se a eventual autenticação, caso o servidor SMTP a exija;
  • linha 22: a autenticação é realizada por meio de uma conexão segura;
  • linha 24: autenticação;
  • linhas 26-33: envio da mensagem. O diálogo com o script [smtp/01/main] será então iniciado. Se houver autenticação, ele ocorrerá por meio de uma conexão segura;
  • linha 35: encerra-se o diálogo cliente/servidor;

Antes de executar o script [smtp/02/main], você deve alterar a configuração da conta do Gmail [pymailparlexemple@gmail.com]:

  • faça login na conta do Gmail [pymailparlexemple@gmail.com];
  • altere a seguinte configuração: Image
  • no [2], autorize aplicativos menos seguros a acessar a conta;

Faça o mesmo com a segunda conta do Gmail [pymail2parlexemple@gmail.com].

Resultados

Ao executar o script [smtp/02/main], obtêm-se os seguintes resultados no console:


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
  • linha 40: o cliente [smtplib] inicia o diálogo para estabelecer uma conexão criptografada com o servidor SMTP, o que não havia sido possível no script [smtp/main/01];
  • caso contrário, encontramos os comandos conhecidos do protocolo SMTP;

Se consultarmos a conta do Gmail do usuário [pymail2parlexemple], temos o seguinte:

Image

21.5.8. scripts [smtp/03]: gerenciamento de arquivos anexados

Completamos o script [smtp/02/main] para que o e-mail enviado possa ter arquivos anexados.

Image

O script [smtp/03/main] é configurado pelo seguinte script [smtp/03/config]:


import os


def configure() -> dict:
    # configuração do aplicativo
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        # descrição: descrição do e-mail enviado
        # smtp-server: servidor SMTP
        # smtp-port: porta do servidor SMTP
        # remetente: remetente
        # para: destinatário
        # assunto: assunto do e-mail
        # mensagem: mensagem do e-mail
        "mails": [
            {
                "description": "mail to gmail via gmail avec smtplib",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymail2parlexemple@gmail.com",
                "to": "pymail2parlexemple@gmail.com",
                "subject": "to gmail via gmail avec smtplib",
                # testando caracteres acentuados
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP com autenticação
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # aqui, é preciso inserir caminhos absolutos para os arquivos anexados
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                ]
            }
        ]
    }

O arquivo [smtp/03/config] difere do arquivo [smtp/02/config] utilizado anteriormente apenas pela presença opcional de uma lista [attachments] (linhas 30-32), que indica a lista de arquivos a serem anexados à mensagem a ser enviada.

O script [smtp/03/main] é o seguinte:


# importações
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):
    # envia o e-mail [message] para o servidor SMTP [smtp-server] em nome de [from]
    # para mail[to]. Se verbose=True, rastreia as trocas entre cliente e servidor

    # utiliza-se a biblioteca smtplib
    # permitimos que as exceções sejam propagadas
    #
    # o servidor SMTP
    server = smtplib.SMTP(mail["smtp-server"])
    # modo detalhado
    server.set_debuglevel(verbose)
    # conexão segura?
    if "user" in mail:
        server.starttls()
        server.login(mail["user"], mail["password"])

    # construção de uma mensagem Multipart — essa é a mensagem que será enviada
    # crédito: 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"]
    # anexamos a mensagem de texto no formato MIMEText
    msg.attach(MIMEText(mail["message"]))
    # percorre-se os anexos
    for path in mail["attachments"]:
        # o caminho deve ser absoluto
        # detecta-se o tipo do arquivo anexado
        ctype, encoding = mimetypes.guess_type(path)
        # se não foi possível identificar
        if ctype is None or encoding is not None:
            # Não foi possível fazer uma suposição, ou o arquivo está codificado (compactado), portanto
            # usa-se um tipo genérico “bag-of-bits”.
            ctype = 'application/octet-stream'
        # decompõe-se o tipo em tipo principal/subtipo
        maintype, subtype = ctype.split('/', 1)
        # tratamos os diferentes casos
        if maintype == 'text':
            with open(path) as fp:
                # Observação: devemos lidar com o cálculo do conjunto de caracteres
                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)
        # caso do tipo mensagem / rfc822
        elif maintype == 'message':
            with open(path, 'rb') as fp:
                part = MIMEMessage(email.message_from_bytes(fp.read()))
        else:
            # outros casos
            with open(path, 'rb') as fp:
                part = MIMEBase(maintype, subtype)
                part.set_payload(fp.read())
            # Codificar a carga útil usando Base64
            encoders.encode_base64(part)
        # Definir o parâmetro de nome do arquivo
        basename = os.path.basename(path)
        part.add_header('Content-Disposition', 'attachment', filename=basename)
        # anexamos o arquivo à mensagem a ser enviada
        msg.attach(part)
    # todos os anexos foram adicionados — enviamos a mensagem como uma sequência de caracteres
    server.send_message(msg)


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

..

Comentários

  • linhas 18-32: a função [sendmail] permanece igual ao que era quando não havia anexos;
  • linha 35: o código a seguir foi extraído de uma documentação oficial do Python;
  • linha 36: a mensagem a ser enviada conterá várias partes: texto e arquivos anexados. Isso é chamado de mensagem [Multipart];
  • linhas 37-40: na mensagem [Multipart], encontramos os campos habituais de qualquer e-mail;
  • linha 42: as diferentes partes da mensagem [Multipart] [msg] são anexadas à mensagem pelo método [msg.attach] (linha 81). Os anexos podem ser de qualquer tipo. Eles são caracterizados pelo tipo MIME. O tipo MIME de um texto comum é o tipo [MIMEText];
  • linhas 44-81: vamos anexar à mensagem [msg Multipart] todos os anexos da mensagem a ser enviada (linha 81);
  • linha 44: [path] representa o caminho absoluto do arquivo a ser anexado;
  • linha 47: para determinar o tipo MIME a ser usado para o anexo, utilizaremos a extensão (.docx, .php…) do arquivo a ser anexado. O método [mimetypes.guess_type] realiza essa tarefa. Ele retorna duas informações:
    • [ctype]: o tipo MIME do arquivo;
    • [encoding]: uma informação sobre sua codificação;
  • linhas 49-52: caso não seja possível determinar o tipo MIME do arquivo, ele é considerado um arquivo binário (linha 52);
  • linha 54: o tipo MIME de um arquivo se divide em tipo principal e tipo secundário, por exemplo, [application/pdf]. Esses dois elementos são separados;
  • linhas 56-76: tratam-se diferentes casos de acordo com o valor do tipo principal MIME. Por exemplo, no caso de um arquivo PDF com tipo [application/pdf], serão executadas as linhas 70-76:
    • linhas 56-59: o caso em que o arquivo anexado é um arquivo de texto. Nesse caso, cria-se um elemento do tipo [MIMEText] com conteúdo [fp.read];
    • linhas 60-62: o caso em que o arquivo contém uma imagem. Nesse caso, cria-se um elemento do tipo [MIMEImage] com conteúdo [fp.read];
    • linhas 63-65: o caso em que o arquivo é um arquivo de áudio. Nesse caso, cria-se um elemento do tipo [MIMEAudio] com conteúdo [fp.read];
    • linhas 66-69: o caso em que o arquivo é um e-mail. Nesse caso, cria-se um elemento do tipo [MIMEMessage] (linha 69) com conteúdo [email.message_from_bytes(fp.read())]. Ao contrário dos casos anteriores, em que o conteúdo do elemento MIME era o conteúdo binário do arquivo associado, aqui o conteúdo do elemento MIMEMessage é do tipo [email.message.Message];
    • linhas 70-76: os demais casos. Isso inclui, por exemplo, os arquivos do Word e PDF do nosso exemplo;
  • linha 72: o arquivo a ser anexado é aberto no modo binário (rb=read binary);
  • linha 74: [fp.read] lê o arquivo binário na íntegra;
  • linhas 72-74: a estrutura [with open(…) as file] realiza duas ações:
    • ela abre o arquivo e atribui a ele o descritor [file];
    • garante que, ao sair do [with], haja ou não erro, o descritor [file] seja fechado. Trata-se, portanto, de uma alternativa à estrutura [try file=open(…)/ finally];
  • linha 73: cria-se um novo elemento [part] para ser incorporado à mensagem Multipart. Aqui, utiliza-se a classe [MIMEBase] e passam-se ao construtor os elementos [maintype, subtype] determinados na linha 54;
  • linha 74: o elemento a ser incorporado à mensagem Multipart deve ter um conteúdo. Esse conteúdo pode ser inicializado com o método [set_payload];
  • linhas 75-76: os arquivos anexados devem ser codificados em 7 bits. De fato, historicamente, alguns servidores SMTP suportavam apenas caracteres codificados em 7 bits. Aqui, é utilizada a codificação chamada “Base64”;
  • linha 77: a partir desta linha, o processamento se aplica a todos os tipos MIME que criamos nas linhas 56-76 [MIMEMessage, MIMEImage, MIMEAudio, MIMEBase, MIMEText];
  • linha 79: o elemento a ser adicionado à mensagem Multipart possui um cabeçalho que o descreve. Indica-se aqui que o elemento adicionado corresponde a um arquivo anexado. O nome desse arquivo é o terceiro parâmetro passado ao método [add_header]. O nome desse arquivo é frequentemente usado pelos leitores de e-mail para salvar, com esse nome, o arquivo anexado no sistema de arquivos do leitor. Até o momento, trabalhamos com o nome absoluto do arquivo anexado. Aqui, simplesmente passamos seu nome sem o caminho (linha 78);
  • linha 81: o binário do arquivo é incorporado à mensagem [msg Multipart];
  • linha 83: quando todas as partes da mensagem forem anexadas ao [msg Multipart], este é enviado;

Resultados

Se executarmos o script [smtp/03/main] com o arquivo [smtp/02/config] já apresentado, a conta [pymail2parlexemple@gmail.com] receberá o seguinte:

Image

Podemos ver os arquivos anexados no [4, 9-11].

Vamos mostrar agora um exemplo com um e-mail anexado. Vamos salvar o e-mail recebido no arquivo [3] acima:

Image

Salvamos o e-mail com o nome [mail attaché 1.eml] na pasta [smtp/03/attachments].

Agora, vamos modificar o arquivo [smtp/03/config] da seguinte maneira:


import os


def configure() -> dict:
    # configuração do aplicativo
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        # descrição: descrição do e-mail enviado
        # smtp-server: servidor SMTP
        # smtp-port: porta do servidor SMTP
        # remetente: remetente
        # para: destinatário
        # assunto: assunto do e-mail
        # mensagem: mensagem do e-mail
        "mails": [
            {
                "description": "mail to gmail via gmail avec smtplib",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymail2parlexemple@gmail.com",
                "to": "pymail2parlexemple@gmail.com",
                "subject": "to gmail via gmail avec smtplib",
                # testando caracteres acentuados
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP com autenticação
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # aqui, é preciso inserir caminhos absolutos para os arquivos anexados
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                    f"{script_dir}/attachments/mail attaché 1.eml",
                ]
            }
        ]
    }
  • na linha 33, adicionamos um anexo;

Agora, executamos novamente o script [smtp/03/main]. Isso gera o seguinte resultado na caixa de correio do usuário [pymail2parlexemple@gmail.com]:

Image

  • em [1], o e-mail recebido;
  • em [2]: o texto da mensagem;
  • em [3]: o texto do e-mail anexado;
  • em [4]: o Thunderbird encontrou 5 anexos:
    • [fichier attaché.docx];
    • [fichier attaché.pdf];
    • [mail attaché 1.eml]. Esse anexo é, por sua vez, um e-mail contendo dois anexos:
      • [fichier attaché.docx];
      • [fichier attaché.pdf];

21.6. O protocolo POP3

21.6.1. Introdução

Para ler os e-mails armazenados em um servidor de e-mail, existem dois protocolos:

  • o protocolo POP3 (Post Office Protocol), historicamente o primeiro protocolo, mas pouco utilizado atualmente;
  • o protocolo IMAP (Internet Message Access Protocol), um protocolo mais recente que o POP3 e o mais utilizado atualmente;

Para conhecer o protocolo POP3, utilizaremos a seguinte arquitetura:

Image

  • O [Serveur B] será, conforme o caso:
    • um servidor POP3 local, implementado pelo servidor de e-mail [hMailServer];
    • o servidor [pop.gmail.com], que é o servidor POP3 do gerenciador de e-mails [gmail.com];
  • [Client A] será um cliente POP3 de diversas formas:
    • o cliente [RawTcpClient] para descobrir o protocolo POP3;
    • um script em Python que simula o protocolo POP3 do cliente [RawTcpClient];
    • um script em Python que utiliza módulos Python para gerenciar anexos, bem como o uso de uma conexão criptografada e autenticada quando o servidor POP3 assim o exigir;

21.6.2. Análise do protocolo POP3

Assim como fizemos com o protocolo SMTP, vamos explorar o protocolo POP3 utilizando os logs do servidor de e-mails [hMailServer]. Para isso, é necessário iniciar esse servidor.

Com o Thunderbird, vamos:

  • enviar um e-mail para o usuário [guest@localhost.com];
  • ler a caixa de entrada desse usuário;

Image

Image

No endereço [3-6] acima, a mensagem recebida pelo usuário [guest@localhost.com].

Agora, vamos examinar os logs do servidor [hMailServer]. Para isso, utilizamos a ferramenta de administração [hMailServer Administrator]:

Image

Os logs do POP3 são os seguintes (as últimas linhas no arquivo de logs de hoje):


"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..."
  • linha 1: o servidor POP3 envia uma mensagem de boas-vindas ao cliente (Thunderbird) que acabou de se conectar;
  • linha 2: o cliente envia o comando [CAPA] (capabilities) para solicitar a lista de comandos que pode utilizar;
  • linha 3: o servidor responde que ele pode usar os comandos [USER, UIDL, TOP]. O servidor POP inicia suas respostas com [+OK] ou [-ERR] para indicar se executou com sucesso ou falhou na execução do comando do cliente;
  • linha 4: o cliente envia o comando [USER guest] para indicar que deseja consultar a caixa de correio do usuário [guest];
  • linha 5: o servidor responde com [+OK] e solicita a senha de [guest];
  • linha 6: o cliente envia o comando [PASS password] para enviar a senha do usuário [guest]. Aqui, a senha está em texto simples, pois o servidor POP3 não exigiu uma conexão segura. Veremos que isso será diferente com o servidor POP3 do Gmail;
  • linha 7: o servidor validou o par nome de usuário/senha. Ele indica que está bloqueando a caixa de correio do usuário [guest];
  • linha 8: o cliente envia a ele o comando [STAT], que solicita informações sobre a caixa de correio;
  • linha 9: o servidor responde que há uma mensagem de 612 bytes. De modo geral, ele informa que há N mensagens e fornece o tamanho total dessas mensagens;
  • linha 10: o cliente envia o comando [LIST]. Esse comando solicita a lista de mensagens;
  • linha 11: o servidor envia a lista de mensagens no seguinte formato:
    • uma linha de resumo com o número de mensagens e seu tamanho total;
    • uma linha por mensagem, indicando o número da mensagem e seu tamanho;
  • linha 13: o cliente envia o comando [UIDL], que solicita a lista de mensagens com seus identificadores. De fato, cada mensagem é identificada por um número único no serviço de e-mails;
  • linha 14: a resposta do servidor. Vemos, assim, que a mensagem nº 1 da lista tem o identificador 42;
  • linha 15: o cliente envia o comando [RETR 1], solicitando que a mensagem nº 1 da lista seja transferida para ele;
  • linha 16: o servidor POP3 atende ao pedido;
  • linha 17: o cliente envia o comando [QUIT] para indicar que vai se desconectar do servidor POP3;
  • linha 18: o servidor também encerrará sua conexão com o cliente, mas antes envia a ele uma mensagem de despedida;

Vamos agora reproduzir elementos do diálogo acima usando o cliente [RawTcpClient] executado em uma janela PyCharm:

Image

O diálogo é o seguinte:


(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
  • linha 1: abrimos uma conexão com a porta 110 da máquina [localhost]. É ali que opera o serviço POP3 do [hMailServer];
  • nas linhas 5, 7, 9, 13 e 34, utilizamos os comandos [USER, PASS, LIST, RETR, QUIT];
  • linha 4: a mensagem de boas-vindas do servidor POP3;
  • linha 5: indicamos que queremos acessar a caixa de correio do usuário [guest];
  • linha 7: enviamos a senha do usuário [guest] em texto simples;
  • linha 9: solicita-se a lista de mensagens da caixa de correio;
  • linha 13: solicita-se a mensagem nº 1;
  • linhas 14-33: o servidor POP3 envia a mensagem nº 1;
  • linha 34: encerra-se a sessão;

Aqui está um resumo de alguns comandos comuns aceitos por um servidor POP3:

  • o comando [USER] serve para definir o usuário cuja caixa de e-mail se deseja ler;
  • o comando [PASS] serve para definir a senha;
  • o comando [LIST] solicita a lista de mensagens presentes na caixa de correio do usuário;
  • O comando [RETR] solicita a exibição da mensagem cujo número é passado;
  • o comando [DELE] solicita a exclusão da mensagem cujo número é passado;
  • o comando [QUIT] indica ao servidor que a operação foi concluída;

A resposta do servidor pode assumir várias formas:

  • uma única linha começando com [+OK] para indicar que o comando anterior do cliente foi bem-sucedido;
  • uma única linha começando com [-ERR] para indicar que o comando anterior do cliente falhou;
  • várias linhas, nas quais:
    • a primeira linha começa com [+OK];
    • a última linha é composta por um único ponto;

21.6.3. scripts [pop3/01]: um cliente POP3 básico

Image

Como o protocolo POP3 tem a mesma estrutura que o protocolo SMTP, o script [pop3/01/main.py] é uma adaptação do script [smtp/01/main.py]. Ele terá o seguinte arquivo de configuração [pop3/01/config.py]:


def configure() -> dict:
    # as caixas de correio das quais os e-mails são recuperados
    mailboxes = [
        # servidor: servidor POP3
        # porta: porta do servidor POP3
        # usuário: usuário cujas mensagens se deseja ler
        # senha: a senha dele
        # maxmails: o número máximo de e-mails a serem baixados
        # timeout: tempo máximo de espera por uma resposta do servidor
        # encoding: codificação dos e-mails recebidos
        # delete: se for True, os e-mails serão excluídos da caixa de correio
        # assim que forem baixados localmente

        {
            "server": "localhost",
            "port": "110",
            "user": "guest",
            "password": "guest",
            "maxmails": 10,
            "timeout": 1.0,
            "encoding": "utf-8",
            "delete": False
        }
    ]
    # a configuração é restaurada
    return {
        "mailboxes": mailboxes
    }
  • linhas 3-24: a lista de caixas de correio a serem consultadas. Aqui há apenas uma;
  • linhas 4-12: significados dos elementos do dicionário que definem cada uma das caixas de correio;
  • linha 15: o servidor POP3 consultado é o servidor local [hMailServer];
  • linhas 17-18: deseja-se ler a caixa de correio do usuário [guest@localhost];
  • linha 19: serão lidos no máximo 10 e-mails;
  • linha 20: o cliente terá um tempo de espera de, no máximo, 1 segundo pela resposta do servidor;
  • linha 21: o tipo de codificação das mensagens lidas;
  • linha 22: as mensagens baixadas não serão excluídas;

O script [pop3/01/main.py] é o seguinte:


# importações
import re
import socket


# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
    # lê a caixa de correio descrita pelo dicionário [mailbox]
    # se verbose=True, rastreia as trocas entre cliente e servidor



# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
    # envia comando no canal de conexão
    # modo detalhado se verbose=True
    # se with_rclf=True, adiciona a sequência rclf à troca
    # retorna a primeira linha da resposta



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


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

# cliente POP3 (Post Office Protocol) que permite ler mensagens de uma caixa de correio
# protocolo de comunicação POP3 cliente-servidor
# -> o cliente se conecta à porta 110 do servidor SMTP
# <- o servidor envia uma mensagem de boas-vindas
# -> o cliente envia o comando USER usuário
# <- o servidor responde com OK ou não
# -> o cliente envia o comando PASS mot_de_passe
# <- o servidor responde com OK ou não
# -> o cliente envia o comando LIST
# <- o servidor responde com OK ou não
# -> o cliente envia o comando RETR, com um número diferente para cada e-mail
# <- o servidor responde com OK ou não. Se for OK, envia o conteúdo do e-mail solicitado
# -> o servidor envia todas as linhas do e-mail e termina com uma linha contendo o
# único caractere.
# -> o cliente envia o comando DELE seguido do número para excluir um e-mail
# <- o servidor responde com OK ou não
# # -> o cliente envia o comando QUIT para encerrar a comunicação com o servidor
# <- o servidor responde com OK ou não
# as respostas do servidor têm o formato +OK texto ou -ERR texto
# A resposta pode conter várias linhas. Nesse caso, a última linha é composta por um único ponto
# as linhas de texto trocadas devem terminar com os caracteres RC(#13) e LF(#10)
# 

# Recupera-se a configuração do aplicativo
import config
config = config.configure()

# processa-se as caixas de e-mail uma a uma
for mailbox in config['mailboxes']:
    try:
        # exibição no console
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
        # leitura da caixa de e-mail no modo detalhado
        readmails(mailbox, True)
        # fim
        print("Lecture terminée...")
    except BaseException as erreur:
        # exibe o erro
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass

Comentários

Como já mencionamos, o [pop3/01/main.py] é uma adaptação do script [smtp/01/main.py], sobre o qual já comentamos. Abordaremos apenas as principais diferenças:

  • linha 64: a função [readmails] é responsável por ler os e-mails de uma caixa de correio. As informações para se conectar a essa caixa de correio estão no dicionário [mailbox]. O segundo parâmetro, [True], é o parâmetro [Verbose], que, neste caso, solicita o acompanhamento das trocas entre cliente e servidor;

A função [readmails] é a seguinte:


# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
    # lê os e-mails da caixa de correio descrita pelo dicionário [mailbox]
    # se verbose=True, rastreia as trocas entre cliente e servidor

    # isolam-se os parâmetros da caixa de e-mail
    # presume-se que o dicionário [mailbox] seja válido
    server = mailbox['server']
    port = int(mailbox['port'])
    user = mailbox['user']
    password = mailbox['password']
    maxmails = mailbox['maxmails']
    delete = mailbox['delete']
    timeout = mailbox['timeout']

    # permite que os erros do sistema sejam relatados
    connexion = None
    try:
        # abertura de uma conexão na porta [port] de [server] com um tempo limite de um segundo
        connexion = socket.create_connection((server, port), timeout=timeout)

        # a conexão representa um fluxo de comunicação bidirecional
        # entre o cliente (este programa) e o servidor POP3 acessado
        # esse canal é utilizado para a troca de comandos e informações

        # leitura da mensagem de boas-vindas
        send_command(mailbox, connexion, "", verbose, True)
        # comando USER
        send_command(mailbox, connexion, f"USER {user}", verbose, True)
        # comando PASS
        send_command(mailbox, connexion, f"PASS {password}", verbose, True)
        # comando LIST
        première_ligne = send_command(mailbox, connexion, "LIST", verbose, True)
        # análise da primeira linha para determinar o número de mensagens
        match = re.match(r"^\+OK (\d+)", première_ligne)
        nbmessages = int(match.groups()[0])
        # iteração pelas mensagens
        imessage = 0
        while imessage < nbmessages and imessage < maxmails:
            # comando RETR
            send_command(mailbox, connexion, f"RETR {imessage + 1}", verbose, True)
            # comando DELE
            if delete:
                send_command(mailbox, connexion, f"DELE {imessage + 1}", verbose, True)
            # próxima mensagem
            imessage += 1
        # comando QUIT
        send_command(mailbox, connexion, "QUIT", verbose, True)
        # fim
    finally:
        # encerramento da conexão
        if connexion:
            connexion.close()

Comentários

  • linhas 8-14: recuperam-se as informações de configuração da caixa de correio a ser consultada;
  • linhas 19-20: abertura de uma conexão com o servidor POP3;
  • linhas 26-27: leitura da mensagem de boas-vindas enviada pelo servidor;
  • linhas 28-29: envia-se o comando [USER] para identificar o usuário cujos e-mails se deseja obter;
  • linhas 30-31: envio do comando [PASS] para fornecer a senha desse usuário;
  • linhas 32-33: envia-se o comando [LIST] para saber quantos e-mails há na caixa de correio desse usuário. A função [sendCommand] retorna a primeira linha da resposta do servidor. Nela, o servidor indica quantas mensagens há na caixa de correio;
  • linhas 34-36: recupera-se o número de mensagens na primeira linha da resposta;
  • linhas 39-46: percorre-se cada uma das mensagens. Para cada uma delas, são emitidos dois comandos:
    • RETR i: para recuperar a mensagem nº i (linhas 40-41);
    • DELE i: para excluí-la, caso a configuração exija que as mensagens lidas sejam excluídas do servidor (linhas 43-44);
  • linhas 47-48: envia-se o comando [QUIT] para informar ao servidor que a operação foi concluída;

A função [send_command] é a seguinte:


# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
    # envia comando no canal de conexão
    # modo detalhado se verbose=True
    # se with_rclf=True, adiciona a sequência rclf à troca
    # retorna a primeira linha da resposta

    # marca de fim de linha
    if with_rclf:
        rclf = "\r\n"
    else:
        rclf = ""
    # envia o comando se não estiver vazio
    if commande:
        connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
        # possível eco
        if verbose:
            affiche(commande, 1)
    # leitura do soquete como se fosse um arquivo de texto
    encoding = f"{mailbox['encoding']}" if mailbox['encoding'] else None
    file = connexion.makefile(encoding=encoding)
    # esse arquivo é processado linha por linha
    # leitura da primeira linha
    première_ligne = réponse = file.readline().strip()
    # modo detalhado?
    if verbose:
        affiche(première_ligne, 2)
    # recuperação do código de erro
    code_erreur = réponse[0]
    if code_erreur == "-":
        # ocorreu um erro
        raise BaseException(réponse[5:])
    # caso específico de respostas com várias linhas: LIST, RETR
    cmd = commande.lower()[0:4]
    if cmd == "list" or cmd == "retr":
        # última linha da resposta?
        dernière_ligne = False
        while not dernière_ligne:
            # leitura da próxima linha
            ligne_suivante = file.readline().strip()
            # modo detalhado?
            if verbose:
                affiche(ligne_suivante, 2)
            # última linha?
            dernière_ligne = ligne_suivante == "."
    # concluído — retornando a primeira linha
    return première_ligne

Comentários

  • linhas 13-18: o comando [command] só é enviado ao servidor POP3 se não estiver vazio. Isso é necessário para ler a mensagem de boas-vindas do servidor POP3, que ele envia mesmo que o cliente ainda não tenha enviado nenhum comando;
  • linhas 19-21: lemos o soquete como se fosse um arquivo de texto. Isso nos permitirá usar o método [readline] (linha 24) e, assim, ler a mensagem linha por linha. Utilizamos a chave [encoding] do dicionário [mailbox] para indicar a codificação das linhas que serão lidas;
  • linha 24: lemos a primeira linha da resposta;
  • linhas 28-32: trata-se de um possível erro. Essas linhas são do tipo [-ERR invalid password, -ERR mailbox unknown, -ERR unable to lock mailbox…];
  • linha 32: lança-se uma exceção com a mensagem de erro;
  • linha 35: apenas os comandos [list, retr] podem ter respostas com várias linhas;
  • linhas 36-45: no caso de uma resposta com várias linhas, exibimos todas as linhas recebidas (linhas 42-43) até receber a última linha (linha 45);
  • linha 46: retorna-se a primeira linha lida, pois, no caso do comando [LIST], ela contém o número de mensagens presentes na caixa de correio;

Resultados

Vamos considerar o exemplo anterior. Com o Thunderbird, enviamos a seguinte mensagem ao usuário [guest@localhost] (é necessário que o servidor hMailServer esteja em execução):

Image

Ao executar, obtemos os seguintes resultados:


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
  • linhas 15-31: recuperamos corretamente a mensagem enviada para [guest@localhost].

Temos aqui um cliente POP3 básico, ao qual faltam algumas funcionalidades:

  1. a capacidade de se comunicar com um servidor POP3 seguro;
  2. a capacidade de ler os anexos de uma mensagem;

Vamos implementar essas duas funcionalidades com um novo script, que desta vez será mais complexo.

21.6.4. scripts [pop3/02]: cliente POP3 com os módulos [poplib] e [email]

Vamos escrever um cliente POP3 que permita gerenciar os anexos, bem como a comunicação com servidores seguros. Além disso, salvaremos em arquivos as mensagens e seus anexos.

Vamos utilizar dois módulos Python:

  • [poplib]: que será responsável pelo protocolo POP3;
  • [email]: que reúne vários submódulos que nos permitirão analisar as mensagens recebidas. Cada mensagem é uma sequência de caracteres estruturada na qual é possível encontrar:
    • os cabeçalhos da mensagem [From, To, Subject, Return-Path…];
    • a mensagem em suas versões de texto e, eventualmente, HTML;
    • os anexos;

Image

O script [inet/pop3/02/main] [1] é configurado pelo arquivo [inet/pop3/02/config] [2] e utiliza o módulo [inet/shared/mail_parser] [3].

O arquivo [pop3/02/config] é o seguinte:


import os


def configure() -> dict:
    # configuração do aplicativo
    config = {
        # lista de caixas de correio a serem gerenciadas
        "mailboxes": [
            # servidor: servidor POP3
            # porta: porta do servidor POP3
            # usuário: usuário cujas mensagens se deseja ler
            # senha: a senha dele
            # maxmails: o número máximo de e-mails a serem baixados
            # timeout: tempo máximo de espera por uma resposta do servidor
            # delete: deve ser verdadeiro se for necessário excluir do servidor as mensagens baixadas
            # ssl: deve ser verdadeiro se a leitura dos e-mails for feita por meio de uma conexão segura
            # output: a pasta de armazenamento das mensagens baixadas

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

    # caminhos absolutos das pastas a serem incluídas no syspath
    absolute_dependencies = [
        # pasta local
        f"{script_dir}/../../shared",
   ]

    # configuração do syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # aplicamos a configuração
    return config

O arquivo define a lista de caixas de correio a serem consultadas e configura o Python Path do aplicativo.

Há aqui apenas uma única caixa de correio:

  • linhas 22-23: o usuário cujos e-mails se deseja ler;
  • linhas 20-21: o nome e a porta do servidor POP3 que armazena os e-mails desse usuário;
  • linha 24: o número máximo de e-mails a serem recuperados. De fato, se você testar este script em sua própria caixa de e-mail, provavelmente não vai querer recuperar as centenas de e-mails que estão lá;
  • linha 25: valor booleano que indica se, após a leitura de um e-mail, este deve ser excluído (delete=True);
  • linha 26: o atributo [ssl] definido como True significa que o servidor POP3, definido nas linhas 20-21, utiliza uma conexão criptografada;
  • linha 27: o tempo máximo de espera pelas respostas do servidor, expresso em segundos;
  • linha 28: a pasta na qual os e-mails lidos serão armazenados. Ela será criada caso não exista. Aqui, temos um nome relativo. Na execução, ele será relativo à pasta a partir da qual você executar o script. Com [Pycharm], essa pasta será a do script [pop3/02];

O script [pop3/02/main] é o seguinte:


# importações
import email
import os
import poplib
import shutil


# leitura de uma caixa de e-mail
def readmails(mailbox: dict, verbose: bool):
    # lê a caixa de e-mail descrita pelo dicionário [mailbox]
    # se verbose=True, rastreia as trocas entre cliente e servidor


# main ----------------------------------------------------------------
#  cliente POP3 (Post Office Protocol) que permite ler e-mails

# recupera-se a configuração do aplicativo
import config
config = config.configure()

# processa-se as caixas de e-mail uma a uma
for mailbox in config['mailboxes']:
    try:
        # exibição no console
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
        # leitura da caixa de e-mail no modo detalhado
        readmails(mailbox, True)
        # fim
        print("Lecture terminée...")
    except BaseException as erreur:
        # exibe o erro
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
  • linhas 17-36: a parte [main] do script é semelhante à do script [pop3/01];

A função [readmails] é a seguinte:


# leitura de uma caixa de e-mail
def readmails(mailbox: dict, verbose: bool):
    # lê a caixa de e-mail descrita pelo dicionário [mailbox]
    # se verbose=True, rastreia as trocas entre cliente e servidor

    # importação de mail_parser
    from mail_parser import save_message

    # isolamos os parâmetros da caixa de e-mail
    # supõe-se que o dicionário [mailbox] seja válido
    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']

    # permitimos que os erros do sistema sejam relatados
    pop3 = None
    try:
        # criam-se as pastas de armazenamento caso não existam
        if not os.path.isdir(output):
            os.mkdir(output)
        # usuário
        dir2 = f"{output}/{user}"
        # exclui-se a pasta [dir2], caso exista, e recria-se
        if os.path.isdir(dir2):
            # exclusão
            shutil.rmtree(dir2)
        # criação
        os.mkdir(dir2)
        # abertura de uma conexão na porta [port] de [server]
        if ssl:
            pop3 = poplib.POP3_SSL(server, port, timeout=timeout)
        else:
            pop3 = poplib.POP3(server, port, timeout=timeout)

        # a conexão representa um fluxo de comunicação bidirecional
        # entre o cliente (este programa) e o servidor POP3 acessado
        # esse canal é utilizado para a troca de comandos e informações

        # modo detalhado
        pop3.set_debuglevel(2 if verbose else 0)
        # lendo mensagem de boas-vindas
        pop3.getwelcome(    )
        # comando USER
        réponse = pop3.user(user)
        # comando PASS
        réponse = pop3.pass_(password)
        # comando LIST
        liste = pop3.list()
        # os e-mails estão na lista [1]
        imail = 0
        nb_mails = len(liste[1])
        fini = imail == maxmails or imail == nb_mails
        éléments = liste[1]
        while not fini:
            # elemento atual
            élément = éléments[imail]
            # o elemento é uma lista de bytes que é decodificada como string
            desc = élément.decode()
            # temos uma sequência de caracteres separada por espaços
            # o primeiro elemento é o número da mensagem
            num = desc.split()[0]
            # recuperamos a mensagem
            message = pop3.retr(int(num))
            # as linhas da mensagem estão em message [1]
            str_message = ""
            for ligne in message[1]:
                # cada linha é uma sequência de bytes que é decodificada como uma string
                str_message += f"{ligne.decode()}\r\n"
            # pasta da mensagem
            dir3 = f"{dir2}/message_{num}"
            # se a pasta não existir, ela é criada
            if not os.path.isdir(dir3):
                os.mkdir(dir3)
            # assunto email.message.Message
            save_message(dir3, email.message_from_string(str_message), 0)
            # mais um e-mail
            imail += 1
            # já atingimos o limite máximo?
            fini = imail == maxmails or imail == nb_mails

        # pedido QUIT
        pop3.quit()
    finally:
        # encerramento da conexão
        if pop3:
            pop3.close()

Comentários

  • linhas 6-7: importa-se a função [mail_parser.save_message] utilizada na linha 80;
  • o código da função está encapsulado em um try (linha 22)/finally (linha 88). Assim, todas as exceções são encaminhadas para o código principal, que as interrompe e as exibe;
  • linhas 11-18: recuperam-se as informações de configuração da caixa de correio;
  • linhas 23-33: todas as mensagens serão armazenadas na pasta [output/user], onde [output] e [user] são definidas na configuração. Criam-se, portanto, sucessivamente as pastas [output] e, em seguida, [output/user]. Para criar esta última, primeiro é necessário excluí-la da linha 31. [shutil] é um módulo que deve ser importado. [shutil.rmtree(dir)] exclui a pasta [dir] e todo o seu conteúdo;
  • para todas as operações em arquivos do sistema, utiliza-se o módulo [os], que também deve ser importado;
  • linhas 34-38: abre-se uma conexão com o servidor POP3. Se o servidor for seguro, utiliza-se a classe [poplib.POP3_SSL]; caso contrário, utiliza-se a classe [poplib.POP3]. O atributo [ssl] utilizado na linha 35 provém da configuração da caixa de correio;
  • linha 45: define-se um nível de logs:
    • 0: sem registros;
    • 1: os comandos emitidos pelo cliente POP3 são registrados;
    • 2: registros detalhados. Também é possível ver o que o cliente POP3 recebe;
  • linha 47: após a conexão, o servidor POP3 envia uma mensagem de boas-vindas. Podemos ler essa mensagem;
  • linhas 48-49: comando USER do protocolo POP3;
  • linhas 50-51: comando PASS do protocolo POP3;
  • linhas 52-53: comando LIST do protocolo POP3. A resposta é uma tupla (response, ['mesg_num octets'…], bytes), por exemplo, lista=(b'+OK 3 mensagens (3859 bytes)', [b'1 584', b'2 550', b'3 2725'], 22). Vemos que os dois primeiros elementos da tupla são bytes (prefixo b). lista[1] é uma matriz em que cada elemento é uma sequência de bytes contendo duas informações: o número da mensagem e seu tamanho em bytes;
  • linha 56: a partir do exposto, deduz-se que o número de mensagens na caixa de correio pode ser obtido por meio de [email.message_from_bytes(data2[0][1])];
  • linhas 59-84: é feito um ciclo por cada uma das mensagens. O processo é interrompido quando todas tiverem sido lidas ou quando for atingido o número máximo de e-mails definido na configuração;
  • linha 61: elemento atual da matriz liste[1], ou seja, algo como b'1 584', uma sequência de bytes;
  • linha 63: converte-se a sequência de bytes em uma cadeia de caracteres. Agora temos a cadeia '1 584';
  • linha 66: recuperamos o número da mensagem, neste caso a sequência '1';
  • linha 68: enviamos o comando POP3 RETR num. Recebemos uma resposta do tipo:

[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)]
  • (continuação)
    • message é uma tupla de três elementos;
    • message[1] é uma matriz de linhas. Cada linha é uma sequência de bytes (prefixo b). A mensagem completa é formada por esse conjunto de linhas;
    • [Return-Path, Received, To, Subject, Message-ID, Content-Type, Content-Transfer-Encoding, Content-Language] são os cabeçalhos da mensagem. Cada um fornece informações sobre a mensagem recebida. Essas informações permitirão recuperar o corpo da mensagem (penúltimo elemento da matriz message[1]);
  • linhas 71-73: cria-se a cadeia [strMessage], formada por todas as linhas da mensagem. Agora temos a mensagem na forma de uma cadeia de caracteres. Essa mensagem pode conter outras mensagens, bem como anexos. Pois os anexos são incluídos na forma de uma cadeia de caracteres. Portanto, um ponto a ser lembrado é que um e-mail é, inicialmente, uma sequência de caracteres, e é essa sequência de caracteres que deve ser analisada para extrair os anexos, eventuais outras mensagens encapsuladas e, é claro, o corpo da mensagem, ou seja, o que o remetente escreveu;
  • linhas 74-78: vamos armazenar o corpo da mensagem e os anexos na pasta [dir3];
  • linhas 79-80: vamos delegar a análise da mensagem a uma função [save_message]:
    • o primeiro parâmetro é [dir3], a pasta na qual o conteúdo da mensagem deve ser armazenado;
    • O segundo parâmetro é do tipo [email.message.Message]. Esse objeto possui métodos para recuperar as diferentes partes da mensagem (corpo, anexos), bem como todos os seus cabeçalhos. É necessário importar o módulo [email] para ter acesso a esse objeto. A função [email.message_from_string] permite criar um objeto [email.message.Message] a partir da sequência de caracteres da mensagem;

A função [save_message] faz parte do módulo [mail_parser]:

Image

O módulo [mail_parser] foi importado nas linhas 6-7 da função [readmails];

Na função [mail_parser.py], a função [save_message] é a seguinte:


# importações
import codecs
import email.contentmanager
import email.header
import email.iterators
import email.message
import os


# salvamento de uma mensagem do tipo email.message.Message
# essa função pode ser chamada de forma recursiva
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # saída: pasta de armazenamento das mensagens
    # email_message: a mensagem a ser salva
    # irfc822: número atual na numeração dos e-mails anexados
    #
    # parte da mensagem
    part = email_message
    # os cabeçalhos [From, To, Subject] são encontrados em uma das partes multipart
    # ou em uma parte [text/*], quando não houver uma parte [multipart]
    keys = part.keys()
    # “From” deve constar nos cabeçalhos; caso contrário, a parte não possui os cabeçalhos que estamos procurando
    if "From" in keys:
        # recuperamos alguns cabeçalhos
        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'))}"]
        # salva os cabeçalhos em um arquivo de texto
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # gravação no arquivo
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # tipo do arquivo [part]
    main_type = part.get_content_maintype()

Comentários

  • linha 12: a função recebe no máximo três parâmetros:
  • [output]: a pasta onde a mensagem deve ser salva (2º parâmetro);
  • [email_message]: uma mensagem do tipo [email.message.Message]. Esse tipo é estruturado. Ele contém o texto do e-mail, bem como todos os arquivos anexados, e oferece métodos para recuperar seus diversos elementos;
  • [irfc822]: este parâmetro é usado para numerar os e-mails encapsulados em [email_message];
  • linha 18: o objeto [email_message] é inserido em [part]. O tipo [email.message.Message] contém partes [part] (corpo da mensagem, anexos, e-mails encapsulados) que também têm o tipo [email.message.Message]. Cada parte [part] pode ter subpartes. Assim, o tipo [email.message.Message] é uma árvore de elementos do tipo [email.message.Message]:
    • [part.ismultipart()] é igual a [True] se a parte [part] contiver subpartes. Estas ficam, então, disponíveis por meio de [part.get_payload()];
    • quando [part.ismultipart()] é igual a [False], significa que se chegou a uma folha da árvore da mensagem inicial: pode tratar-se de:
      • do corpo da mensagem na forma de um texto normal;
      • do corpo da mensagem na forma de um texto HTML;
      • um anexo (exceto no caso de uma mensagem encapsulada, para a qual [part.ismultipart()] é igual a [True]);
  • devido à estrutura em árvore do parâmetro [email.message.Message], a função [save_message] será chamada de forma recursiva. A recursividade cessa quando se atingem as folhas da árvore, ou seja, uma parte [part] para a qual [part.ismultipart()] é igual a [False];
  • linha 21: solicitamos as chaves (ou cabeçalhos) da mensagem atualmente em análise (que, devido à recursividade, pode ser uma subparte da mensagem inicial);
  • linhas 23-35: queremos registrar os cabeçalhos:
    • [From]: o remetente da mensagem;
    • [To]: o destinatário da mensagem;
    • [Subject]: o assunto da mensagem;
    • [Return-Path]: o destinatário ao qual devemos responder, caso queiramos responder. De fato, essa informação nem sempre consta no [From];
    • [User-Agent]: o cliente POP3 que se comunica com o servidor POP3;
    • [Date]: data de envio do e-mail;
  • linha 23: apenas uma das partes de uma mensagem contém esses cabeçalhos. Para as outras partes, o código das linhas 23 a 35 será ignorado;
  • linhas 25-30: cria-se uma lista com os seis cabeçalhos;
  • linha 25: analisemos o primeiro cabeçalho:
    • [part.get(key)] permite obter o cabeçalho associado à chave [key];
    • esse cabeçalho pode estar codificado. Se a codificação não for UTF-8, o cabeçalho é decodificado para ser recodificado em UTF-8 por meio da função [decode_header];
    • o primeiro cabeçalho terá o formato [From: pymail2lexemple@gmail.com];
  • linhas 31-35: os cabeçalhos são salvos no arquivo [output/headers.txt];

A função [decode_header] é a seguinte (ainda no arquivo [mail_parser.py]):


# decodificação dos cabeçalhos
def decode_header(header: object) -> str:
    # decodifica-se o cabeçalho
    header = email.header.decode_header(f"{header}")
    # o resultado é um array — aqui ele terá apenas um elemento do tipo (header, encoding)
    # se encoding==None, então header é uma sequência de caracteres
    # caso contrário, é uma lista de bytes codificados por encoding
    header, encoding = header[0]
    if not encoding:
        # se não houver codificação
        return header
    else:
        # se houver codificação, descodifica-se
        return header.decode(encoding)

Comentários

  • linha 4: decodifica-se o cabeçalho:
    • é necessário importar o módulo [email.header];
    • obtém-se uma lista de tuplas [(header1,encoding1) , (header2, encoding2)…];
    • para os cabeçalhos [From, To, Subject, Return-Path, Date], a lista terá apenas um elemento;
    • linha 8: recuperamos o cabeçalho único e sua codificação:
      • se [encoding==None], então [header] é o cabeçalho na forma de uma sequência de caracteres;
      • caso contrário, [header] é uma sequência de bytes que representa o cabeçalho codificado;
  • linhas 10-11: se não houvesse codificação, então o cabeçalho é retornado;
  • linhas 12-14: se houvesse codificação, então se decodifica, em uma sequência de caracteres, a sequência de bytes recuperada e se retorna essa sequência;

Voltemos à função [save_message]:


# salvamento de uma mensagem do tipo email.message.Message
# essa função pode ser chamada de forma recursiva
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # saída: pasta de armazenamento das mensagens
    # email_message: a mensagem a ser salva
    # irfc822: número atual na numeração dos e-mails anexados
    #
    # parte da mensagem
    part = email_message
    # os cabeçalhos [From, To, Subject] são encontrados em uma das partes multipart
    # ou em uma parte [text/*], quando não houver uma parte [multipart]
    keys = part.keys()
    # “From” deve constar nos cabeçalhos; caso contrário, a parte não possui os cabeçalhos que estamos procurando
    if "From" in keys:
        # recuperamos alguns cabeçalhos
        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'))}"]
        # salva os cabeçalhos em um arquivo de texto
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # gravação no arquivo
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # tipo da mensagem [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # se a mensagem for do tipo text/plain
    if type_of_part == "text/plain":
        # mensagem de texto
        save_textmessage(output, part, 0)

    # se a mensagem for do tipo text/html
    elif type_of_part == "text/html":
        # mensagem HTML
        save_textmessage(output, part, 1)

    # se a mensagem for um contêiner de partes
    elif part.is_multipart():
        
    else:
        
    # as demais partes são ignoradas (não text/plain, não text/html, não anexo)
    # retorna o valor atual de irfc822 (numeração dos e-mails anexados armazenados na pasta de saída)
    return irfc822

Comentários

  • linhas 1-26: processamos os cabeçalhos da mensagem inicial;
  • linhas 28-31: as partes de uma mensagem do tipo [email.message.Message] possuem um tipo principal e um subtipo. Nós as recuperamos;
  • linhas 32-35: se a parte processada for do tipo [text/plain], significa que chegamos a uma folha da árvore da mensagem inicial. Esse é o texto que o remetente escreveu em sua mensagem;
  • linha 35: esse texto é gravado em um arquivo:
    • o primeiro parâmetro, [output], é a pasta na qual o texto deve ser salvo;
    • o segundo parâmetro é a parte da mensagem que contém o texto a ser salvo;
    • o terceiro parâmetro tem valor 0 para salvar um texto normal e 1 para um texto HTML;
  • linhas 37-40: se a parte for do tipo [text/html], então também chegamos a uma folha da árvore da mensagem inicial. Esse é o texto que o remetente escreveu em sua mensagem, desta vez no formato HTML. Nem todos os gerenciadores de e-mail oferecem esse formato;

A função [save_textmessage] é a seguinte:


# salvamento de uma mensagem de texto
def save_textmessage(output: str, part: email.message.Message, type_of_text: int):
    # cabeçalhos
    headers = []
    # conjunto de caracteres da mensagem
    charset = part.get_content_charset()
    if charset is not None:
        charset = part.get_content_charset().lower()
        headers.append(f"Charset: {charset}")
    # modo de codificação do conteúdo
    content_transfer_encoding = part.get("Content-Transfer-Encoding")
    if content_transfer_encoding is not None:
        headers.append(f"Transfer-Content-Encoding: {content_transfer_encoding}")
    # o modo de 8 bits apresentou problemas
    if content_transfer_encoding == "8bit":
        # recuperamos a mensagem do e-mail
        msg = part.get_payload()
    else:
        # recuperamos a mensagem do e-mail
        msg = email.contentmanager.raw_data_manager.get_content(part)
    # de acordo com os tipos de texto
    filename = None
    if type_of_text == 0:
        # salvamento dos cabeçalhos
        with codecs.open(f"{output}/headers.txt", "a", "utf-8") as file:
            # gravação no arquivo
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")
        # arquivo de texto para o conteúdo
        filename = f"{output}/mail.txt"
    elif type_of_text == 1:
        # arquivo HTML para o conteúdo
        filename = f"{output}/mail.html"
    # salvar a mensagem
    with codecs.open(filename, "w", "utf-8") as file:
        # gravação em arquivo
        file.write(msg)

Comentários

  • assim como os cabeçalhos, o texto da mensagem pode estar codificado. Podem existir duas codificações:
    • a codificação inicial do texto (utf-8, iso-8859-1…). Essa é a codificação usada pelo gerenciador de e-mail que enviou a mensagem. Ela é identificada pelo cabeçalho [Content-Type] da mensagem recebida;
    • uma segunda codificação pela qual o texto anterior pode ter passado para ser enviado. Ela é identificada pelo cabeçalho [Transfer-Content-Encoding] da mensagem recebida;
  • linha 6: a codificação inicial do texto;
  • linha 11: a segunda codificação pela qual o texto passou para sua transferência ao destinatário;
  • linhas 9, 13: essas duas informações são inseridas na lista [headers]. Elas serão adicionadas às informações do arquivo [headers.txt], que registra certos cabeçalhos da mensagem;
  • linha 20: o [email.contentmanager.raw_data_manager.get_content] permite obter a mensagem com sua codificação inicial 1. A codificação 2 foi removida. Apenas o objeto [email.contentmanager.raw_data_manager] gerencia apenas dois tipos de [Transfer-Content-Encoding]:
    • [quoted-printable];
    • [base64];

Ele ignora os demais. No entanto, o Thunderbird, por exemplo, utiliza o [Transfer-Content-Encoding] denominado “8bit”. Essa codificação é ignorada e as mensagens com caracteres acentuados ficam distorcidas. A mensagem pode então ser obtida pelo método [part.get_payload()] (linhas 15-17);

  • linha 21: quando chegamos a este ponto, temos a mensagem livre da codificação de transferência, ou seja, a mensagem tal como foi escrita pelo remetente;
  • linhas 22-37: estamos no caso em que é necessário salvar uma mensagem de texto;
    • linhas 24-28: salvamos os dois cabeçalhos criados nas linhas 9 e 13 no arquivo [headers.txt]. Esse arquivo já existe e contém cabeçalhos. Por isso, utiliza-se o modo “a” (linha 25) para abrir esse arquivo. “a” significa “append” (acrescentar) e os novos cabeçalhos são adicionados (no final do arquivo) ao conteúdo existente do arquivo [headers.txt];
    • linha 30: o nome do arquivo no qual salvar a mensagem de texto;
    • linha 33: o nome do arquivo no qual salvar a mensagem HTML;
    • linhas 34-37: o texto UTF-8 é salvo em um arquivo;

Voltemos à função [save_message]:


# salvamento de uma mensagem do tipo email.message.Message
# essa função pode ser chamada de forma recursiva
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # saída: pasta de armazenamento das mensagens
    # email_message: a mensagem a ser salva
    # irfc822: número atual na numeração dos e-mails anexados
    #
    # parte da mensagem
    part = email_message
    # os cabeçalhos [From, To, Subject] são encontrados em uma das partes multipart
    # ou em uma parte [text/*], quando não houver uma parte [multipart]
    keys = part.keys()
    # “From” deve constar nos cabeçalhos; caso contrário, a parte não possui os cabeçalhos que estamos procurando
    if "From" in keys:
        # recuperamos alguns cabeçalhos
        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'))}"]
        # salva os cabeçalhos em um arquivo de texto
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # gravação no arquivo
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # tipo da mensagem [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # se a mensagem for do tipo text/plain
    if type_of_part == "text/plain":
        # mensagem de texto
        save_textmessage(output, part, 0)

    # se a mensagem for do tipo text/html
    elif type_of_part == "text/html":
        # mensagem HTML
        save_textmessage(output, part, 1)

    # se a mensagem for um contêiner de partes
    elif part.is_multipart():
        # caso específico de e-mail com anexo
        if type_of_part == "message/rfc822":
            # criação de uma nova pasta output2 para o e-mail anexado
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # salvamento das subpartes da mensagem irfc822 na pasta output2
            for subpart in part.get_payload():
                # na nova pasta irfc822, o contador reinicia em 0
                save_message(output2, subpart, 0)

        else:
            # não se trata de um e-mail com anexo
            # salvamento das subpartes na pasta atual output
            # O valor de irfc822 deve, então, ser incrementado para cada subparte message/rfc822
            for subpart in part.get_payload():
                # save_message retorna o último valor de irfc822
                # incrementado em 1 se subpart="message/rfc822"; caso contrário, não é incrementado
                irfc822 = save_message(output, subpart, irfc822)
    else:
        # outros casos (não text/plain, não text/html, não multipart)
        # anexo?
        disposition = part.get('Content-Disposition')
        if disposition and disposition.startswith('attachment'):
            save_attachment(output, part)
    # ignora-se as demais partes (não text/plain, não text/html, não anexo)
    # retorna o valor atual de irfc822 (numeração dos e-mails anexados armazenados na pasta de saída)
    return irfc822

Comentários

  • linhas 33-40: tratamos dois casos possíveis de uma mensagem em uma extremidade da árvore da mensagem inicial (sem subpartes). Ainda restam dois casos a serem tratados:
    • linhas 43-62: o caso em que a parte analisada contém, por sua vez, subpartes (part.ismultipart()==True);
    • linhas 63-68: para os casos restantes, tratamos apenas o caso em que a parte analisada é um anexo;

Vamos tratar deste último caso. Estamos, mais uma vez, em uma extremidade da mensagem inicial (sem subpartes). Já encontramos dois casos desse tipo: os tipos text/plain e text/html. Agora, vamos tratar do caso do arquivo anexado.

  • linha 66: o anexo é identificado pela chave [Content-Disposition];
  • linha 67: se essa chave existir e começar com a sequência [attachment], então estamos diante de um anexo à mensagem;
  • linha 68: o anexo é salvo na pasta [output];

A função [save_attachment] é a seguinte:


# salvar um anexo
def save_attachment(output: str, part: email.message.Message):
    # nome do arquivo anexado
    filename = os.path.basename(part.get_filename())

    # o nome do arquivo pode ser codificado
    # por exemplo =?utf-8?Q?Cursos-Tutoriais-Serge-Tah=C3=A9-1568x268=2Ep
    filename = decode_header(filename)
    # salvamos o arquivo anexado
    with open(f"{output}/{filename}", "wb") as file:
        file.write(part.get_payload(decode=True))
  • linha 4: se [part] for um anexo, então o nome do arquivo anexado é obtido por meio da função [part.get_filename]. Mantém-se apenas o nome do arquivo, e não seu caminho;
  • linha 8: os nomes dos arquivos geralmente são codificados da mesma forma que os cabeçalhos da mensagem. Por isso, utiliza-se a função [decode_header] para decodificá-los;
  • linha 11: o conteúdo do arquivo anexado é, por enquanto, uma sequência de caracteres gerada pela codificação (geralmente base64) do conteúdo original do arquivo. Para obter esse conteúdo original, utiliza-se a função [part.get_payload(decode=True)]. O parâmetro [decode=True] indica que o conteúdo do anexo deve ser decodificado. Obtém-se, então, uma sequência de bytes;
  • linha 10: essa sequência de bytes é salva no arquivo [output/filename]. O modo “wb” de abertura do arquivo significa “write binary”;

Voltemos ao código da função [save_message]:


def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # saída: pasta de armazenamento das mensagens
    # email_message: a mensagem a ser salva
    # irfc822: número atual na numeração dos e-mails anexados
    #
    # parte da mensagem
    part = email_message
    # os cabeçalhos [From, To, Subject] são encontrados em uma das partes multipart
    # ou em uma parte [text/*], quando não houver uma parte [multipart]
    keys = part.keys()
    # “From” deve constar nos cabeçalhos; caso contrário, a parte não possui os cabeçalhos que estamos procurando
    if "From" in keys:
        # recuperamos alguns cabeçalhos
        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'))}"]
        # salva os cabeçalhos em um arquivo de texto
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # gravação no arquivo
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # tipo da mensagem [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # se a mensagem for do tipo text/plain
    if type_of_part == "text/plain":
        # mensagem de texto
        save_textmessage(output, part, 0)

    # se a mensagem for do tipo text/html
    elif type_of_part == "text/html":
        # mensagem HTML
        save_textmessage(output, part, 1)

    # se a mensagem for um contêiner de partes
    elif part.is_multipart():
        # caso específico de e-mail com anexo
        if type_of_part == "message/rfc822":
            # criação de uma nova pasta output2 para o e-mail anexado
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # salvamento das subpartes da mensagem irfc822 na pasta output2
            for subpart in part.get_payload():
                # na nova pasta irfc822, o contador reinicia em 0
                save_message(output2, subpart, 0)

        else:
            # não se trata de um e-mail com anexo
            # salvamento das subpartes na pasta atual output
            # o irfc822 deve então ser incrementado para cada subparte message/rfc822
            for subpart in part.get_payload():
                # save_message retorna o último valor de irfc822
                # incrementado em 1 se subpart="message/rfc822"; caso contrário, não é incrementado
                irfc822 = save_message(output, subpart, irfc822)
    else:
        # outros casos (não text/plain, não text/html, não multipart)
        # anexo?
        disposition = part.get('Content-Disposition')
        if disposition and disposition.startswith('attachment'):
            save_attachment(output, part)
    # ignora-se as demais partes (não text/plain, não text/html, não anexo)
    # retorna o valor atual de irfc822 (numeração dos e-mails anexados armazenados na pasta de saída)
    return irfc822

Comentários

  • tratamos os casos das terminações da árvore da mensagem inicial: as partes [text/plain, text/html et Content-Disposition=attachment;…]. Resta-nos tratar o caso em que a parte analisada é um contêiner de partes, ou seja, contém subpartes [part.is_multipart()==True], linha 41. Para chegar às terminações da árvore da mensagem, é necessário, portanto, analisar essas subpartes;
  • linha 43: tratamos de maneira específica o caso em que a parte analisada tem o tipo [message/rfc822]. Esse é o tipo de um e-mail. Trata-se, portanto, do caso em que um e-mail tem como anexo outro e-mail;

O código é o seguinte:


    # se a mensagem for um contêiner de partes
    elif part.is_multipart():
        # caso específico do e-mail anexado
        if type_of_part == "message/rfc822":
            # criação de uma nova pasta “output2” para o e-mail anexado
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # salvamento das subpartes da mensagem irfc822 na pasta output2
            for subpart in part.get_payload():
                # na nova pasta irfc822, o contador reinicia em 0
                save_message(output2, subpart, 0)

        else:
            # não se trata de um e-mail com anexo
            # salvamento das subpartes na pasta atual output
            # o irfc822 deve então ser incrementado para cada subparte message/rfc822
            for subpart in part.get_payload():
                # save_message retorna o último valor de irfc822
                # incrementado em 1 se subpart="message/rfc822"; caso contrário, não é incrementado
                irfc822 = save_message(output, subpart, irfc822)

    return irfc822
  • a diferença entre uma parte [message/rfc822] e as outras partes multipart é que a pasta de armazenamento muda;
    • linhas 6-8: para a parte [message/rfc822], a pasta de armazenamento passa a ser a da linha 7, [output/rfc822_x], onde x é o número do e-mail anexado, 1 para o primeiro, 2 para o segundo…;
    • linha 21: para as demais partes multipart, a pasta de salvamento continua sendo a pasta [output] da mensagem inicial. Não há mudança de pasta;
  • linhas 10-12: cada subparte é salva por meio de uma chamada recursiva a [save_message]. O terceiro parâmetro é o índice de numeração dos e-mails encapsulados em [subpart]. Inicialmente, esse índice é igual a 0;
  • linha 21: mesma explicação da linha 12, mas o valor do terceiro parâmetro [irfc822] muda. Se, no loop das linhas 18-21, houver vários e-mails encapsulados, eles devem ser organizados em pastas […/rfc822-1…/rfc822_2…]. Portanto, o terceiro parâmetro da função [save_message] deve assumir sucessivamente os valores 1, 2, 3… Para isso, [save_message] retorna o valor de [irfc822] (linha 21).

Vamos dar um exemplo e supor que a lista de subpartes da linha 18 seja [subpart1, subpart2, subpart3, subpart4, subpart5] e que [subpart1, subpart3, subpart5] sejam e-mails anexados, [subpart2] seja uma parte do tipo text/plain e [subpart4] seja um anexo, e que ainda não tenhamos encontrado nenhum e-mail com anexo na mensagem [irfc822=0]. Nesse caso:

  • (continuação)
    • [subpart1] é gravado na linha 21: a função [saveMessage] é executada com irfc822=0;
    • [subpart1] é um e-mail com anexo, portanto irfc822 passa para 1 (linha 6 do código). É criada uma pasta [output/irfc822_1]. O valor retornado por [saveMessage(ouput,subpart1,0)] é, portanto, 1 (linha 23);
    • [subpart2] é gravado na linha 21: a função [saveMessage] é executada com irfc822=1;
    • [subpart2] não é um e-mail com anexo. Portanto, irfc822 permanece em 1. Esse é o valor recuperado na linha 21;
    • [subpart3] é gravado na linha 21: a função [save_message] é executada com irfc822=1;
    • [subpart3] é um e-mail com anexo; portanto, irfc822 passa para 2 (linha 6 do código). É criada uma pasta [output/irfc822_2]. O valor retornado por [save_message(ouput,subpart1,1)] é, portanto, 2 (linha 21);
    • [subpart4] é gravado na linha 21: a função [save_message] é executada com irfc822=2;
    • [subpart4] não é um e-mail com anexo. Portanto, irfc822 permanece em 2. Esse é o valor recuperado na linha 21;
    • [subpart5] é gravado na linha 21: a função [save_message] é executada com irfc822=2;
    • [subpart5] é um e-mail com anexo; portanto, irfc822 passa para 3 (linha 6 do código). É criada uma pasta [output/irfc822_3]. O valor retornado por [save_message(ouput,subpart1,2)] é, portanto, 3 (linha 21);

Exemplos de execução

Enviamos 4 e-mails para [pymail2parlexemple@gmail.com] a partir de: [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/];

Todos os e-mails terão como assunto [hélène va au marché] e como corpo do texto [acheter des légumes]. Queremos testar como os caracteres acentuados são recuperados.

Nós os lemos com o script [pop3/02/main] configurado com o seguinte arquivo [pop3/02/config]:


import os


def configure() -> dict:
    # configuração do aplicativo
    config = {
        # lista de caixas de correio a serem gerenciadas
        "mailboxes": [
            # servidor: servidor POP3
            # porta: porta do servidor POP3
            # usuário: usuário cujas mensagens se deseja ler
            # senha: a senha dele
            # maxmails: o número máximo de e-mails a serem baixados
            # timeout: tempo máximo de espera por uma resposta do servidor
            # delete: deve ser verdadeiro se for necessário excluir do servidor as mensagens baixadas
            # ssl: deve ser verdadeiro se a leitura dos e-mails for feita por meio de uma conexão segura
            # output: a pasta de armazenamento das mensagens baixadas

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

    # caminhos absolutos das pastas a serem incluídas no syspath
    absolute_dependencies = [
        # pasta local
        f"{script_dir}/../../shared",
    ]

    # configuração do syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # aplicamos a configuração
    return config

O resultado é o seguinte:

Image

A mensagem 1 é a enviada pelo Thunderbird:

Image

  • no [5], o Thunderbird [3] utiliza um [Transfer-Content-Encoding] do tipo [8bit];
  • em [4]: a mensagem está codificada em UTF-8;

A mensagem 2 é aquela enviada pelo cliente:

Image

Image

Observe-se que o [em Client] codifica os textos em UTF-8 ([4]) e os transfere para o [quoted-printable] e o [5]. Ele também enviou uma cópia da mensagem nos formatos HTML e [7-8]. Todos os gerenciadores de e-mail testados aqui são capazes de fazer isso. Trata-se de uma configuração.

A mensagem 3 é a enviada pelo Gmail:

Image

Observe-se que o Gmail codifica os textos em UTF-8 ([3]) e os transfere como [quoted-printable] e [4]. Em [6], a versão HTML da mensagem.

A mensagem 4 é aquela enviada pelo Outlook:

Image

Observe-se que o Outlook codifica os textos em iso-8859-1 ([3]) e os transfere para [quoted-printable] e [4].

Os exemplos anteriores mostram duas coisas:

  • nosso cliente [pop3/02] funcionou corretamente;
  • os gerenciadores de e-mail têm maneiras diferentes de enviar um e-mail;

Vamos agora examinar os arquivos anexados. Com o Thunderbird, esvaziamos a caixa de e-mail do usuário [pymail2parlexemple@gmail.com]. Em seguida, usamos o script [smtp/03/main] para enviar um e-mail com a seguinte configuração [smtp/03/config]:


import os


def configure() -> dict:
    # configuração do aplicativo
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        # descrição: descrição do e-mail enviado
        # smtp-server: servidor SMTP
        # smtp-port: porta do servidor SMTP
        # remetente: remetente
        # para: destinatário
        # assunto: assunto do e-mail
        # mensagem: mensagem do e-mail
        "mails": [
            {
                "description": "mail to gmail via gmail avec smtplib",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymail2parlexemple@gmail.com",
                "to": "pymail2parlexemple@gmail.com",
                "subject": "to gmail via gmail avec smtplib",
                # testando caracteres acentuados
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP com autenticação
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # aqui, é preciso inserir caminhos absolutos para os arquivos anexados
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                    f"{script_dir}/attachments/mail attaché 1.eml",
                ]
            }
        ]
    }
  • linhas 31-33: anexamos ao e-mail:
  • um arquivo do Word;
  • um arquivo PDF;
  • um e-mail contendo os mesmos dois arquivos anexados;

Depois de enviar o e-mail, executamos o script [pop3/02] para ler a caixa de e-mail do usuário [pymail2parlexemple@gmail.com]. Os resultados são os seguintes:

Image

  • em [1]: a mensagem com seus dois arquivos anexados;
  • em [2]: o próprio e-mail anexado com seus dois arquivos anexados;

Conclusão

O módulo [mail_parser.py] é particularmente complexo. Isso se deve à complexidade dos próprios e-mails. Vamos reutilizar esse módulo para o protocolo IMAP.

21.7. O protocolo IMAP

21.7.1. Introdução

Para ler os e-mails armazenados em um servidor de e-mails, existem dois protocolos:

  • o protocolo POP3 (Post Office Protocol), historicamente o primeiro protocolo, mas pouco utilizado atualmente;
  • o protocolo IMAP (Internet Message Access Protocol), um protocolo mais recente que o POP3 e o mais utilizado atualmente;

Para conhecer o protocolo IMAP, utilizaremos a seguinte arquitetura:

Image

  • O [Serveur B] será, conforme o caso:
    • um servidor IMAP local, implementado pelo servidor de e-mail [hMailServer];
    • o servidor [imap.gmail.com:993], que é o servidor IMAP do gerenciador de e-mails [Gmail];
  • [Client A] será um script em Python que utiliza módulos de Python para gerenciar anexos, bem como para estabelecer uma conexão criptografada e autenticada quando o servidor IMAP assim o exigir;

O protocolo IMAP vai além do protocolo POP3:

  • os e-mails são armazenados no servidor IMAP e podem ser organizados em pastas;
  • o cliente IMAP pode enviar comandos para criar, modificar ou excluir essas pastas;

Vejamos um exemplo com o Thunderbird. Na seguinte arquitetura:

Image

  • o Thunderbird é o cliente A;
  • [imap.gmail.com] é o servidor B (Gmail);

Vamos criar uma pasta nos e-mails do usuário [pymail2parlexemple@gmail.com] usando o Thunderbird:

Image

  • no [1-6], criamos a pasta [dossier1];

Image

  • em [7-8], movemos (com o mouse) todos os arquivos da pasta [Courrier entrant] para a pasta [dossier1];

Agora, vamos acessar o site do Gmail e fazer login como o usuário [pymail2parlexemple@gmail.com]:

Image

  • em [2-3], a caixa de entrada está vazia;
  • em [1], a pasta [dossier1] que foi criada;

Image

  • em [4-6]: os e-mails que foram movidos para a pasta [dossier1];

Estamos diante da seguinte arquitetura:

Image

  • O Cliente A é o aplicativo Thunderbird;
  • O Cliente C é o aplicativo web do Gmail;
  • O servidor B é o servidor IMAP do Gmail;

A árvore de pastas do usuário é mantida pelo servidor IMAP. Em seguida, todos os clientes IMAP se sincronizam com ele para apresentar ao usuário as pastas de sua conta. Aqui, o Thunderbird enviou vários comandos para:

  • criar a pasta [dossier1];
  • transferir mensagens para essa pasta;

21.7.2. script [imap/main]: cliente IMAP com o módulo [imaplib]

Image

O script [imap/main] é configurado pelo seguinte script [imap/config]:


import os


def configure() -> dict:
    # configuração do aplicativo
    config = {
        # lista de caixas de correio a serem gerenciadas
        "mailboxes": [
            # servidor: servidor IMAP
            # porta: porta do servidor IMAP
            # usuário: usuário cujas mensagens se deseja ler
            # senha: a senha dele
            # maxmails: o número máximo de e-mails a serem baixados
            # timeout: tempo máximo de espera por uma resposta do servidor
            # delete: deve ser verdadeiro se for necessário excluir do servidor as mensagens baixadas
            # ssl: deve ser verdadeiro se a leitura dos e-mails for feita por meio de uma conexão segura
            # output: a pasta de armazenamento das mensagens baixadas

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

    # caminhos absolutos das pastas a serem incluídas no syspath
    absolute_dependencies = [
        # pasta local
        f"{script_dir}/../shared",
    ]

    # configuração do syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # aplicamos a configuração
    return config

Comentários

  • linhas 8-29: a chave [mailboxes] está associada à lista de caixas de correio a serem consultadas;
  • linha 20: o servidor IMAP;
  • linha 21: sua porta de serviço;
  • linhas 22-23: o usuário cujos e-mails se deseja ler;
  • linha 24: o número máximo de e-mails que se deseja ler;
  • linha 25: indica se é necessário estabelecer uma conexão segura com o servidor IMAP (True) ou não (False);
  • linha 26: o tempo máximo de espera por uma resposta do servidor;
  • linha 27: pasta de armazenamento dos e-mails lidos;

O script [imap/main] é o seguinte:


# importações
import email
import imaplib
import os
import shutil


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

def readmails(mailbox: dict):
    


# main ----------------------------------------------------------------
#  cliente IMAP que permite ler e-mails

# recuperamos a configuração do aplicativo
import config
config = config.configure()

# processa-se as caixas de e-mail uma a uma
for mailbox in config['mailboxes']:
    try:
        # exibição no console
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']} / {mailbox['server']}:{mailbox['port']}")
        # leitura da caixa de e-mail
        readmails(mailbox)
        # fim
        print("Lecture terminée...")
    # exceto BaseException como erro:
    #     # exibe o erro
    #     print(f"Ocorreu o seguinte erro: {erro}")
    finally:
        pass

Comentários

  • linhas 14-36: encontramos aqui a abordagem já vista no script |pop3/02/main|;

A função [readmails] é a seguinte:


def readmails(mailbox: dict):
    # deixa-se que as exceções sejam propagadas
    #
    # módulo do analisador de e-mail
    from mail_parser import save_message

    # recuperando informações de configuração
    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']
    #
    # vamos lá
    imap_resource = None
    try:
        # criando as pastas de armazenamento, caso ainda não existam
        if not os.path.isdir(output):
            os.mkdir(output)
        # usuário
        dir2 = f"{output}/{user}"
        # excluindo a pasta [dir2], caso exista, e recriando-a
        if os.path.isdir(dir2):
            # exclusão
            shutil.rmtree(dir2)
        # criação
        os.mkdir(dir2)
        # conexão com o servidor IMAP
        if ssl:
            imap_resource = imaplib.IMAP4_SSL(server, port)
        else:
            imap_resource = imaplib.IMAP4(server, port)
        # tempo limite das comunicações do cliente
        sock = imap_resource.socket()
        sock.settimeout(timeout)
        # autenticação
        imap_resource.login(user, password)
        # seleciona-se a pasta INBOX (correio recebido)
        imap_resource.select('INBOX')
        # recuperam-se todas as mensagens dessa pasta: critério ALL
        # sem codificação específica: None
        typ1, data1 = imap_resource.search(None, 'ALL')
        # print(f"typ={typ1}, data={data1}")

        # data1[0] é uma matriz de bytes que reúne os números de todas as mensagens, separados por um espaço
        nums = data1[0].split()
        imail = 0
        fini = imail >= maxmails or imail >= len(nums)
        # os e-mails são lidos um por um
        while not fini:
            # num é um número de mensagem em binário
            num = nums[imail]
            # print(f"mensagem nº {num}")

            # recuperamos a mensagem nº num
            typ2, data2 = imap_resource.fetch(num, '(RFC822)')
            # print(f"tipo={typ2}, dados={data2}")

            # data é uma lista que contém tuplas; neste caso, apenas uma
            # data[0] é o tuplo; dataQZXW2HTMLBWzBdZQXQZXW2HTMLBWzFdZQX é o segundo elemento do tuplo
            # dataQZXW2HTMLBWzBdZQXQZXW2HTMLBWzFdZQX contém uma sequência de bytes que representa todas as linhas da mensagem
            # por mensagem deve-se entender o texto da mensagem + todos os arquivos anexados

            # a mensagem é recuperada como do tipo email.message.Message
            message = email.message_from_bytes(data2[0][1])
            # pasta da mensagem
            dir3 = f"{dir2}/message_{int(num)}"
            # se a pasta não existir, ela é criada
            if not os.path.isdir(dir3):
                os.mkdir(dir3)
            # a mensagem é salva
            save_message(dir3, message)
            # próxima mensagem
            imail += 1
            fini = imail >= maxmails or imail >= len(nums)
    finally:
        if imap_resource:
            # encerra-se a conexão com a caixa de correio
            imap_resource.close()
            # desconectamos do servidor IMAP
            imap_resource.logout()

Comentários

  • linhas 7-15: recuperamos os elementos da configuração;
  • linhas 19, 79: o código é controlado por um try / finally. Portanto, as exceções não são interceptadas (ausência da cláusula except) e, assim, são repassadas ao código chamador, que as interrompe e as exibe;
  • linhas 23-30: cria-se a pasta de armazenamento dos e-mails;
  • linhas 31-35: conecta-se ao servidor IMAP. A classe utilizada varia dependendo se se trata de um servidor IMAP seguro (IMAP4_SSL) ou não (IMAP4);
  • linhas 36-38: define-se o tempo limite para as comunicações cliente/servidor;
  • linhas 39-40: realiza-se a autenticação no servidor IMAP;
  • linhas 41-42: vimos que a caixa de e-mail de um usuário IMAP pode ser organizada em pastas. A pasta [INBOX] é a de e-mails recebidos. Para selecionar a pasta [dossier1], escreveríamos [imapResource.select('dossier1')];
  • linhas 43-45: solicita-se a lista de todas as mensagens encontradas em [INBOX]:
    • o primeiro parâmetro de [imapResource.search] é um tipo de codificação. [None] significa “sem filtro de codificação”;
    • O segundo parâmetro é um critério. Existem diferentes maneiras de expressá-lo. O critério [ALL] significa que queremos todas as mensagens da pasta;

O resultado de [imapResource.search] é semelhante a este:


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

[data] é uma lista que contém os números das mensagens obtidas. Esses números estão em binário. Acima, duas mensagens foram encontradas na pasta [INBOX];

  • linha 49: recuperamos os números das mensagens. Acima, teremos a lista [b'1' b'2'], uma lista de números codificados em binário;
  • linhas 53-78: vamos executar um loop para ler as mensagens da pasta [INBOX];
  • linhas 54-55: número da mensagem;
  • linhas 58-59: a mensagem nº [num] é solicitada ao servidor IMAP;
    • o primeiro parâmetro é o número da mensagem desejada;
    • o segundo parâmetro é uma sequência “(parte1)(parte2)…”, em que [parti] é o nome de uma parte da mensagem. Não aprofundei esse ponto. O nome (RFC822) designa o e-mail na íntegra;

Recebemos algo no seguinte formato:


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

O elemento [data] é, neste caso, uma lista com um único elemento, e esse único elemento é uma tupla de três elementos:


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

O segundo elemento dessa tupla é uma sequência binária que representa a mensagem solicitada na íntegra. Reconhecemos acima elementos já apresentados durante o estudo do módulo [mail_parser].

data[0] representa uma tupla com dois elementos. data[0][1] representa as linhas da mensagem em formato binário.

  • linha 68: a função [taxpayers[slice(10,12)]] constrói um objeto do tipo [email.message.Message] a partir das linhas da mensagem. O tipo [email.message.Message] é o tipo do parâmetro do módulo [mail_parser] que escrevemos anteriormente;
  • linhas 69-73: criamos a pasta de armazenamento da mensagem nº [num];
  • linha 75: chamamos a função [save_message] do módulo [mail_parser] da linha 5. Essa função foi descrita no parágrafo |pop3/02/main|;
  • linhas 76-78: entramos em um loop para processar a mensagem seguinte;
  • linhas 79-84: independentemente de ter ocorrido erro ou não:
    • linha 82: encerra-se a conexão com a pasta consultada;
    • linha 84: desconecta-se do servidor IMAP;

Os resultados obtidos são idênticos aos obtidos com o script [pop3/02/main]. Isso é normal, já que é utilizado o mesmo analisador de e-mail [mail_parser].