21. Funções de Internet
Passamos agora às funções de Internet do Python que nos permitem programar em TCP / IP (Transfer Control Protocol / Internet Protocol).

21.1. Noções básicas de programação na Internet
21.1.1. Generalidades
Consideremos a comunicação entre duas máquinas remotas A e B:

Quando uma aplicação AppA da máquina A pretende comunicar com uma aplicação AppB da máquina B na Internet, tem de saber várias coisas:
- 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. Com efeito, a máquina B pode suportar várias aplicações que operam na Internet. Quando recebe informações provenientes da rede, tem de saber a que aplicação essas informações se destinam. As aplicações da máquina B têm acesso à rede através de interfaces, também denominadas portas de comunicação. Esta informação está contida no pacote recebido pela máquina B, para que seja entregue à aplicação correta;
- os protocolos de comunicação compreendidos pela máquina B. No nosso estudo, utilizaremos apenas os protocolos TCP-IP;
- o protocolo de comunicação aceite pela aplicação AppB. Com efeito, as máquinas A e B vão «comunicar» entre si. O que vão transmitir será encapsulado nos protocolos TCP-IP. No entanto, quando, no final da cadeia, a aplicação AppB receber a informação enviada pela aplicação AppA, terá de ser capaz de a interpretar. Isto é análogo à situação em que duas pessoas, A e B, comunicam por telefone: o seu diálogo é transportado pelo telefone. A fala será codificada sob a forma de sinais pelo telefone A, transportada por linhas telefónicas, chegará ao telefone B para aí ser descodificada. A pessoa B ouve então as palavras. É aqui que entra o conceito de protocolo de diálogo: se A falar francês e B não compreender essa língua, A e B não poderão dialogar de forma útil;
Por isso, as duas aplicações que comunicam entre si têm de chegar a acordo quanto ao tipo de diálogo que vão adotar. Por exemplo, o diálogo com um serviço ftp não é o mesmo que com um serviço pop: estes dois serviços não aceitam os mesmos comandos. Têm um protocolo de diálogo diferente;
21.1.2. As características do protocolo TCP
Aqui, iremos analisar apenas as comunicações de rede que utilizam o protocolo de transporte TCP, cujas principais características são as seguintes:
- o processo que pretende transmitir estabelece, em primeiro lugar, uma ligação com o processo destinatário das informações que vai transmitir. Esta ligação é estabelecida entre uma porta da máquina emissora e uma porta da máquina recetora. Entre as duas portas é criado um caminho virtual, que ficará reservado exclusivamente aos dois processos que estabeleceram a ligação;
- todos os pacotes enviados pelo processo de origem seguem este 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 ao seu próprio ritmo. Estas não são necessariamente enviadas de imediato: o protocolo TCP aguarda até ter quantidade suficiente para as enviar. São armazenadas numa estrutura denominada segmento TCP. Assim que estiver preenchido, este segmento será transmitido para a camada IP, onde será encapsulado num pacote IP;
- cada segmento enviado pelo protocolo TCP é numerado. O protocolo TCP destinatário verifica se recebe os segmentos na sequência correta. Por cada segmento recebido corretamente, envia um aviso de receção ao remetente;
- quando este último o recebe, comunica-o ao processo emissor. Este pode, assim, saber que um segmento chegou ao destino;
- se, após um determinado período de tempo, o protocolo TCP que emitiu um segmento não receber uma confirmação de receção, reenvia 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 comunicam entre si é o full-duplex: isto significa que a informação pode transitar nos dois sentidos. Assim, o processo de destino pode enviar confirmações de receção mesmo enquanto o processo de origem continua a enviar informações. Isto permite, por exemplo, que o protocolo de origem TCP envie vários segmentos sem esperar por uma confirmação de receção. Se, após algum tempo, verificar que não recebeu a confirmação de receção de um determinado segmento n.º n, 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 ligação para solicitar um serviço à máquina B, especificando que pretende estabelecer uma ligação com o serviço SB1 da máquina B. Esta aceita ou recusa. Se aceitar, a máquina A pode enviar os seus pedidos ao serviço SB1. Estes devem estar em conformidade com o protocolo de diálogo compreendido pelo serviço SB1. Estabelece-se assim um diálogo de pedido-resposta entre a máquina A, a que se chama máquina cliente, e a máquina B, a que se chama máquina servidor. Um dos dois parceiros encerrará a ligação.
21.1.4. Arquitetura de um cliente
A arquitetura de um programa de rede que solicita os serviços de uma aplicação servidor será a seguinte:
21.1.5. Arquitetura de um servidor
A arquitetura de um programa que oferece serviços será a seguinte:
O programa servidor trata de forma diferente o pedido de ligação inicial de um cliente das suas solicitações posteriores destinadas a obter um serviço. O programa não presta o serviço por si próprio. Se o fizesse, durante o período em que o serviço estivesse a ser prestado, deixaria de estar à escuta dos pedidos de ligação e os clientes não seriam, então, atendidos. O procedimento é outro: assim que uma solicitação de ligação é recebida na porta de escuta e, em seguida, aceite, o servidor cria uma tarefa encarregada de prestar o serviço solicitado pelo cliente. Este serviço é prestado numa outra porta da máquina servidora, denominada porta de serviço. Desta forma, é possível atender vários clientes ao mesmo tempo.
Uma tarefa de serviço terá a seguinte estrutura:
21.2. Descubra os protocolos de comunicação da Internet
21.2.1. Introdução
Quando um cliente se liga a um servidor, estabelece-se então um diálogo entre ambos. A natureza desse diálogo constitui o que se denomina 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 correio eletrónico (servidor SMTP);
- POP: Post Office Protocol — o protocolo de comunicação com um servidor de armazenamento de correio eletrónico (servidor POP). Trata-se aqui de recuperar os e-mails recebidos e não de os enviar;
- IMAP: Internet Message Access Protocol — o protocolo de comunicação com um servidor de armazenamento de correio eletrónico (servidor IMAP). Este protocolo substituiu progressivamente o protocolo mais antigo POP;
- FTP: File Transfer Protocol — o protocolo de comunicação com um servidor de armazenamento de ficheiros (servidor FTP);
Todos estes protocolos têm a particularidade de serem protocolos de linhas de texto: o cliente e o servidor trocam entre si linhas de texto. Se tivermos um cliente capaz de:
- estabelecer uma ligação com um servidor TCP;
- exibir na consola as linhas de texto que o servidor lhe envia;
- enviar ao servidor as linhas de texto que um utilizador digitaria no teclado;
Assim, é possível 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

Nos códigos associados a este documento, encontram-se dois utilitários de comunicação TCP:
- O [RawTcpClient] permite ligar-se à porta P de um servidor S;
- O [RawTcpServer] permite criar um servidor que aguarda clientes na porta P;
Trata-se de dois programas em C# cujos códigos-fonte lhe são fornecidos. Pode, portanto, 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 em que está a trabalhar):
- o servidor pode atender vários clientes simultaneamente;
- o servidor executa os comandos digitados pelo utilizador no teclado. Estes são os seguintes:
- list: lista os clientes atualmente ligados ao servidor. Estes são apresentados 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. São necessários no comando. Servem para delimitar visualmente o texto enviado ao cliente;
- close x: encerra a ligação com o cliente n.º x;
- quit: encerra todas as ligações e interrompe o serviço;
- as linhas enviadas pelo cliente para o servidor são apresentadas na consola;
- todas as trocas de dados são registadas num ficheiro de texto com o nome [machine-port.txt], em que
- [machine] é o nome da máquina na qual o código está a ser executado;
- [port] é a porta do serviço que responde aos pedidos do cliente;
O cliente TCP [RawTcpClient] é chamado com a sintaxe [RawTcpClient serveur port] para se ligar à porta [port] do servidor [serveur]:
- as linhas digitadas pelo utilizador no teclado são enviadas para o servidor;
- as linhas enviadas pelo servidor são apresentadas na consola;
- todas as trocas de dados são registadas num ficheiro de texto com o nome [serveur-port.txt];
Vejamos um exemplo. Abrimos duas janelas de terminal PyCharm e, em cada uma delas, acedemos à pasta dos utilitários:

Numa 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 dos utilitários;
- na linha 1, iniciamos o servidor TCP na porta 100;
- linhas 2-4, o servidor fica à espera de um cliente TCP e apresenta uma lista de comandos que o utilizador pode introduzir através do teclado;
- na linha 5, o servidor aguarda um comando digitado pelo utilizador;
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) :
- na linha 1, estamos na pasta dos utilitários;
- na linha 1, iniciamos o cliente TCP: indicamos-lhe que se ligue à porta 100 da máquina local (aquela em que está a ser executado o código de [RawTcpClient]);
- na linha 2, o cliente conseguiu ligar-se ao servidor. Indicam-se as coordenadas do cliente: este encontra-se na máquina [DESKTOP-30FF5FB] (a máquina local neste exemplo) e utiliza a porta [51173] para comunicar com o servidor:
- linha 3, o cliente aguarda um comando digitado pelo utilizador no teclado;
Voltemos à janela do servidor. O 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, foi detetado um cliente. O servidor atribuiu-lhe o n.º 1. O servidor identificou corretamente o cliente remoto (máquina e porta);
- linha 6, o servidor volta a ficar à espera de 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. O 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 parênteses retos, 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 os 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 é o que está entre parênteses;
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 ligaçã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 detetou o fim do serviço;
Foram criados dois ficheiros de registo, um para o servidor e outro para o cliente:

- em [1], os registos do servidor: o nome do ficheiro é o nome do cliente na forma [machine-port]. Isto permite ter ficheiros de registo diferentes para clientes diferentes;
- em [2], os registos do cliente: o nome do ficheiro é o nome do servidor no formato [machine-port];
Os registos do servidor são os seguintes:
<-- [hello from client]
--> [hello from server]
Os registos 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

Os computadores na Internet são identificados por um endereço IP (IPv4 ou IPv6) e, na maioria das vezes, por um nome. Mas, em última análise, apenas o endereço IP é utilizado pelos protocolos de comunicação da Internet. Por isso, é necessário conhecer o endereço IP de um computador identificado pelo seu 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 pretende obter IP
try:
# nom_machine-->endereço IP
ip = socket.gethostbyname(nom_machine)
print(f"ip[{nom_machine}]={ip}")
except socket.error as erreur:
# é apresentado 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:
# é apresentado o erro
print(f"names[{ip}]={erreur}")
return
# ---------------------------------------- main
# as máquinas de 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 a gestão dos sockets 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 um computador, 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 um computador a partir de um desses nomes (um computador na Internet pode ter um nome principal e aliases);
- linha 12: as funções relacionadas com os 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 do 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á errado. O nome [localhost] refere-se à máquina na qual está a trabalhar e que está a executar o script;
- linhas 33-35: são apresentados os IP destas 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

Quando um navegador apresenta um URL, este atua como cliente de um servidor web ou, por outras palavras, de um servidor HTTP. É ele que toma a iniciativa e começa por enviar 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, através de um navegador, solicitamos o URL [http://localhost:100], ou seja, indicamos que o servidor HTTP consultado está a funcionar na porta 100 do computador local:

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 ligou;
- linhas 9-22: a sequência de linhas de texto que ele enviou:
- linha 9: esta linha tem o formato [GET URL HTTP/1.1]. Solicita o URL / e pede ao servidor para utilizar o protocolo HTTP 1.1;
- linha 10: esta linha tem o formato [Host: serveur:port]. A maiúscula ou minúscula do comando [Host] não importa. Recorde-se aqui que o cliente consulta um servidor local a operar na porta 100;
- linha 14: o comando [User-Agent] indica a identidade do cliente;
- linha 15: o comando [Accept] indica quais os tipos de documentos aceites pelo cliente;
- linha 21: o comando [Accept-Language] indica em que língua se pretendem os documentos solicitados, caso existam em várias línguas;
- linha 11: o comando [Connection] indica o modo de ligação pretendido: [keep-alive] indica que a ligação deve ser mantida até que as trocas de dados estejam concluídas;
- linha 22: o cliente termina os seus comandos com uma linha em branco;
Terminamos a ligaçã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 este URL com o nosso cliente TCP [RawTcpClient]. O servidor Apache do Laragon (parágrafo |Instalação do Laragon|) será o nosso servidor web.
Vamos iniciar o Laragon e, em seguida, o servidor web Apache:


Agora, utilizando um navegador, vamos aceder à página URL [http://localhost:80]. Aqui, especificamos apenas o servidor [localhost:80] e não o documento URL. Neste caso, é o URL / que é solicitado, ou seja, a raiz do servidor web:

- em [1], o URL solicitado. Inicialmente, digitámos [http://localhost:80] e o navegador (Firefox, neste caso) transformou-a simplesmente em [localhost], uma vez que 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:

- clicamos com o botão direito do rato 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, solicitemos o URL [http://localhost:80] com o 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, ligamo-nos à porta 80 do servidor localhost. É aí que funciona o servidor web do Laragon;
Digitamos agora 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...
- na linha 4, o comando [GET]. Solicitamos a raiz / do servidor web;
- linha 5, o comando [Host];
- estes são os únicos dois comandos indispensáveis. Para os restantes comandos, o servidor web utilizará valores por predefiniçã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 assinala o fim dos cabeçalhos HTTP;
- linhas 14-82: o documento HTML solicitado na linha 4;
Carregamos o ficheiro de registos [localhost-80.txt]:

--> [GET / HTTP/1.1]
--> [Host: localhost:80]
--> []
<-- [HTTP/1.1 200 OK]
<-- [Date: Sun, 05 Jul 2020 12:42:14 GMT]
<-- [Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19]
<-- [X-Powered-By: PHP/7.2.19]
<-- [Content-Length: 1776]
<-- [Content-Type: text/html; charset=UTF-8]
<-- []
<-- [<!DOCTYPE html>]
<-- [<html>]
<-- [ <head>]
<-- [ <title>Laragon</title>]
<-- []
<-- [ <link href="https://fonts.googleapis.com/css?family=Karla:400" rel="stylesheet" type="text/css">]
<-- []
<-- [ <style>]
<-- [ html, body {]
<-- [ height: 100%;]
<-- [ }]
<-- []
<-- [ body {]
<-- [ margin: 0;]
<-- [ padding: 0;]
<-- [ width: 100%;]
<-- [ display: table;]
<-- [ font-weight: 100;]
<-- [ font-family: 'Karla';]
<-- [ }]
<-- []
<-- [ .container {]
<-- [ text-align: center;]
<-- [ display: table-cell;]
<-- [ vertical-align: middle;]
<-- [ }]
<-- []
<-- [ .content {]
<-- [ text-align: center;]
<-- [ display: inline-block;]
<-- [ }]
<-- []
<-- [ .title {]
<-- [ font-size: 96px;]
<-- [ }]
<-- []
<-- [ .opt {]
<-- [ margin-top: 30px;]
<-- [ }]
<-- []
<-- [ .opt a {]
<-- [ text-decoration: none;]
<-- [ font-size: 150%;]
<-- [ }]
<-- [ ]
<-- [ a:hover {]
<-- [ color: red;]
<-- [ }]
<-- [ </style>]
<-- [ </head>]
<-- [ <body>]
<-- [ <div class="container">]
<-- [ <div class="content">]
<-- [ <div class="title" title="Laragon">Laragon</div>]
<-- [ ]
<-- [ <div class="info"><br />]
<-- [ Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19<br />]
<-- [ PHP version: 7.2.19 <span><a title="phpinfo()" href="/?q=info">info</a></span><br />]
<-- [ Document Root: C:/MyPrograms/laragon/www<br />]
<-- []
<-- [ </div>]
<-- [ <div class="opt">]
<-- [ <div><a title="Getting Started" href="https://laragon.org/docs">Getting Started</a></div>]
<-- [ </div>]
<-- [ </div>]
<-- []
<-- [ </div>]
<-- [ </body>]
<-- [</html>]
- linhas 11-79: o documento HTML recebido. No exemplo anterior, o Firefox tinha recebido o mesmo;
Temos agora as bases para programar um cliente TCP que solicitaria um URL.
21.4.3. Exemplo 3

O script [http/01/main.py] é um cliente HTTP configurado pelo ficheiro [config.py]. O conteúdo deste é o seguinte:
def configure():
# URLs a consultar
urls = [
# site: nome do site ao qual se deve ligar
# porta: porta do serviço web
# GET: URL solicitado
# headers: cabeçalhos HTTP a enviar 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 ficheiro é uma lista de URL, sendo cada elemento da lista um dicionário. Este dicionário indica como estabelecer ligação 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ê o URL URL do site url["GET"] e guarda-o no ficheiro url[site].html
# a comunicação cliente/servidor é efetuada de acordo com o protocolo HTTP indicado no dicionário [url]
# permite-se que as exceções sejam propagadas
sock = None
html = None
try:
# ligação ao [site] na porta 80 com um tempo limite
site = url['site']
sock = socket.create_connection((site, int(url['port'])), float(url['timeout']))
# a ligação representa um fluxo de comunicação bidirecional
# entre o cliente (este programa) e o servidor web contactado
# este canal é utilizado para a troca de comandos e informações
# o protocolo de comunicação é HTTP
# criação do ficheiro site.html — substituem-se os caracteres indesejáveis por um nome de ficheiro
site2 = site.replace("/", "_")
site2 = site2.replace(".", "_")
html_filename = f'{site2}.html'
html = codecs.open(f"output/{html_filename}", "w", "utf-8")
# o cliente irá iniciar a comunicação HTTP com o servidor
if suivi:
print(f"Client : début de la communication avec le serveur [{site}]")
# dependendo dos servidores, 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}"
# acompanhamento?
if suivi:
print(f"--> {commande}", end='')
# envia-se o comando para o servidor
sock.send(bytearray(commande, 'utf-8'))
# emissão dos cabeçalhos HTTP
for verb, value in url['headers'].items():
# construção do comando a enviar
commande = f"{verb}: {value}{end_of_line}"
# seguimento?
if suivi:
print(f"--> {commande}", end='')
# envia-se o comando para o servidor
sock.send(bytearray(commande, 'utf-8'))
# envia-se o cabeçalho HTTP [Connection: close] para solicitar ao servidor web
# que encerre a ligação assim que tiver enviado 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 vai agora responder no canal sock. Vai enviar todos
# os seus dados e, em seguida, encerrará o canal. O cliente lê, portanto, tudo o que chega do sock
# até ao encerramento do canal
#
# primeiro, lêem-se os cabeçalhos HTTP enviados pelo servidor
# que também terminam com uma linha vazia
if suivi:
print(f"Réponse du serveur [{site}]")
# leitura do socket como se fosse um ficheiro de texto
encoding = f"{url['encoding']}" if url['encoding'] else None
if encoding:
file = sock.makefile(encoding=encoding)
else:
file = sock.makefile()
# este ficheiro é processado linha a linha
fini = False
while not fini:
# leitura da linha atual
ligne = file.readline().strip()
# temos uma linha não vazia?
if ligne:
if suivi:
# exibe-se o cabeçalho HTTP
print(f"<-- {ligne}")
else:
# era a linha vazia — os cabeçalhos HTTP terminaram
fini = True
# está a ser lido o documento HTML, que se seguirá à linha vazia
# leitura da linha atual
ligne = file.readline()
while ligne:
# registo no ficheiro de logs
html.write(str(ligne))
# linha seguinte
ligne = file.readline()
# o ciclo termina quando o servidor encerra a ligação
finally:
# o cliente encerra a ligação
if sock:
sock.close()
# fecho do ficheiro HTML
if html:
html.close()
# -------------------main
# configura-se a aplicação
import config
config = config.configure()
# obter os URL do ficheiro 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 ao código:
- linhas 108-109: o dicionário [config] do módulo [config.py] é recuperado;
- linhas 111-122: este dicionário é utilizado;
- linha 118, 7: a função [get_url(url)] solicita um documento do site url[site] e armazena-o no ficheiro de texto url[site].HTML. Por predefinição, as trocas cliente/servidor são registadas na consola (monitorização=True);
- tudo é feito num [try / finally] (linhas 14-96). Não existe uma cláusula [except]. As exceções são encaminhadas para o código chamador, sendo este que as interrompe e as apresenta (linhas 119-120);
- linhas 16-17: abertura de uma ligação ao servidor web. A função [socket.create_connection] aceita três parâmetros:
- [param1]: é o nome do servidor na Internet ao qual se pretende aceder;
- [param2]: é o número da porta do serviço ao qual se pretende ligar;
- [param3]: a função [socket.create_connection] devolve um socket e a função [param3], se estiver presente, define o tempo de espera do socket criado. O tempo de espera é o período máximo durante o qual o socket aguarda uma resposta do computador remoto;
- linhas 27-28: criação do ficheiro [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 ao cliente enviar dados para o servidor. Aqui, a linha de texto enviada tem o seguinte significado: «Quero (GET) a página [URL] do site ao qual estou ligado. Estou a trabalhar 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). Esta matriz é obtida através da conversão da cadeia [commande] numa sequência de bytes codificados em UTF-8;
- linhas 44-52: enviam-se as restantes linhas do protocolo HTTP [Host, User-Agent, Accept, Accept-Language…]. A sua ordem não importa;
- linhas 53-55: envia-se o cabeçalho HTTP [Connection: close] para solicitar ao servidor que encerre a sua ligação assim que tiver enviado o documento solicitado. Por predefinição, o servidor não o faz. Por isso, é necessário solicitá-lo explicitamente. A vantagem é que este encerramento será detetado do lado do cliente e é assim que este saberá que recebeu todo o documento solicitado;
- linhas 56-57: envia-se uma linha vazia ao servidor para indicar que o cliente terminou de enviar os seus cabeçalhos HTTP e que aguarda agora o documento solicitado;
- linhas 68-86: o servidor irá, em primeiro lugar, enviar uma série de cabeçalhos HTTP que fornecerão diversas informações sobre o documento solicitado. Estes cabeçalhos terminam com uma linha vazia;
- linhas 69-73: para poder ler a resposta do servidor, linha a linha, utiliza-se o método [sock.makefile(encoding=encoding)]. O parâmetro opcional [encoding] especifica a codificação do texto esperado. Após esta operação, o fluxo de linhas enviadas pelo servidor poderá ser lido como um ficheiro de texto clássico;
- linha 78: lê-se uma linha enviada pelo servidor com o método [readline]. Removem-se os espaços (espaços em branco, caracteres de fim de linha) do início e do fim da linha;
- linhas 81-83: se a linha não estiver vazia e tiver sido solicitado o acompanhamento, a linha recebida é apresentada na consola;
- linhas 84-86: se tiver sido recuperada a linha vazia que marca o fim dos cabeçalhos HTTP enviados pelo servidor, então interrompe-se o ciclo da linha 76;
- linhas 90-95: as linhas de texto da resposta do servidor podem ser lidas linha a linha com um ciclo while e guardadas no ficheiro de texto [html]. Quando o servidor web tiver enviado a totalidade da página que lhe foi solicitada, encerra a sua ligação com o cliente. Do lado do cliente, isto será detetado como um fim de ficheiro e sairemos do ciclo das linhas 90-95;
- linhas 96-102: quer haja erro ou não, libertam-se todos os recursos utilizados pelo código;
Resultados:
A consola apresenta os seguintes registos:
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: o pedido feito ao servidor [http://tahe.developpez.com] está incorreto (código 400);
- linha 65: a página URL [http://www.sergetahe.com/] não foi encontrada (código 301). O código 301 significa que a página solicitada mudou de endereço (URL) de forma definitiva. A nova URL é indicada pelo cabeçalho HTTP [Location] da linha 71;
De um modo geral, os códigos 3xx, 4xx e 5xx de um servidor HTTP são códigos de erro.
A execução produziu os seguintes ficheiros:

O ficheiro [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 facto, obtivemos o mesmo documento que com o navegador Firefox.
O documento [output/sergetahe_com.html] recebido é o seguinte:

A maioria dos servidores HTTP envia as suas respostas às solicitações que lhes são feitas em partes. Cada parte enviada é precedida por uma linha que indica o número de bytes da parte seguinte. Isto permite ao cliente ler exatamente esse número de bytes para obter a parte. Aqui, o 0 indica que a parte seguinte tem zero bytes. Recorde-se que o servidor tinha indicado que o documento [http://sergetahe.com/] tinha substituído o URL. Por conseguinte, não enviou qualquer 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 ao servidor especificar a causa do erro. Esta é indicada nas linhas 6 e 7:
- linha 7: o nosso cliente utilizou o protocolo HTTP;
- linha 8: o servidor funciona 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 envia um documento HTML que detalha esse erro (linhas 1-7).
21.4.4. Exemplo 4
Os exemplos anteriores mostraram-nos que o nosso cliente HTTP era insuficiente. Vamos agora apresentar uma ferramenta chamada [curl] que permite recuperar documentos da Web, gerindo as dificuldades mencionadas: protocolo HTTPS, documento enviado em partes, redirecionamentos… A ferramenta [curl] foi instalada com o Laragon:

Abramos um terminal PyCharm [1]:

- em [1], o acesso aos terminais de PyCharm;
- em [2-3], os terminais já ativos;
- em [4], a pasta em que se encontra. 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 facto de o comando [curl –help] ter produzido resultados mostra que o comando [curl] se encontra no PATH do terminal. No Windows, o PATH é o conjunto de pastas exploradas quando o utilizador introduz um comando executável, neste caso o [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. Nesta lista não aparece nenhuma pasta relacionada com o Laragon. Se investigarmos um pouco, descobrimos que existe um [curl] na pasta [c:\windows\system32]. Foi este que respondeu anteriormente.
Se quisermos utilizar a ferramenta [curl] fornecida com o Laragon, podemos proceder da seguinte forma:


- em [2], o terminal Laragon;
- em [3], este botão permite criar novos terminais, cada um dos quais se instala num separador da janela acima;
- em [4], solicita-se o PATH do terminal Laragon;
- obtém-se algo muito diferente do que se tinha obtido num 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:

Posteriormente, utilize o terminal da sua preferência. Basta ter em conta que, quando pretender utilizar uma ferramenta fornecida pelo Laragon, é preferível utilizar o terminal do Laragon.
O comando [curl --help] apresenta todas as opções de configuração do [curl]. Existem várias dezenas delas. Iremos utilizar muito poucas. Para solicitar um URL, basta digitar o comando [curl URL]. Este comando irá apresentar na consola o documento solicitado. Se, além disso, quisermos ver as trocas de dados HTTP entre o cliente e o servidor, escreveremos [curl --verbose URL]. Por fim, para guardar o documento HTML solicitado num ficheiro, escreveremos [curl --verbose --output fichier URL].
Para evitar sobrecarregar o sistema de ficheiros do nosso computador, vamos mudar para outro local (neste caso, estou a utilizar 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, deslocamo-nos para a pasta [c:\temp]. Se esta pasta não existir, pode criá-la ou escolher outra;
- na linha 6, criamos uma pasta chamada [curl];
- na linha 9, posiciona-se nessa pasta;
- na linha 12, listamos o seu conteúdo. Está vazio (linha 20);
Certifique-se de que o servidor Apache do Laragon está em execução e, a partir de [curl], solicite os ficheiros URL e [http://localhost/] com 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 inalterado
- linhas 10-13: linhas enviadas por [curl] para o servidor [localhost]. Reconhece-se o protocolo HTTP;
- linhas 14-20: linhas enviadas em resposta pelo servidor;
- linha 14: indica que o documento solicitado foi efetivamente recebido;
O ficheiro [localhost.html] contém o documento solicitado. Pode verificar isso abrindo o ficheiro num editor de texto.
Agora, vamos solicitar o URL [https://tahe.developpez.com:443/]. Para obter este URL, o cliente HTTP tem de saber comunicar em HTTPS. É o caso do cliente [curl].
Os resultados da consola 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 mantido intacto
- linhas 10-39: as trocas entre cliente e servidor para proteger a ligação: esta será encriptada;
- 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;
O [curl] gere corretamente tanto o protocolo seguro HTTPS como o facto de o documento ser enviado em partes. O documento enviado pode ser encontrado aqui no ficheiro [tahe.developpez.com.html].
Vamos agora solicitar o URL [http://sergetahe.com/cours-tutoriels-de-programmation]. Tínhamos visto que, para este URL, havia um redirecionamento para o URL e o [http://sergetahe.com/cours-tutoriels-de-programmation/] (com um / no final).
Os resultados na consola 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-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-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-tutoriais-de-programação/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/cursos-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 alojar o sergetahe.com, mantido intacto
- linha 2: utiliza-se a opção [--location] para indicar que se pretende seguir os redirecionamentos enviados pelo servidor;
- linha 13: o servidor indica que o documento solicitado mudou para URL;
- linha 18: indica o novo URL do documento solicitado;
- linha 31: o [curl] envia um novo pedido, desta vez para o novo URL;
- linha 36: o servidor responde novamente que o URL mudou;
- linha 41: o novo URL é exatamente igual ao que foi redirecionado, com uma única diferença: o protocolo mudou. Passou a ser HTTPS (linha 41), quando anteriormente era http (linha 31);
- linha 49: é enviada uma nova solicitação para o novo URL. Este está encriptado. Assim, inicia-se todo um diálogo de configuração da segurança, 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 ficheiro [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 as funcionalidades da ferramenta [curl] num programa Python. Instalamos este módulo:

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

O ficheiro [http/02/config] é o seguinte:
def configure():
# lista de URL a consultar
urls = [
# site: servidor ao qual se deve ligar
# timeout: tempo máximo de espera por uma resposta do servidor
# alvo: URL a solicitar
# 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"
}
]
# a configuração é devolvida
return {
'«urls»: URLs
}
O ficheiro contém uma lista de dicionários, cada um dos quais com 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 desligar-se-á;
- 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 armazena-a no ficheiro output/url['site'].html
# se [suivi=True], então existe um registo na consola da troca cliente/servidor
# url[timeout] é o tempo limite das chamadas do cliente;
# a URL [encoding] corresponde à codificação do documento solicitado
# recuperam-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}]")
# deixa-se 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 ligaçã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 os 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 estas configurações
curl.perform()
# criação do ficheiro server.html — alteração dos caracteres indesejáveis para um nome de ficheiro
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 ficheiro HTML
html.write(flux.getvalue().decode(encoding))
finally:
# libertação dos recursos
if curl:
curl.close()
if html:
html.close()
# -------------------principal
# configuração da aplicação
import config
config = config.configure()
# obter os URL do ficheiro de configuração
for url in config['urls']:
print("-------------------------")
print(url['site'])
print("-------------------------")
try:
# leitura de 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: importa-se o módulo [pycurl];
- linha 3: importa-se a classe [BytesIO], que nos permitirá armazenar os dados recebidos do servidor num fluxo binário;
- linhas 70-72: recuperamos a configuração da aplicação;
- 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 irá descarregar o URL URL com um tempo limite de [‘target’];
- linha 9: a função [get_url] recebe a configuração do URL a consultar;
- linhas 16-19: recupera-se a configuração do URL 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, pelo que são encaminhadas para o código chamador, que as intercepta;
- linha 28: prepara-se uma sessão [curl]. O [pycurl.Curl()] devolve um recurso [curl] que irá efetuar a transação com um servidor;
- linha 30: instância do fluxo binário que irá armazenar os dados recebidos;
- linhas 32-48: o dicionário [options] irá configurar a ligação [curl] ao servidor. A sua função está indicada nos comentários;
- linhas 49-51: as opções da ligação são transmitidas ao recurso [curl];
- linha 53: é solicitada a ligação ao URL com as opções definidas. Devido à opção [curl.WRITEDATA: flux] (linha 36), a função [curl.perform()] irá armazenar os dados recebidos em [flux];
- linhas 54-60: cria-se o ficheiro HTML, que irá armazenar o documento HTML recebido;
- linha 60: o fluxo binário [flux.getvalue()] será armazenado como uma cadeia de caracteres no ficheiro HTML. A codificação desta cadeia é 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 descodificação do fluxo binário falhará. A codificação é especificada no ficheiro de configuração do URL (na linha 12, por exemplo). Poderíamos ter gerido esta informação dinamicamente, uma vez que 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 pretendido com um navegador e consultar os cabeçalhos HTTP enviados por este no modo de depuração do navegador (F12) ou ainda o próprio documento, uma vez que este também especifica a codificação:


- linhas 61-66: os recursos alocados são libertados;
Ao executar o script [main.py], obtêm-se os seguintes resultados na consola:
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-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-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-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-tutoriais-de-programação/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/cursos-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, mantido 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
- a azul, os comandos HTTP enviados ao servidor;
- a 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 mudou de local. Linha 21, o novo URL;
- linha 32: é solicitada a URL [http://sergetahe.com/cours-tutoriels-de-programmation];
- linha 38: o servidor responde que a página foi movida. Linha 43, a nova URL;
- linha 54: é solicitada a URL [http://sergetahe.com/cours-tutoriels-de-programmation/];
- linha 60: o servidor responde que a página foi movida. Linha 65, a nova URL. Esta utiliza o protocolo seguro [HTTPS];
- linhas 71-75: o protocolo seguro é estabelecido com o servidor;
- linha 76: é solicitada a URL [https://sergetahe.com/cours-tutoriels-de-programmation/];
- linha 82: o documento solicitado foi encontrado;
21.4.6. Conclusão
Nesta secção, descobrimos o protocolo HTTP e escrevemos um script [http/02/main.py] capaz de descarregar um URL da Internet.
21.5. O protocolo SMTP (Simple Mail Transfer Protocol)
21.5.1. Introdução

Neste capítulo:
- O [Serveur B] será um servidor SMTP local que iremos instalar;
- [Client A] será um cliente SMTP de várias formas:
- o cliente [RawTcpClient] para descobrir o protocolo SMTP;
- um script Python que simula o protocolo SMTP do cliente [RawTcpClient];
- um script em Python que utiliza o módulo [smtplib], permitindo enviar todo o tipo de e-mails;
21.5.2. Criação de um endereço [gmail]
Para realizar os nossos testes SMTP, precisaremos de um endereço de e-mail para onde enviar mensagens. Para tal, vamos criar um endereço Gmail [https://www.google.com/intl/fr/gmail/about/]:

Nota: Envie alguns e-mails para o endereço que criou. Só avance para o passo seguinte quando tiver a certeza de que a conta criada consegue receber e-mails.
21.5.3. Instalação de um servidor SMTP
Para os nossos testes, iremos instalar o servidor de e-mail [hMailServer], que é simultaneamente 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. Permite, nomeadamente, gerir 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).

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

- em [1-2], selecione tanto o servidor de e-mail como as ferramentas para o administrar;
- durante a instalação, ser-lhe-á solicitada a palavra-passe do administrador: anote-a, pois será necessária;
O [hMailServer] instala-se como um serviço do Windows iniciado automaticamente ao arrancar o computador. É preferível escolher um arranque manual:
- no [3], digite [services] na área de entrada da barra de estado;

- 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]:

- no [2], na área de introdução de dados da barra de estado, digite [hmailserver];
- em [3], inicie o administrador;
- em [4], ligue o administrador ao servidor [hMailServer];
- No [5], introduza a palavra-passe definida durante a instalação do [hMailServer];
Se se esqueceu da palavra-passe, proceda da seguinte forma:
- encerre o servidor [hMailServer];
- abra o ficheiro [<hmailserver>/bin/hmailserver.ini], em que <hmailserver> é a pasta de instalação do servidor:

- no ficheiro [100], remova a palavra-passe da linha [AdministratorPassword]. Isto fará com que o administrador deixe de ter palavra-passe. Basta digitar [Entrée] quando lhe for solicitada;
ValidLanguages=english,swedish
[Security]
AdministratorPassword=
[Database]
Vamos continuar a configuração do servidor:

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

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

Vamos criar uma conta de utilizador:
- clicar com o botão direito do rato em [Accounts] (7) e, em seguida, (8) para adicionar um novo utilizador;
- no separador [General] (9), definimos um utilizador [guest] (10) com a palavra-passe [guest] (11). Este terá o endereço de e-mail [guest@localhost] (10);
- em [12], o utilizador [guest] está ativado;

- em [13-14], o utilizador é criado;

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

Fazemos o mesmo com o servidor POP3:

Repetimos o mesmo procedimento para o servidor IMAP:

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

- no [37], indique que o domínio predefinido do servidor SMTP é aquele que criou no [38];
Depois de guardar esta configuração, pode testá-la da seguinte forma. Abra um terminal PyCharm na pasta dos utilitários:

Em seguida, introduza 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: estabelece-se ligação à porta 25 da máquina [localhost]. É aí que funciona um servidor SMTP não seguro do servidor [hMailServer];
- linha 4: recebemos a mensagem de boas-vindas que configurámos na etapa 30 anterior;
O servidor SMTP está, portanto, devidamente instalado. Digite o comando [quit] para encerrar a comunicação com o servidor SMTP 25.
Agora, vamos fazer o mesmo com a porta 587, que é a porta predefinida do serviço seguro de recolha de correio SMTP:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 587
Client [DESKTOP-30FF5FB:50217] connecté au serveur [localhost-587]
Tapez vos commandes (quit pour arrêter) :
<-- [220 Bienvenue sur le serveur SMTP localhost.com]
- linha 4, a resposta do servidor SMTP a operar na porta 587;
Agora, vamos fazer o mesmo com a porta 110, que é a porta predefinida do serviço POP3 de retransmissão de e-mail:
(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]
- na linha 4, recebemos a mensagem de boas-vindas do servidor POP3;
Agora vamos fazer o mesmo com a porta 143, que é a porta predefinida do serviço IMAP de recolha de e-mail:
(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 leitor de e-mail
Para ler o e-mail que vamos enviar, precisamos de um leitor de e-mail. Para quem não tiver um, vamos mostrar como instalar e configurar o leitor [Thunderbird]:
- no [1]: descarregue o [thunderbird] e, em seguida, instale-o;

- inicie o servidor de e-mail [hMailServer], caso ainda não esteja em execução;
- em [2-3]: assim que o Thunderbird estiver em execução, vamos criar uma conta de e-mail para o utilizador [guest@localhost] do servidor de e-mail [hMailServer];



- em [7-11]: o servidor POP3, que nos permitirá ler o correio do servidor de e-mail [hMailServer], encontra-se na morada [localhost] e opera na porta 110;
- em [12-16]: o servidor SMTP, que nos permitirá enviar e-mails em nome dos utilizadores do servidor de e-mail [hMailServer], encontra-se na morada [localhost] e opera na porta 25;
- [18]: é possível testar a validação desta configuração;


- em [26]: como não existe encriptação em SSL, o Thunderbird avisa-nos de que a nossa configuração apresenta riscos;
- em [28]: a conta foi criada;
Para testar a conta criada, vamos, com o Thunderbird:
- enviar um e-mail para o utilizador [guest@localhost.com] (protocolo SMTP);
- ler o e-mail recebido por esse utilizador (protocolo POP3);

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

- em [8-9]: é recolhida a correspondência do utilizador [guest@localhost];
- em [10-15]: a mensagem recebida;
Vamos também enviar um e-mail ao utilizador [pymailparlexemple@gmail.com]. Vamos criar-lhe uma conta no Thunderbird para que possa ler o e-mail que irá receber:


- em [4]: insira o que quiser;
- em [5]: o endereço é [pymailparlexemple@gmail.com];
- em [6]: introduza a palavra-passe que atribuiu a este utilizador quando o criou;
- em [7]: confirme esta configuração;

- em [8]: o Thunderbird recuperou as seguintes informações da sua base de dados;
- em [9]: o protocolo de leitura do correio já não é POP3, mas sim IMAP. A principal diferença entre os dois é que o [POP3] transfere o e-mail lido para o computador local onde se encontra o leitor de e-mail e o elimina 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;

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

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

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

- em [32]: verifica-se o correio das diferentes contas;

- em [33-36]: o correio recebido pelo utilizador [pymailparlexemple@gmail.com]
Da mesma forma, criamos:
- uma nova conta Gmail [pymail2parlexemple@gmail.com];
- uma nova conta Thunderbird [pymail2parlexemple@gmail.com] para recolher as mensagens do utilizador com o mesmo nome:


Dispomos agora das ferramentas para explorar os protocolos SMTP, POP3 e IMAP. Começamos pelo protocolo SMTP.
21.5.5. O protocolo SMTP

Vamos descobrir o protocolo SMTP analisando os registos do servidor [hMailServer]. Para tal, ativamo-los com o comando [hmailServerAdministrator]:


- no [2], os registos estão ativados;
- no [3-5]: ativamo-los para os protocolos SMTP, POP3 e IMAP;
- no [7], solicita-se a visualização dos registos;
- no [8], abre-se o ficheiro de registos com qualquer editor de texto;

No exemplo que se segue, o cliente será [Thunderbird] e o servidor será [hMailServer]. No Thunderbird, certifique-se de que o utilizador [guest@localhost.com] se envie uma mensagem a si próprio:

Os registos são então os seguintes:
"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 gestor de e-mail 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 ligaçã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, indica o 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 carácter \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 terminou e que pode enviar um comando. A série de comandos [250] tinha como objetivo indicar ao cliente SMTP uma série de comandos que este poderia utilizar;
- linha 4: o cliente SMTP envia o comando [MAIL FROM : adresse_mail_expéditeur], que indica quem está a enviar 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]. Isto significa que vai enviar o conteúdo da mensagem;
- linha 9: o servidor SMTP indica, através da resposta [354 OK], que está pronto para receber a mensagem. O texto [send .] indica que o cliente SMTP deve terminar a sua mensagem com uma linha que contenha apenas um único ponto;
- o que não se vê a seguir é que o cliente SMTP envia a sua mensagem. Os registos não a mostram;
- linha 10: o cliente SMTP enviou o ponto que indica o fim da mensagem. O servidor SMTP responde-lhe que colocou a mensagem na fila (queued);
- o cliente SMTP envia-lhe o comando [QUIT] para indicar que vai encerrar a ligação;
- linha 12: o servidor responde-lhe;
Agora que conhecemos o diálogo cliente/servidor do protocolo SMTP, vamos tentar reproduzi-lo com o nosso cliente [RawTcpClient]. Utilizamos um terminal PyCharm:

Analisemos um novo exemplo:
- o cliente A será o cliente genérico TCP, [RawTcpClient];
- o servidor B será o servidor de e-mail [hMailServer];
- o cliente A solicitará ao servidor B que distribua um e-mail enviado pelo utilizador [guest@localhost.com] para si próprio;
- vamos verificar se o destinatário recebeu efetivamente o e-mail enviado;
Iniciamos o cliente da seguinte forma:
(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], ligamo-nos à porta 25 da máquina local, onde funciona o serviço SMTP de [hMailServer]. O argumento [--quit bye] indica que o utilizador sairá do programa ao digitar o comando [bye]. Sem este argumento, o comando para encerrar o programa é [quit]. No entanto, [quit] é também um comando do protocolo SMTP. Por isso, temos de evitar esta ambiguidade;
- na linha [2], o cliente está devidamente ligado;
- na linha [3], o cliente aguarda comandos introduzidos pelo teclado;
- na linha [4], o servidor envia-lhe a sua mensagem de boas-vindas;
Continuamos o diálogo da seguinte forma:
(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-lhe com uma sequência de mensagens do tipo [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 utilizador [guest@localhost.com];
- em [13], a resposta do servidor;
- em [14], o comando [DATA] indica ao servidor que o cliente vai 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 que contenha apenas um único ponto. A mensagem pode conter linhas [Subject:, From:, To:] (16-18) para definir, respetivamente, 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 que contém apenas um único ponto, indicando o fim da mensagem;
- em [23], assim que o servidor receber a linha que contém apenas um ponto, coloca a mensagem na fila;
- em [24], o cliente indica ao servidor que terminou;
- em [25], verifica-se que o servidor encerrou a ligação que o ligava ao cliente;
Agora, vamos verificar com o Thunderbird se o utilizador [guest@localhost.com] recebeu efetivamente a mensagem:

- em [1-6], vemos que o utilizador [guest@localhost.com] recebeu efetivamente a mensagem;
Por fim, o nosso cliente [RawTcpClient] conseguiu enviar uma mensagem através do servidor SMTP [localhost]. Agora, vamos utilizar 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: utilizamos o servidor SMTP do Gmail, que opera na porta 587;
- linha 15: ficamos bloqueados porque o servidor SMTP nos pede para iniciar uma ligação segura, o que não sabemos fazer. Ao contrário do exemplo anterior, o servidor [smtp.gmail.com] (linha 1) exige autenticação. Este só aceita como clientes os utilizadores registados no domínio [gmail.com]. Esta autenticação é segura e ocorre no âmbito de uma ligação encriptada.
O primeiro exemplo deu-nos as bases para criar um cliente SMTP básico em Python. O segundo mostrou-nos que alguns servidores SMTP (na verdade, a maioria) exigem uma autenticação efetuada através de uma ligação encriptada.
21.5.6. Scripts [smtp/01]: um cliente SMTP básico
Vamos reproduzir em Python o que aprendemos anteriormente sobre o protocolo SMTP.

O ficheiro [smtp/01/config] configura a aplicação da seguinte forma:
def configure() -> dict:
return {
# descrição: descrição do e-mail enviado
# smtp-server: servidor SMTP
# smtp-port: porta do servidor SMTP
# from: 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",
# enviamos o UTF-8
"content-type": 'text/plain; charset="utf-8"',
# estamos a testar 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 a enviar UTF-8
"Content-type": 'text/plain; charset="utf-8"',
# testamos os caracteres acentuados
"message": "aglaë séléné\nva au marché\nacheter des fleurs"
}
]
}
- linhas 10-35: uma lista de e-mails a enviar. Para cada um deles, especificam-se as seguintes informações:
- [description]: um texto que descreve o e-mail;
- [smtp-server]: o servidor SMTP a utilizar;
- [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 para o servidor SMTP smtpserver em nome do remetente
# para o destinatário. Se verbose=True, faz um acompanhamento das trocas cliente-servidor
# deixa que os erros do sistema sejam reportados
connexion = None
try:
# nome da máquina local (necessário para o protocolo SMTP)
client = socket.gethostbyaddr(socket.gethostbyname("localhost"))[0]
# abertura de uma ligação na porta 25 do smtpServer
connexion = socket.create_connection((mail["smtp-server"], 25))
#representa um fluxo de comunicação bidirecional
# entre o cliente (este programa) e o servidor SMTP contactado
# este canal é utilizado para a troca de comandos e informações
# após a ligaçã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 enviar
# deve conter as seguintes 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 ligação
if connexion:
connexion.close()
# --------------------------------------------------------------------------
def send_command(connexion: socket, commande: str, verbose: bool, with_rclf: bool):
# envia comando no canal de ligaçã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'))
# eventual resposta
if verbose:
affiche(commande, 1)
# leitura da resposta com menos de 1000 caracteres
reponse = str(connexion.recv(1000), 'utf-8')
# eco eventual
if verbose:
affiche(reponse, 2)
# recuperação do código de erro
codeErreur = int(reponse[0:3])
# erro devolvido 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 no ecrã?
# 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 ficheiro 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
# from: remetente
# para: destinatário
# assunto: assunto do e-mail
# mensagem: mensagem do e-mail
# protocolo de comunicação cliente-servidor SMTP
# -> o cliente liga-se à porta 25 do servidor SMTP
# <- o servidor envia-lhe 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 da sua mensagem e termina com uma linha que contém 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 por 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 da aplicação
import config
config = config.configure()
# os e-mails são processados um a um
for mail in config['mails']:
try:
# registos
print("----------------------------------")
print(f"Envoi du message [{mail['description']}]")
# preparação da mensagem a enviar
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 em 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
# e-mail seguinte
Comentários
- linhas 134-136: configura-se a aplicação;
- linhas 139-151: processam-se todos os e-mails encontrados na configuração;
- linhas 141-143: exibe-se o que se vai fazer;
- linhas 144-149: define-se a mensagem a enviar. A mensagem [message] é precedida pelos cabeçalhos [From, To, Subject, Content-type];
- linha 151: o envio do e-mail é assegurado pela função [sendmail], que aceita dois parâmetros:
- [mail]: o dicionário que contém as informações necessárias para o envio do e-mail;
- [verbose]: um valor booleano que indica se as interações cliente/servidor devem ou não ser registadas na consola;
- linhas 154-156: interceptam-se todas as exceções que saem da função [sendmail]. Estas são apresentadas;
- linha 6: [mail] é o dicionário que descreve o e-mail a enviar;
- linha 14: no protocolo SMTP, o cliente deve enviar o seu nome. Aqui, recupera-se o nome da máquina local que servirá de cliente;
- linha 16: ligação ao servidor SMTP, para o qual a mensagem será enviada;
- linhas 22-23: se a ligação tiver sido estabelecida com o servidor SMTP, este enviará uma mensagem de boas-vindas que é lida aqui;
- a função [sendmail] envia, em seguida, os vários comandos que um cliente SMTP deve enviar:
- linhas 24-25: o comando EHLO;
- linhas 26-27: o comando MAIL FROM:;
- linhas 28-29: a encomenda 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 num [try / finally] que permite que todas as exceções sejam reenviadas para o código chamador. Sabe-se que este as intercepta todas para as apresentar;
- linhas 48-50: libertação de recursos;
- linha 54: a função [send_command] é responsável por enviar os comandos do cliente para o servidor SMTP. Aceita quatro parâmetros:
- [connexion]: a ligação que liga o cliente ao servidor;
- [commande]: o comando a enviar;
- [verbose]: se TRUE, então as trocas entre cliente e servidor são registadas na consola;
- [with_rclf]: se TRUE, envia o comando terminado pela sequência \r\n. Isto é necessário para todos os comandos do protocolo SMTP, mas o [send_command] também serve para enviar a mensagem. Neste 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 cadeia de bytes UTF-8;
- linhas 70-71: leitura de todas as linhas da resposta. Presume-se que tenha menos de 1000 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 1.ª linha;
- linhas 78-80: se o código numérico XXX for superior a 500, então o servidor devolveu um erro. Nesse caso, é lançada uma exceção;
Resultados
A execução do script produz os seguintes resultados na consola:
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: a utilização do servidor SMTP [hMailServer] para enviar um e-mail para [guest@localhost] decorre sem problemas;
- linhas 32-46: a utilização dos servidores SMTP e [smtp.gmail.com] para enviar um e-mail para [pymailparlexemple@gmail.com] não decorre 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, previamente, autenticar-se através de uma ligação segura. O nosso cliente não o fez e, por isso, a ligação é recusada;
Os resultados no Thunderbird são os seguintes:

21.5.7. scripts [smtp/02]: um link SMTP criado com a biblioteca [smtplib]

O cliente anterior apresenta pelo menos duas falhas:
- não consegue utilizar uma ligação segura se o servidor a exigir;
- não consegue anexar ficheiros à mensagem;
Vamos resolver a primeira falha no script [smtp/02]. No nosso novo script, vamos utilizar o módulo Python [smtplib].
O script [smtp/02/main] utilizará o seguinte ficheiro 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
# from: 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",
# estamos a testar os 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",
# testamos os caracteres acentuados
"message": "aglaë séléné\nva au marché\nacheter des fleurs",
# SMTP com autenticação
"user": "pymail2parlexemple@gmail.com",
"password": "#6prIlh@1QZ3TG",
}
]
}
Encontram-se as mesmas rubricas que no ficheiro [smtp/01/config], com duas rubricas adicionais quando o servidor SMTP solicita uma autenticação:
- linha 31, [user]: o nome do utilizador que autentica a ligação;
- linha 32, [password]: a sua palavra-passe;
Estes dois campos só estão presentes se o servidor SMTP contactado exigir autenticação. A autenticação é então efetuada através de uma ligaçã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 para o servidor SMTP «smtpserver» em nome do remetente
# para o destinatário. Se verbose=True, monitoriza 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)
# ligação segura?
if "user" in mail:
# ligação segura
server.starttls()
# EHLO comando + autenticação
server.login(mail["user"], mail["password"])
# criação de uma mensagem Multipart — é esta 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"]
# envia-se a mensagem
server.send_message(msg)
# sair
server.quit()
# main ----------------------------------------------------------------
# as informações são obtidas de um ficheiro 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
# from: remetente
# para: destinatário
# assunto: assunto do e-mail
# content-type: codificação do e-mail
# mensagem: mensagem do e-mail
# configuração da aplicação
import config
config = config.configure()
# os e-mails são processados um a um
for mail in config['mails']:
try:
# registos
print("----------------------------------")
print(f"Envoi du message [{mail['description']}]")
# envio da mensagem em modo detalhado
sendmail(mail, True)
# fim
print("Message envoyé...")
except BaseException as erreur:
# exibe o erro
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
pass
# e-mail seguinte
Comentários
- linhas 8-35: apenas a função [sendmail] é utilizada. Passará a utilizar o módulo [smtplib] (linha 2);
- linha 16: ligação ao servidor SMTP;
- linha 18: se for [verbose=True], as trocas cliente/servidor serão apresentadas na consola;
- linhas 20-24: é efetuada a eventual autenticação, caso o servidor SMTP a exija;
- linha 22: a autenticação é efetuada através de uma ligação segura;
- linha 24: autenticação;
- linhas 26-33: envio da mensagem. O diálogo com o script [smtp/01/main] terá então início. Se tiver havido autenticação, decorrerá numa ligação segura;
- linha 35: termina-se o diálogo cliente/servidor;
Antes de executar o script [smtp/02/main], deve alterar a configuração da conta Gmail [pymailparlexemple@gmail.com]:
- inicie sessão na conta Gmail [pymailparlexemple@gmail.com];
- altere a seguinte configuração:
- no [2], autorize as aplicações menos seguras a aceder à conta;
Faça o mesmo com a segunda conta Gmail [pymail2parlexemple@gmail.com].
Resultados
Ao executar o script [smtp/02/main], obtêm-se os seguintes resultados na consola:
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 ligação encriptada com o servidor SMTP, algo que não se conseguiu fazer no script [smtp/main/01];
- de resto, encontram-se os comandos conhecidos do protocolo SMTP;
Se consultarmos a conta Gmail do utilizador [pymail2parlexemple], temos o seguinte:

21.5.8. scripts [smtp/03]: gestão de ficheiros anexados
Completamos o script [smtp/02/main] para que o e-mail enviado possa ter ficheiros anexados.

O script [smtp/03/main] é configurado pelo seguinte script [smtp/03/config]:
import os
def configure() -> dict:
# configuração da aplicação
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
# from: 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",
# estamos a testar os 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, é necessário indicar caminhos absolutos para os ficheiros anexados
"attachments": [
f"{script_dir}/attachments/fichier attaché.docx",
f"{script_dir}/attachments/fichier attaché.pdf",
]
}
]
}
O ficheiro [smtp/03/config] difere do ficheiro [smtp/02/config] utilizado anteriormente apenas pela presença opcional de uma lista [attachments] (linhas 30-32), que indica a lista de ficheiros a anexar à mensagem a enviar.
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, monitoriza as trocas entre cliente e servidor
# utiliza-se a biblioteca smtplib
# deixa-se que as exceções sejam propagadas
#
# o servidor SMTP
server = smtplib.SMTP(mail["smtp-server"])
# modo detalhado
server.set_debuglevel(verbose)
# ligação segura?
if "user" in mail:
server.starttls()
server.login(mail["user"], mail["password"])
# criação de uma mensagem Multipart — esta é 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
# deteta-se o tipo do ficheiro anexado
ctype, encoding = mimetypes.guess_type(path)
# se não tiver sido possível determinar
if ctype is None or encoding is not None:
# Não foi possível fazer uma estimativa, ou o ficheiro está codificado (comprimido), pelo que
# utiliza-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)
# tratam-se os diferentes casos
if maintype == 'text':
with open(path) as fp:
# Nota: devemos tratar do 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 utilizando Base64
encoders.encode_base64(part)
# Definir o parâmetro do nome do ficheiro
basename = os.path.basename(path)
part.add_header('Content-Disposition', 'attachment', filename=basename)
# anexamos o ficheiro à mensagem a enviar
msg.attach(part)
# Todos os anexos foram adicionados — enviamos a mensagem como uma cadeia de caracteres
server.send_message(msg)
# main ----------------------------------------------------------------
..
Comentários
- linhas 18-32: a função [sendmail] permanece tal como era quando não havia anexos;
- linha 35: o código que se segue foi retirado de uma documentação oficial do Python;
- linha 36: a mensagem que vai ser enviada incluirá várias partes: texto e ficheiros anexados. A isto chama-se uma mensagem [Multipart];
- linhas 37-40: na mensagem [Multipart] encontram-se os campos habituais de qualquer e-mail;
- linha 42: as diferentes partes da mensagem [Multipart] [msg] são anexadas à mensagem através do método [msg.attach] (linha 81). As partes anexadas podem ser de qualquer natureza. Estas são caracterizadas por um 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 enviar (linha 81);
- linha 44: [path] representa o caminho absoluto do ficheiro a anexar;
- linha 47: para determinar o tipo MIME a utilizar para a parte a anexar, vamos utilizar o sufixo (.docx, .php…) do ficheiro a anexar. O método [mimetypes.guess_type] realiza esta tarefa. Este método devolve duas informações:
- [ctype]: o tipo MIME do ficheiro;
- [encoding]: informação sobre a sua codificação;
- linhas 49-52: caso não seja possível determinar o tipo MIME do ficheiro, considera-se que se trata de um ficheiro binário (linha 52);
- linha 54: o tipo MIME de um ficheiro divide-se em tipo principal / tipo secundário, por exemplo, [application/pdf]. Estes dois elementos são separados;
- linhas 56-76: tratam-se diferentes casos consoante o valor do tipo principal MIME. Por exemplo, no caso [application/pdf] de um ficheiro PDF, executam-se as linhas 70-76:
- linhas 56-59: o caso em que o ficheiro anexado é um ficheiro de texto. Neste caso, cria-se um elemento do tipo [MIMEText] com conteúdo [fp.read];
- linhas 60-62: o caso em que o ficheiro contém uma imagem. Neste caso, cria-se um elemento do tipo [MIMEImage] com conteúdo [fp.read];
- linhas 63-65: o caso em que o ficheiro é um ficheiro de áudio. Neste caso, cria-se um elemento do tipo [MIMEAudio] com conteúdo [fp.read];
- linhas 66-69: o caso em que o ficheiro é um e-mail. Neste caso, cria-se um elemento do tipo [MIMEMessage] (linha 69) com o 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 ficheiro associado, aqui o conteúdo do elemento MIMEMessage é do tipo [email.message.Message];
- linhas 70-76: os outros casos. Isto inclui, por exemplo, os ficheiros Word e PDF do nosso exemplo;
- linha 72: o ficheiro a anexar é aberto em modo binário (rb=read binary);
- linha 74: o [fp.read] lê a totalidade do ficheiro binário;
- linhas 72-74: a estrutura [with open(…) as file] faz duas coisas:
- abre o ficheiro e atribui-lhe 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 incorporar na 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 incorporar na mensagem Multipart deve ter um conteúdo. Este pode ser inicializado com o método [set_payload];
- linhas 75-76: os ficheiros anexados devem ser codificados em 7 bits. Com efeito, historicamente, alguns servidores SMTP só suportavam caracteres codificados em 7 bits. Aqui, é utilizada a codificação denominada «Base64»;
- linha 77: a partir desta linha, o processamento aplica-se a todos os tipos MIME que criámos nas linhas 56-76 [MIMEMessage, MIMEImage, MIMEAudio, MIMEBase, MIMEText];
- linha 79: o elemento a adicionar à mensagem Multipart tem um cabeçalho que o descreve. Indica-se aqui que o elemento adicionado corresponde a um ficheiro anexo. O nome deste ficheiro é o terceiro parâmetro passado ao método [add_header]. O nome deste ficheiro é frequentemente utilizado pelos programas de e-mail para guardar, com esse nome, o ficheiro anexado no sistema de ficheiros do utilizador. Até agora, trabalhámos com o nome absoluto do ficheiro anexado. Aqui, passamos simplesmente o seu nome sem o caminho (linha 78);
- linha 81: o conteúdo binário do ficheiro é incorporado na mensagem [msg Multipart];
- linha 83: quando todas as partes da mensagem tiverem sido anexadas ao [msg Multipart], este é enviado;
Resultados
Se executarmos o script [smtp/03/main] com o ficheiro [smtp/02/config] já apresentado, a conta [pymail2parlexemple@gmail.com] recebe o seguinte:

Podemos ver os ficheiros anexados no [4, 9-11].
Vamos agora mostrar um exemplo com um e-mail em anexo. Vamos guardar o e-mail recebido no ficheiro [3] acima referido:

Guardamos o e-mail com o nome [mail attaché 1.eml] na pasta [smtp/03/attachments].
Vamos agora alterar o ficheiro [smtp/03/config] da seguinte forma:
import os
def configure() -> dict:
# configuração da aplicação
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",
# estamos a testar os 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, é necessário indicar caminhos absolutos para os ficheiros 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, adicionámos um anexo;
Agora, executamos novamente o script [smtp/03/main]. Isto produz o seguinte resultado na caixa de correio do utilizador [pymail2parlexemple@gmail.com]:

- em [1], o e-mail recebido;
- em [2]: o texto da mensagem;
- em [3]: o texto do e-mail em anexo;
- em [4]: o Thunderbird encontrou 5 anexos:
- [fichier attaché.docx];
- [fichier attaché.pdf];
- [mail attaché 1.eml]. Este anexo é, por sua vez, um e-mail que contém dois anexos:
- [fichier attaché.docx];
- [fichier attaché.pdf];
21.6. O protocolo POP3
21.6.1. Introdução
Para ler os e-mails armazenados num servidor de e-mail, existem dois protocolos:
- o protocolo POP3 (Post Office Protocol), historicamente o primeiro protocolo, mas atualmente pouco utilizado;
- o protocolo IMAP (Internet Message Access Protocol), um protocolo mais recente do que o POP3 e o mais utilizado atualmente;
Para explorar o protocolo POP3, vamos utilizar a seguinte arquitetura:

- O [Serveur B] será, consoante o caso:
- um servidor POP3 local, implementado pelo servidor de e-mail [hMailServer];
- o servidor [pop.gmail.com], que é o servidor POP3 do gestor de e-mails [gmail.com];
- o [Client A] será um cliente do POP3 de várias formas:
- o cliente [RawTcpClient] para descobrir o protocolo POP3;
- um script em Python que simula o protocolo POP3 do cliente [RawTcpClient];
- um script Python que utiliza módulos Python para gerir os anexos, bem como a utilização de uma ligação encriptada e autenticada quando o servidor POP3 assim o exige;
21.6.2. Descoberta do protocolo POP3
Tal como fizemos com o protocolo SMTP, vamos explorar o protocolo POP3 com base nos registos do servidor de e-mail [hMailServer]. Para tal, é necessário iniciar este servidor.
Com o Thunderbird, vamos:
- enviar um e-mail ao utilizador [guest@localhost.com];
- ler a caixa de correio desse utilizador;


No endereço [3-6] acima, a mensagem recebida pelo utilizador [guest@localhost.com].
Vamos agora analisar os registos do servidor [hMailServer]. Para tal, utilizamos a ferramenta de administração [hMailServer Administrator]:

Os registos do POP3 são os seguintes (as últimas linhas do ficheiro de registos 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 ligar;
- linha 2: o cliente envia o comando [CAPA] (capabilities) para solicitar a lista de comandos que pode utilizar;
- linha 3: o servidor responde-lhe que pode utilizar os comandos [USER, UIDL, TOP]. O servidor POP inicia as suas respostas com [+OK] ou [-ERR] para indicar se conseguiu ou não executar o comando do cliente;
- linha 4: o cliente envia o comando [USER guest] para indicar que pretende consultar a caixa de correio do utilizador [guest];
- linha 5: o servidor responde com [+OK] e solicita a palavra-passe de [guest];
- linha 6: o cliente envia o comando [PASS password] para enviar a palavra-passe do utilizador [guest]. Aqui, a palavra-passe está em texto simples, uma vez que o servidor POP3 não impôs uma ligação segura. Veremos que isto será diferente com o servidor POP3 do Gmail;
- linha 7: o servidor validou o conjunto de nome de utilizador e palavra-passe. Indica que está a bloquear a caixa de correio do utilizador [guest];
- linha 8: o cliente envia-lhe o comando [STAT], que solicita informações sobre a caixa de correio;
- linha 9: o servidor responde que existe uma mensagem de 612 octetos. Em geral, responde que existem N mensagens e indica o tamanho total dessas mensagens;
- linha 10: o cliente envia o comando [LIST]. Este comando solicita a lista de mensagens;
- linha 11: o servidor envia-lhe a lista de mensagens no seguinte formato:
- uma linha de resumo com o número de mensagens e o seu tamanho total;
- uma linha por mensagem, indicando o número da mensagem e o seu tamanho;
- linha 13: o cliente envia o comando [UIDL], que solicita a lista de mensagens com os respetivos identificadores. Com efeito, cada mensagem é identificada por um número único no serviço de e-mail;
- linha 14: a resposta do servidor. Vê-se, assim, que a mensagem n.º 1 da lista tem o identificador 42;
- linha 15: o cliente envia o comando [RETR 1], que solicita a transferência da mensagem n.º 1 da lista;
- linha 16: o servidor POP3 executa essa ação;
- linha 17: o cliente envia o comando [QUIT] para indicar que vai desligar-se do servidor POP3;
- linha 18: o servidor também vai encerrar a sua ligação com o cliente, mas antes envia-lhe uma mensagem de despedida;
Vamos agora reproduzir alguns elementos do diálogo acima utilizando o cliente [RawTcpClient] executado numa janela PyCharm:

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: abre-se uma ligação com a porta 110 da máquina [localhost]. É aí que funciona 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 aceder à caixa de correio do utilizador [guest];
- linha 7: enviamos a palavra-passe do utilizador [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: termina-se a sessão;
Eis um resumo de alguns comandos comuns aceites por um servidor POP3:
- o comando [USER] serve para definir o utilizador cuja caixa de correio se pretende consultar;
- o comando [PASS] serve para definir a sua palavra-passe;
- o comando [LIST] solicita a lista de mensagens presentes na caixa de correio do utilizador;
- O comando [RETR] solicita a visualização da mensagem cujo número é indicado;
- o comando [DELE] solicita a eliminação da mensagem cujo número é indicado;
- o comando [QUIT] indica ao servidor que o processo está concluído;
A resposta do servidor pode assumir várias formas:
- uma única linha que começa por [+OK] para indicar que o comando anterior do cliente foi bem-sucedido;
- uma única linha que começa por [-ERR] para indicar que o comando anterior do cliente falhou;
- várias linhas em que:
- a primeira linha começa por [+OK];
- a última linha é constituída por um único ponto;
21.6.3. scripts [pop3/01]: um cliente POP3 básico

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]. Terá o seguinte ficheiro de configuração [pop3/01/config.py]:
def configure() -> dict:
# as caixas de correio de onde se recolhem os e-mails
mailboxes = [
# servidor: servidor POP3
# porta: porta do servidor POP3
# utilizador: utilizador cujas mensagens se pretende ler
# password: a sua palavra-passe
# maxmails: o número máximo de e-mails a descarregar
# 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 são eliminados da caixa de correio
# assim que forem descarregados 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 consultar. Neste caso, existe 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: pretende-se ler a caixa de correio do utilizador [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 por uma resposta do servidor;
- linha 21: o tipo de codificação das mensagens lidas;
- linha 22: as mensagens descarregadas não serão eliminadas;
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, monitoriza as trocas entre cliente e servidor
…
# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
# envia o comando no canal de ligaçã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 liga-se à porta 110 do servidor SMTP
# <- o servidor envia-lhe uma mensagem de boas-vindas
# -> o cliente envia o comando USER utilizador
# <- 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 carácter.
# -> o cliente envia o comando DELE n.º para eliminar 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 é constituída por um único ponto
# as linhas de texto trocadas devem terminar com os caracteres RC(#13) e LF(#10)
#
# Recupera-se a configuração da aplicação
import config
config = config.configure()
# processam-se as caixas de correio uma a uma
for mailbox in config['mailboxes']:
try:
# exibição na consola
print("----------------------------------")
print(
f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
# leitura da caixa de correio 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á referimos, o [pop3/01/main.py] é uma adaptação do script [smtp/01/main.py] que já comentámos. Iremos comentar 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 aceder a essa caixa de correio encontram-se 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, monitoriza as trocas entre cliente e servidor
# isolam-se os parâmetros da caixa de correio
# presume-se que o dicionário [mailbox] é 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 reportados
connexion = None
try:
# abertura de uma ligação na porta [port] de [server] com um tempo limite de um segundo
connexion = socket.create_connection((server, port), timeout=timeout)
# a ligação representa um fluxo de comunicação bidirecional
# entre o cliente (este programa) e o servidor POP3 contactado
# este 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 1.ª linha para determinar o número de mensagens
match = re.match(r"^\+OK (\d+)", première_ligne)
nbmessages = int(match.groups()[0])
# iteramos 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)
# mensagem seguinte
imessage += 1
# comando QUIT
send_command(mailbox, connexion, "QUIT", verbose, True)
# fim
finally:
# encerramento da ligação
if connexion:
connexion.close()
Comentários
- linhas 8-14: recuperam-se as informações de configuração da caixa de correio a consultar;
- linhas 19-20: abertura de uma ligaçã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 utilizador cujos e-mails se pretendem obter;
- linhas 30-31: envia-se o comando [PASS] para indicar a palavra-passe desse utilizador;
- linhas 32-33: envia-se o comando [LIST] para saber quantos e-mails existem na caixa de correio desse utilizador. A função [sendCommand] devolve a primeira linha da resposta do servidor. Nesta, o servidor indica quantas mensagens existem 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 a eliminar, caso a configuração exija que as mensagens lidas sejam eliminadas do servidor (linhas 43-44);
- linhas 47-48: envia-se o comando [QUIT] para indicar ao servidor que o processo está concluído;
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 ligaçã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 socket como se fosse um ficheiro de texto
encoding = f"{mailbox['encoding']}" if mailbox['encoding'] else None
file = connexion.makefile(encoding=encoding)
# este ficheiro é processado linha a 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 linha seguinte
ligne_suivante = file.readline().strip()
# modo detalhado?
if verbose:
affiche(ligne_suivante, 2)
# última linha?
dernière_ligne = ligne_suivante == "."
# concluído - devolvemos a primeira linha
return première_ligne
Comentários
- linhas 13-18: o comando [command] só é enviado para o servidor POP3 se não estiver vazio. Este caso é necessário para ler a mensagem de boas-vindas do servidor POP3, que este envia mesmo que o cliente ainda não tenha enviado quaisquer comandos;
- linhas 19-21: lê-se o socket como se fosse um ficheiro de texto. Isto vai permitir-nos utilizar o método [readline] (linha 24) e, assim, ler a mensagem linha a linha. Utiliza-se a chave [encoding] do dicionário [mailbox] para indicar a codificação das linhas que vão ser lidas;
- linha 24: lemos a primeira linha da resposta;
- linhas 28-32: trata-se de um eventual erro. Estes 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, exibem-se todas as linhas recebidas (linhas 42-43) até receber a última linha (linha 45);
- linha 46: devolve-se a primeira linha lida, pois, no caso do comando [LIST], esta contém o número de mensagens presentes na caixa de correio;
Resultados
Tomemos o exemplo anterior. Com o Thunderbird, enviámos a seguinte mensagem ao utilizador [guest@localhost] (é necessário que o servidor hMailServer esteja em execução):

Ao executar, obtêm-se 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: a mensagem enviada para [guest@localhost] é recuperada corretamente.
Temos aqui um cliente POP3 básico, ao qual faltam algumas capacidades:
- a capacidade de comunicar com um servidor POP3 seguro;
- a capacidade de ler os anexos de uma mensagem;
Vamos implementar estas 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 gerir os anexos, bem como a comunicação com servidores seguros. Além disso, iremos guardar em ficheiros as mensagens e os respetivos anexos.
Vamos utilizar dois módulos Python:
- [poplib]: que irá assegurar o protocolo POP3;
- [email]: que agrupa vários submódulos que nos permitirão analisar as mensagens recebidas. Cada mensagem é uma cadeia de caracteres estruturada na qual é possível encontrar:
- os cabeçalhos da mensagem [From, To, Subject, Return-Path…];
- a mensagem nas suas versões de texto e, eventualmente, HTML;
- os anexos;

O script [inet/pop3/02/main] [1] é configurado pelo ficheiro [inet/pop3/02/config] [2] e utiliza o módulo [inet/shared/mail_parser] [3].
O ficheiro [pop3/02/config] é o seguinte:
import os
def configure() -> dict:
# configuração da aplicação
config = {
# lista de caixas de correio a gerir
"mailboxes": [
# servidor: servidor POP3
# porta: porta do servidor POP3
# utilizador: utilizador cujas mensagens se pretende ler
# password: a sua palavra-passe
# maxmails: o número máximo de e-mails a descarregar
# timeout: tempo máximo de espera por uma resposta do servidor
# delete: deve ser verdadeiro se for necessário eliminar do servidor as mensagens descarregadas
# ssl: definido como verdadeiro se a leitura dos e-mails for feita através de uma ligação segura
# output: a pasta onde as mensagens descarregadas são guardadas
{
"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 incluir 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 ficheiro define a lista de caixas de correio a consultar e define o Python Path da aplicação.
Aqui existe apenas uma caixa de correio:
- linhas 22-23: o utilizador cujos e-mails se pretende ler;
- linhas 20-21: o nome e a porta do servidor POP3 que armazena os e-mails deste utilizador;
- linha 24: o número máximo de e-mails a recuperar. De facto, se experimentar este script na sua própria caixa de correio, provavelmente não vai querer recuperar as centenas de e-mails que lá se encontram;
- linha 25: valor booleano que indica se, após a leitura de um e-mail, este deve ser eliminado (delete=True);
- linha 26: o atributo [ssl] definido como True significa que o servidor POP3, definido nas linhas 20-21, utiliza uma ligação encriptada;
- linha 27: o tempo máximo de espera pelas respostas do servidor, expresso em segundos;
- linha 28: a pasta onde guardar os e-mails lidos. Será criada caso não exista. Trata-se aqui de um nome relativo. Na execução, será relativa à pasta a partir da qual executar o script. Com [Pycharm], esta 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 correio
def readmails(mailbox: dict, verbose: bool):
# lê a caixa de correio descrita pelo dicionário [mailbox]
# se verbose=True, monitoriza as trocas entre cliente e servidor
…
# main ----------------------------------------------------------------
# cliente POP3 (Post Office Protocol) que permite ler e-mails
# recupera-se a configuração da aplicação
import config
config = config.configure()
# processam-se as caixas de correio uma a uma
for mailbox in config['mailboxes']:
try:
# exibição na consola
print("----------------------------------")
print(
f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
# leitura da caixa de correio 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 correio
def readmails(mailbox: dict, verbose: bool):
# lê a caixa de correio descrita pelo dicionário [mailbox]
# se verbose=True, monitoriza as trocas entre cliente e servidor
# importação de mail_parser
from mail_parser import save_message
# isolam-se os parâmetros da caixa de correio
# assume-se que o dicionário [mailbox] é 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']
# deixa-se que os erros do sistema sejam reportados
pop3 = None
try:
# criam-se as pastas de armazenamento, caso não existam
if not os.path.isdir(output):
os.mkdir(output)
# utilizador
dir2 = f"{output}/{user}"
# elimina-se a pasta [dir2], caso exista, e recria-se-a
if os.path.isdir(dir2):
# eliminação
shutil.rmtree(dir2)
# criação
os.mkdir(dir2)
# abertura de uma ligaçã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 ligação representa um fluxo de comunicação bidirecional
# entre o cliente (este programa) e o servidor POP3 contactado
# este canal é utilizado para a troca de comandos e informações
# modo detalhado
pop3.set_debuglevel(2 if verbose else 0)
# leitura da 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 se descodifica como uma cadeia de caracteres
desc = élément.decode()
# temos uma cadeia 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 encontram-se em «message» [1]
str_message = ""
for ligne in message[1]:
# uma linha é uma sequência de bytes que se descodifica como uma string
str_message += f"{ligne.decode()}\r\n"
# pasta da mensagem
dir3 = f"{dir2}/message_{num}"
# se a pasta não existir, é 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
# atingiu-se o limite máximo?
fini = imail == maxmails or imail == nb_mails
# pedido QUIT
pop3.quit()
finally:
# encerramento da sessã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 num try (linha 22)/finally (linha 88). Assim, todas as exceções são encaminhadas para o código principal, que as interrompe e as apresenta;
- 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, assim, sucessivamente as pastas [output] e, em seguida, [output/user]. Para criar esta última, deve-se primeiro eliminá-la na linha 31. [shutil] é um módulo que tem de ser importado. [shutil.rmtree(dir)] elimina a pasta [dir] e todo o seu conteúdo;
- para todas as operações nos ficheiros do sistema, utiliza-se o módulo [os], que também tem de ser importado;
- linhas 34-38: abre-se uma ligaçã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 registos:
- 0: sem registos;
- 1: os comandos emitidos pelo cliente POP3 são registados;
- 2: registos detalhados. Também se vê o que o cliente POP3 recebe;
- linha 47: após a ligaçã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). Vê-se que os dois primeiros elementos da tupla são bytes (prefixo b). lista[1] é um tabuleiro em que cada elemento é uma sequência de octetos contendo duas informações: o número da mensagem e o seu tamanho em octetos;
- linha 56: do que precede, deduz-se que o número de mensagens na caixa de correio pode ser obtido através de [email.message_from_bytes(data2[0][1])];
- linhas 59-84: percorre-se cada uma das mensagens. O processo termina quando todas tiverem sido lidas ou quando se atingir o número máximo de e-mails definido na configuração;
- linha 61: elemento atual da tabela liste[1], ou seja, algo como «b'1 584'», uma sequência de bytes;
- linha 63: converte-se a sequência de bytes numa cadeia de caracteres. Temos agora a cadeia '1 584';
- linha 66: recupera-se o número da mensagem, neste caso a cadeia «1»;
- linha 68: enviamos o comando POP3 RETR num. Obtemos 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)
- A mensagem é um tuplo de três elementos;
- message[1] é um tabular de linhas. Cada linha é uma sequência de octetos (prefixo b). A mensagem completa é formada por este 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. Estas 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. Temos agora a mensagem na forma de uma cadeia de caracteres. Esta mensagem pode conter outras mensagens, bem como anexos. Isto porque os anexos são incluídos na forma de uma cadeia de caracteres. Portanto, um ponto a reter é que um e-mail é, inicialmente, uma cadeia de caracteres e é essa cadeia 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 guardar 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 arquivado;
- O segundo parâmetro é do tipo [email.message.Message]. Este objeto dispõe de 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 dispor deste objeto. A função [email.message_from_string] permite criar um objeto [email.message.Message] a partir da cadeia de caracteres da mensagem;
A função [save_message] faz parte do módulo [mail_parser]:

O módulo [mail_parser] foi importado para as 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
# gravação de uma mensagem do tipo email.message.Message
# esta 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 guardar
# irfc822: número atual da numeração dos e-mails anexados
#
# parte da mensagem
part = email_message
# os cabeçalhos [From, To, Subject] encontram-se numa das partes multiparte
# ou numa parte [text/*], quando não existe uma parte [multipart]
keys = part.keys()
# «From» deve constar dos cabeçalhos; caso contrário, a parte não contém os cabeçalhos que procuramos
if "From" in keys:
# recuperam-se 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'))}"]
# guardar os cabeçalhos num ficheiro de texto
with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
# gravação no ficheiro
string = '\r\n'.join(headers)
file.write(f"{string}\r\n")
# tipo da parte [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 guardar a mensagem (2.º parâmetro);
- [email_message]: uma mensagem do tipo [email.message.Message]. Este tipo é um tipo estruturado. Contém o texto do e-mail, bem como todos os ficheiros anexados, e disponibiliza métodos para recuperar os seus diferentes elementos;
- [irfc822]: este parâmetro é utilizado 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 através 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 texto normal;
- do corpo da mensagem na forma de um texto HTML;
- de um anexo (com exceção de uma mensagem encapsulada, para a qual [part.ismultipart()] é igual a [True]);
- devido à natureza 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 a ser analisada (que, devido à recursividade, pode ser uma subparte da mensagem inicial);
- linhas 23-35: pretendemos registar os cabeçalhos:
- [From]: o remetente da mensagem;
- [To]: o destinatário da mensagem;
- [Subject]: o assunto da mensagem;
- [Return-Path]: o destinatário a quem se deve responder, caso se pretenda responder. Com efeito, esta informação nem sempre consta do [From];
- [User-Agent]: o cliente POP3 que comunica com o servidor POP3;
- [Date]: data de envio do e-mail;
- linha 23: apenas uma das partes de uma mensagem contém estes cabeçalhos. Para as outras partes, o código das linhas 23-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];
- este cabeçalho pode estar codificado. Se a codificação não for UTF-8, o cabeçalho é descodificado para ser recodificado em UTF-8 utilizando a função [decode_header];
- o primeiro cabeçalho terá o formato [From: pymail2lexemple@gmail.com];
- linhas 31-35: guardam-se os cabeçalhos no ficheiro [output/headers.txt];
A função [decode_header] é a seguinte (ainda no ficheiro [mail_parser.py]):
# descodificaçã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 — neste caso, terá apenas um elemento do tipo (header, encoding)
# se encoding==None, então header é uma cadeia 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: descodifica-se o cabeçalho:
- é necessário importar o módulo [email.header];
- obtém-se uma lista de tuplos [(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 a sua codificação:
- se [encoding==None], então [header] é o cabeçalho na forma de uma cadeia 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 devolve-se o cabeçalho;
- linhas 12-14: se houvesse codificação, então descodifica-se, numa cadeia de caracteres, a sequência de octetos que foi recuperada e devolve-se essa cadeia;
Voltemos à função [save_message]:
# guardar uma mensagem do tipo email.message.Message
# esta 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 gravação das mensagens
# email_message: a mensagem a guardar
# irfc822: número atual da numeração dos e-mails anexados
#
# parte da mensagem
part = email_message
# os cabeçalhos [From, To, Subject] encontram-se numa das partes multiparte
# ou numa parte [text/*], quando não existe uma parte [multipart]
keys = part.keys()
# «From» deve constar dos cabeçalhos; caso contrário, a parte não contém os cabeçalhos que procuramos
if "From" in keys:
# recuperam-se 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'))}"]
# guardar os cabeçalhos num ficheiro de texto
with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
# gravação no ficheiro
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 contentor de partes
elif part.is_multipart():
…
else:
…
# as outras partes são ignoradas (que não sejam text/plain, text/html ou anexos)
# retorna o valor atual de irfc822 (numeração dos e-mails anexados guardados na pasta de saída)
return irfc822
Comentários
- linhas 1-26: processámos os cabeçalhos da mensagem inicial;
- linhas 28-31: as partes de uma mensagem do tipo [email.message.Message] têm um tipo principal e um subtipo. Recuperamo-las;
- linhas 32-35: se a parte processada for do tipo [text/plain], significa que chegámos a uma folha da árvore da mensagem inicial. Trata-se do texto que o remetente escreveu na sua mensagem;
- linha 35: este texto é gravado num ficheiro:
- o primeiro parâmetro, [output], é a pasta na qual o texto deve ser guardado;
- o segundo parâmetro é a parte da mensagem que contém o texto a guardar;
- o terceiro parâmetro tem o valor 0 para guardar um texto normal, e 1 para um texto HTML;
- linhas 37-40: se a parte for do tipo [text/html], então também chegámos a uma folha da árvore da mensagem inicial. Trata-se do texto que o remetente escreveu na sua mensagem, desta vez no formato HTML. Nem todos os gestores de correio eletrónico suportam este formato;
A função [save_textmessage] é a seguinte:
# gravação 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 causou 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)
# conforme os tipos de texto
filename = None
if type_of_text == 0:
# gravação dos cabeçalhos
with codecs.open(f"{output}/headers.txt", "a", "utf-8") as file:
# gravação no ficheiro
string = '\r\n'.join(headers)
file.write(f"{string}\r\n")
# ficheiro de texto para o conteúdo
filename = f"{output}/mail.txt"
elif type_of_text == 1:
# ficheiro HTML para o conteúdo
filename = f"{output}/mail.html"
# guardar a mensagem
with codecs.open(filename, "w", "utf-8") as file:
# gravação num ficheiro
file.write(msg)
Comentários
- tal 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…). Esta é a codificação utilizada pelo gestor de correio que enviou a mensagem. É identificada através do cabeçalho [Content-Type] da mensagem recebida;
- uma segunda codificação a que o texto anterior pode ter sido submetido para ser enviado. É identificada pelo cabeçalho [Transfer-Content-Encoding] da mensagem recebida;
- linha 6: a codificação inicial do texto;
- linha 11: a segunda codificação a que o texto foi submetido para a sua transferência para o destinatário;
- linhas 9 e 13: estas duas informações são incluídas na lista [headers]. Serão adicionadas às informações do ficheiro [headers.txt], que regista determinados cabeçalhos da mensagem;
- linha 20: o [email.contentmanager.raw_data_manager.get_content] permite obter a mensagem com a sua codificação inicial 1. A codificação 2 foi eliminada. No entanto, o objeto [email.contentmanager.raw_data_manager] apenas suporta dois tipos de [Transfer-Content-Encoding]:
- [quoted-printable];
- [base64];
Ignora os restantes. No entanto, o Thunderbird, por exemplo, utiliza o [Transfer-Content-Encoding] denominado «8bit». Esta codificação é ignorada e as mensagens com caracteres acentuados ficam distorcidas. A mensagem pode então ser obtida através do método [part.get_payload()] (linhas 15-17);
- linha 21: nesta fase, temos a mensagem livre da sua 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 guardar uma mensagem de texto;
- linhas 24-28: guardam-se os dois cabeçalhos criados nas linhas 9 e 13 no ficheiro [headers.txt]. Este ficheiro já existe e contém cabeçalhos. Por isso, utiliza-se o modo «a» (linha 25) para abrir este ficheiro. «a» significa «append» e os novos cabeçalhos são adicionados (no final do ficheiro) ao conteúdo existente do ficheiro [headers.txt];
- linha 30: o nome do ficheiro no qual guardar a mensagem de texto;
- linha 33: o nome do ficheiro no qual se vai guardar a mensagem HTML;
- linhas 34-37: guarda-se o texto UTF-8 num ficheiro;
Voltemos à função [save_message]:
# gravação de uma mensagem do tipo email.message.Message
# esta 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 gravação das mensagens
# email_message: a mensagem a guardar
# irfc822: número atual da numeração dos e-mails anexados
#
# parte da mensagem
part = email_message
# os cabeçalhos [From, To, Subject] encontram-se numa das partes multiparte
# ou numa parte [text/*], quando não existe uma parte [multipart]
keys = part.keys()
# «From» deve constar dos cabeçalhos; caso contrário, a parte não contém os cabeçalhos que procuramos
if "From" in keys:
# recuperam-se 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'))}"]
# guardar os cabeçalhos num ficheiro de texto
with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
# gravação no ficheiro
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 contentor 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)
# gravação das subpartes da mensagem irfc822 na pasta «output2»
for subpart in part.get_payload():
# na nova pasta irfc822, recomeça do zero
save_message(output2, subpart, 0)
else:
# não se trata de um e-mail com anexo
# grava as 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)
# ignoram-se as outras partes (que não sejam text/plain, text/html ou anexo)
# retorna o valor atual de irfc822 (numeração dos e-mails com anexos guardados na pasta de saída)
return irfc822
Comentários
- linhas 33-40: tratámos dois casos possíveis de uma mensagem numa extremidade da árvore da mensagem inicial (sem subpartes). Ainda nos restam dois casos para tratar:
- linhas 43-62: o caso em que a parte analisada contém, por sua vez, subpartes (part.ismultipart()==True);
- linhas 63-68: para os restantes casos, tratamos apenas o caso em que a parte analisada é um anexo;
Vamos tratar deste último caso. Estamos, mais uma vez, numa extremidade da mensagem inicial (sem subpartes). Já nos deparámos com dois casos deste tipo: os tipos text/plain e text/html. Vamos agora tratar do caso do ficheiro anexado.
- linha 66: o anexo é identificado pela chave [Content-Disposition];
- linha 67: se esta chave existir e começar pela cadeia [attachment], então estamos perante um anexo à mensagem;
- linha 68: o anexo é guardado na pasta [output];
A função [save_attachment] é a seguinte:
# guardar um anexo
def save_attachment(output: str, part: email.message.Message):
# nome do ficheiro anexado
filename = os.path.basename(part.get_filename())
# o nome do ficheiro pode estar codificado
# por exemplo =?utf-8?Q?Cursos-Tutoriais-Serge-Tah=C3=A9-1568x268=2Ep
filename = decode_header(filename)
# guarda-se o ficheiro 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 ficheiro anexado é obtido através de [part.get_filename]. Guarda-se apenas o nome do ficheiro, não o seu caminho;
- linha 8: os nomes dos ficheiros são geralmente codificados, da mesma forma que os cabeçalhos da mensagem. Por isso, utiliza-se a função [decode_header] para os descodificar;
- linha 11: o conteúdo do ficheiro anexado é, por enquanto, uma cadeia de caracteres resultante da codificação (frequentemente base64) do conteúdo inicial do ficheiro. Para obter esse conteúdo inicial, utiliza-se a função [part.get_payload(decode=True)]. O parâmetro [decode=True] indica que o conteúdo do anexo deve ser descodificado. Obtém-se então uma sequência de bytes;
- linha 10: esta sequência de bytes é guardada no ficheiro [output/filename]. O modo «wb» de abertura do ficheiro 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:
# resultado: pasta de armazenamento das mensagens
# email_message: a mensagem a guardar
# irfc822: número atual da numeração dos e-mails anexados
#
# parte da mensagem
part = email_message
# os cabeçalhos [From, To, Subject] encontram-se numa das partes multiparte
# ou numa parte [text/*], quando não existe uma parte [multipart]
keys = part.keys()
# «From» deve constar dos cabeçalhos; caso contrário, a parte não contém os cabeçalhos que procuramos
if "From" in keys:
# recuperam-se 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'))}"]
# grava os cabeçalhos num ficheiro de texto
with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
# gravação no ficheiro
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 contentor de partes
elif part.is_multipart():
# caso específico do 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)
# gravação das subpartes da mensagem irfc822 na pasta «output2»
for subpart in part.get_payload():
# na nova pasta irfc822, o contador recomeça a 0
save_message(output2, subpart, 0)
else:
# não se trata de um e-mail com anexo
# grava as 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 devolve 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)
# ignoram-se as outras partes (que não sejam text/plain, text/html ou anexo)
# retorna o valor atual de irfc822 (numeração dos e-mails anexados guardados na pasta de saída)
return irfc822
Comentários
- tratámos 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 contentor de partes, ou seja, que 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 forma específica o caso em que a parte analisada tem o tipo [message/rfc822]. Este é 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)
# gravação das subpartes da mensagem irfc822 na pasta «output2»
for subpart in part.get_payload():
# na nova pasta irfc822, recomeça do zero
save_message(output2, subpart, 0)
else:
# não se trata de um e-mail com anexo
# grava as 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 devolve 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 multipartidas é 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], em que x é o número do e-mail anexado, 1 para o primeiro, 2 para o segundo…;
- linha 21: para as outras partes multipart, a pasta de gravação continua a ser a pasta [output] da mensagem inicial. Não se altera a pasta;
- linhas 10-12: cada subparte é guardada através de uma chamada recursiva a [save_message]. O terceiro parâmetro é o índice de numeração dos e-mails encapsulados em [subpart]. Inicialmente, este índice é igual a 0;
- linha 21: a mesma explicação que para a linha 12, mas o valor do terceiro parâmetro [irfc822] altera-se. Se, no ciclo das linhas 18-21, houver vários e-mails encapsulados, estes devem ser organizados em pastas […/rfc822-1…/rfc822_2…]. Assim, o terceiro parâmetro da função [save_message] deve assumir sucessivamente os valores 1, 2, 3… Para tal, a função [save_message] atribui o valor da função [irfc822] (linha 21).
Vejamos um exemplo e suponhamos 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]. Neste caso:
- (continuação)
- [subpart1] é guardado pela linha 21: a função [saveMessage] é executada com irfc822=0;
- [subpart1] é um e-mail com anexo, pelo que irfc822 passa para 1 (linha 6 do código). É criada uma pasta [output/irfc822_1]. O valor devolvido por [saveMessage(ouput,subpart1,0)] é, portanto, 1 (linha 23);
- [subpart2] é guardado na linha 21: a função [saveMessage] é executada com irfc822=1;
- [subpart2] não é um e-mail com anexo. Por isso, irfc822 permanece em 1. Este é o valor recuperado na linha 21;
- [subpart3] é guardado na linha 21: a função [save_message] é executada com irfc822=1;
- [subpart3] é um e-mail com anexo, pelo que irfc822 passa para 2 (linha 6 do código). É criada uma pasta [output/irfc822_2]. O valor devolvido por [save_message(ouput,subpart1,1)] é, portanto, 2 (linha 21);
- [subpart4] é guardado na linha 21: a função [save_message] é executada com irfc822=2;
- [subpart4] não é um e-mail com anexo. Por isso, irfc822 permanece em 2. Este é o valor recuperado na linha 21;
- [subpart5] é guardado na linha 21: a função [save_message] é executada com irfc822=2;
- [subpart5] é um e-mail com anexo, pelo que irfc822 passa para 3 (linha 6 do código). É criada uma pasta [output/irfc822_3]. O valor devolvido 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 «[acheter des légumes]». Pretendemos testar como são recuperados os caracteres acentuados.
Lemos-os com o script [pop3/02/main] configurado com o seguinte ficheiro [pop3/02/config]:
import os
def configure() -> dict:
# configuração da aplicação
config = {
# lista de caixas de correio a gerir
"mailboxes": [
# servidor: servidor POP3
# porta: porta do servidor POP3
# utilizador: utilizador cujas mensagens se pretende ler
# password: a sua palavra-passe
# maxmails: o número máximo de e-mails a descarregar
# timeout: tempo máximo de espera por uma resposta do servidor
# delete: deve ser verdadeiro se for necessário eliminar do servidor as mensagens descarregadas
# ssl: definido como verdadeiro se a leitura dos e-mails for feita através de uma ligação segura
# output: a pasta onde as mensagens descarregadas são guardadas
{
"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 incluir 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:

A mensagem 1 é a enviada pelo Thunderbird:

- 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 é a enviada pelo cliente:


Note-se que o [em Client] codifica os textos em UTF-8 ([4]) e que os transfere para o [quoted-printable] e o [5]. Também enviou uma cópia da mensagem em HTML e [7-8]. Todos os gestores de e-mail aqui testados são capazes de fazer isto. Trata-se de uma opção de configuração.
A mensagem 3 é a enviada pelo Gmail:

Note-se que o Gmail codifica os textos em UTF-8 ([3]) e os transfere em [quoted-printable] e [4]. Em [6], a versão HTML da mensagem.
A mensagem 4 é a enviada pelo Outlook:

Note-se que o Outlook codifica os textos em iso-8859-1 [3] e que os transfere para [quoted-printable] e [4].
Os exemplos anteriores mostram duas coisas:
- o nosso cliente [pop3/02] funcionou corretamente;
- os gestores de e-mail têm formas diferentes de enviar um e-mail;
Vejamos agora os ficheiros anexados. Com o Thunderbird, esvaziamos a caixa de correio do utilizador [pymail2parlexemple@gmail.com]. Em seguida, utilizamos 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 da aplicação
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
# from: 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",
# estamos a testar os 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, é necessário indicar caminhos absolutos para os ficheiros 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 ficheiro Word;
- um ficheiro PDF;
- um e-mail contendo os mesmos dois ficheiros em anexo;
Depois de enviar o e-mail, executamos o script [pop3/02] para ler a caixa de correio do utilizador [pymail2parlexemple@gmail.com]. Os resultados são os seguintes:

- em [1]: a mensagem com os seus dois ficheiros anexados;
- em [2]: o próprio e-mail anexado com os seus dois ficheiros anexados;
Conclusão
O módulo [mail_parser.py] é particularmente complexo. Isto deve-se à complexidade dos próprios e-mails. Vamos reutilizar este módulo para o protocolo IMAP.
21.7. O protocolo IMAP
21.7.1. Introdução
Para ler os e-mails armazenados num 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 do que o POP3 e o mais utilizado atualmente;
Para explorar o protocolo IMAP, vamos utilizar a seguinte arquitetura:

- O [Serveur B] será, consoante o caso:
- um servidor IMAP local, implementado pelo servidor de e-mail [hMailServer];
- o servidor [imap.gmail.com:993], que é o servidor IMAP do gestor de e-mails [Gmail];
- O [Client A] será um script Python que utiliza módulos Python para gerir os anexos, bem como para utilizar uma ligação encriptada 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, alterar ou eliminar essas pastas;
Vejamos um exemplo com o Thunderbird. Na seguinte arquitetura:

- o Thunderbird é o cliente A;
- o [imap.gmail.com] é o servidor B (Gmail);
Vamos criar uma pasta nos e-mails do utilizador [pymail2parlexemple@gmail.com] com o Thunderbird:

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

- em [7-8], movemos (com o rato) todos os ficheiros da pasta [Courrier entrant] para a pasta [dossier1];
Agora, acedemos ao site do Gmail e iniciamos sessão como o utilizador [pymail2parlexemple@gmail.com]:

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

- em [4-6]: os e-mails que foram movidos para a pasta [dossier1];
Estamos perante a seguinte arquitetura:

- O Cliente A é a aplicação Thunderbird;
- O Cliente C é a aplicação web do Gmail;
- O servidor B é o servidor IMAP do Gmail;
A árvore de pastas do utilizador é gerida pelo servidor IMAP. Em seguida, todos os clientes IMAP sincronizam-se com ele para apresentar ao utilizador as pastas da sua conta. Neste caso, 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]

O script [imap/main] é configurado pelo seguinte script [imap/config]:
import os
def configure() -> dict:
# configuração da aplicação
config = {
# lista de caixas de correio a gerir
"mailboxes": [
# servidor: servidor IMAP
# porta: porta do servidor IMAP
# utilizador: utilizador cujas mensagens se pretende ler
# password: a sua palavra-passe
# maxmails: o número máximo de e-mails a descarregar
# timeout: tempo máximo de espera por uma resposta do servidor
# delete: deve ser verdadeiro se for necessário eliminar do servidor as mensagens descarregadas
# ssl: definido como verdadeiro se a leitura dos e-mails for feita através de uma ligação segura
# output: a pasta onde as mensagens descarregadas são guardadas
{
"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 incluir 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 consultar;
- linha 20: o servidor IMAP;
- linha 21: a sua porta de serviço;
- linhas 22-23: o utilizador cujos e-mails se pretende ler;
- linha 24: o número máximo de e-mails que se pretende ler;
- linha 25: indica se é necessário estabelecer uma ligaçã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 da aplicação
import config
config = config.configure()
# processa-se as caixas de correio uma a uma
for mailbox in config['mailboxes']:
try:
# exibição na consola
print("----------------------------------")
print(
f"Lecture de la boîte mail POP3 {mailbox['user']} / {mailbox['server']}:{mailbox['port']}")
# leitura da caixa de correio
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 o procedimento já visto 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
# recuperar 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:
# criam-se as pastas de armazenamento, caso não existam
if not os.path.isdir(output):
os.mkdir(output)
# utilizador
dir2 = f"{output}/{user}"
# elimina-se a pasta [dir2], caso exista, e recria-se-a
if os.path.isdir(dir2):
# eliminação
shutil.rmtree(dir2)
# criação
os.mkdir(dir2)
# ligação ao 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 desta 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 a 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"type={typ2}, data={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» entende-se o texto da mensagem + todos os ficheiros 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, é criada
if not os.path.isdir(dir3):
os.mkdir(dir3)
# guarda-se
save_message(dir3, message)
# mensagem seguinte
imail += 1
fini = imail >= maxmails or imail >= len(nums)
finally:
if imap_resource:
# encerra-se a ligação à caixa de correio
imap_resource.close()
# desligamo-nos do servidor IMAP
imap_resource.logout()
Comentários
- linhas 7-15: recuperam-se os elementos da configuração;
- linhas 19, 79: o código é controlado por um try / finally. Assim, não se interceptam as exceções (ausência da cláusula except), que são então encaminhadas para o código chamador, que as interrompe e as apresenta;
- linhas 23-30: cria-se a pasta de armazenamento dos e-mails;
- linhas 31-35: estabelece-se ligação ao servidor IMAP. A classe utilizada varia consoante se trate 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: efetua-se a autenticação junto do servidor IMAP;
- linhas 41-42: vimos que a caixa de correio de um utilizador IMAP pode ser organizada em pastas. A pasta [INBOX] é a dos e-mails recebidos. Para selecionar a pasta [dossier1], escrever-se-ia [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 várias formas de o expressar. O critério [ALL] significa que se pretendem todas as mensagens da pasta;
O resultado de [imapResource.search] é semelhante a isto:
typ=OK, data=[b'1 2']
[data] é uma lista que contém os números das mensagens obtidas. Estes estão em binário. Acima, foram encontradas duas mensagens na pasta [INBOX];
- linha 49: recuperam-se 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 ciclo para ler as mensagens da pasta [INBOX];
- linhas 54-55: n.º da mensagem;
- linhas 58-59: a mensagem n.º [num] é solicitada ao servidor IMAP;
- o primeiro parâmetro é o número da mensagem pretendida;
- o segundo parâmetro é uma cadeia de caracteres «(part1)(part2)…», em que [parti] é o nome de uma parte da mensagem. Não aprofundei este ponto. O nome (RFC822) designa a totalidade do e-mail;
Recebemos algo com o 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 desta tupla é uma cadeia binária que representa a totalidade da mensagem solicitada. Reconhecem-se 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 numa forma binária.
- 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. Esta função foi descrita no parágrafo |pop3/02/main|;
- linhas 76-78: repetimos o ciclo para processar a mensagem seguinte;
- linhas 79-84: quer tenha ocorrido um erro ou não:
- linha 82: encerra-se a ligação com a pasta consultada;
- linha 84: desligamo-nos do servidor IMAP;
Os resultados obtidos são idênticos aos obtidos com o script [pop3/02/main]. Isto é normal, uma vez que é utilizado o mesmo analisador de e-mail [mail_parser].