16. Funções de rede
Vamos agora abordar as funções de rede do PHP, que nos permitem realizar programação TCP/IP (Protocolo de Controlo de Transmissão/Protocolo de Internet).

16.1. Noções básicas de programação para a Internet
16.1.1. Visão geral
Considere a comunicação entre duas máquinas remotas, A e B:

Quando uma aplicação AppA na máquina A pretende comunicar com uma aplicação AppB na máquina B através da 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 utilizada pela aplicação AppB. Com efeito, a máquina B pode hospedar muitas aplicações em execução na Internet. Quando recebe informações da rede, deve saber a que aplicação se destinam essas informações. As aplicações na máquina B acedem à rede através de interfaces também conhecidas como portas de comunicação. Esta informação está contida no pacote recebido pela máquina B, para que possa ser entregue à aplicação correta;
- os protocolos de comunicação compreendidos pela máquina B. No nosso estudo, utilizaremos apenas protocolos TCP-IP;
- o protocolo de comunicação suportado pela aplicação AppB. De facto, as máquinas A e B irão «comunicar» entre si. O que trocarem será encapsulado nos protocolos TCP/IP. No entanto, quando, no final da cadeia, a aplicação AppB recebe a informação enviada pela aplicação AppA, deve ser capaz de a interpretar. Isto é análogo à situação em que duas pessoas, A e B, comunicam por telefone: a sua conversa é transportada pelo telefone. A fala é codificada como uma série de sinais pelo telefone A, transmitida através das linhas telefónicas, e chega ao telefone B para ser descodificada. A pessoa B ouve então a fala. É aqui que entra em jogo o conceito de protocolo de comunicação: se A fala francês e B não compreende essa língua, A e B não conseguirão comunicar eficazmente;
Por conseguinte, as duas aplicações em comunicação devem chegar a acordo quanto ao tipo de diálogo que irão utilizar. 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. Possuem um protocolo de diálogo diferente;
16.1.2. 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 dados estabelece primeiro uma ligação com o processo que irá receber a informação que está prestes a transmitir. Esta ligação é estabelecida entre uma porta na máquina emissora e uma porta na máquina recetora. É assim criado um caminho virtual entre as duas portas, que será reservado exclusivamente para os 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 parece contínua. O processo de envio envia informação ao seu próprio ritmo. Esta informação não é necessariamente enviada imediatamente: o protocolo TCP aguarda até ter informação suficiente para enviar. É armazenada numa estrutura denominada segmento TCP. Assim que este segmento estiver cheio, é transmitido para a camada IP, onde é encapsulado num pacote IP;
- Cada segmento enviado pelo protocolo TCP é numerado. O protocolo TCP de receção verifica se está a receber os segmentos em sequência. Por cada segmento recebido corretamente, envia um aviso de receção ao remetente;
- quando o remetente recebe este aviso, notifica o processo de envio. O processo de envio pode assim confirmar que um segmento chegou em segurança;
- se, após um determinado período de tempo, o protocolo TCP que enviou um segmento não receber um aviso de receção, retransmite o segmento em questão, garantindo assim a qualidade do serviço de entrega de informação;
- o circuito virtual estabelecido entre os dois processos em comunicação é full-duplex: isto significa que a informação pode fluir em ambas as direções. Assim, o processo de destino pode enviar confirmações mesmo enquanto o processo de origem continua a enviar informação. Isto permite, por exemplo, que o protocolo TCP de origem envie múltiplos segmentos sem esperar por uma confirmação. Se, após um determinado período de tempo, verificar que não recebeu um aviso de receção para um segmento específico n.º n, retomará o envio de segmentos a partir desse ponto;
16.1.3. A relação cliente-servidor
A comunicação pela Internet é frequentemente assimétrica: a máquina A inicia uma ligação para solicitar um serviço à máquina B, especificando que pretende abrir uma ligação com o serviço SB1 na máquina B. A máquina B aceita ou recusa. Se aceitar, a máquina A pode enviar os seus pedidos ao serviço SB1. Estes pedidos devem estar em conformidade com o protocolo de comunicação compreendido pelo serviço SB1. Estabelece-se assim um diálogo de pedido-resposta entre a máquina A, conhecida como máquina cliente, e a máquina B, conhecida como máquina servidor. Um dos dois parceiros encerrará a ligação.
16.1.4. Arquitetura do Cliente
A arquitetura de um programa de rede que solicita os serviços de uma aplicação de servidor será a seguinte:
ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
tant que ce n'est pas fini
préparer une demande
l'émettre vers la machine B
attendre et récupérer la réponse
la traiter
fin tant que
finsi
fermer la connexion
16.1.5. Arquitetura do servidor
A arquitetura de um programa que oferece serviços será a seguinte:
ouvrir le service sur la machine locale
tant que le service est ouvert
se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que
O programa servidor trata o pedido de ligação inicial de um cliente de forma diferente dos seus pedidos de serviço subsequentes. O programa não presta o serviço por si próprio. Se o fizesse, deixaria de estar à escuta de pedidos de ligação enquanto o serviço estivesse em curso, e os clientes não seriam atendidos. Por isso, procede de forma diferente: assim que um pedido de ligação é recebido na porta de escuta e depois aceite, o servidor cria uma tarefa responsável por prestar o serviço solicitado pelo cliente. Este serviço é prestado noutra porta da máquina do servidor, denominada porta de serviço. Isto permite que vários clientes sejam atendidos ao mesmo tempo.
Uma tarefa de serviço terá a seguinte estrutura:
tant que le service n'a pas été rendu totalement
attendre une demande sur le port de service
lorsqu'il y en a une, élaborer la réponse
transmettre la réponse via le port de service
fin tant que
libérer le port de service
16.2. Saiba mais sobre os protocolos de comunicação da Internet
16.2.1. Introdução
Quando um cliente se liga a um servidor, estabelece-se um diálogo entre ambos. A natureza desse diálogo constitui o que se designa por protocolo de comunicação do servidor. Entre os protocolos de Internet mais comuns encontram-se os seguintes:
- HTTP: Protocolo de Transferência de Hipertexto – o protocolo utilizado para comunicar com um servidor web (servidor HTTP);
- SMTP: Simple Mail Transfer Protocol – o protocolo para comunicar com um servidor de envio de e-mail (servidor SMTP);
- POP: Post Office Protocol – o protocolo para comunicar com um servidor de armazenamento de e-mail (servidor POP). Este é utilizado para recuperar e-mails recebidos, não para os enviar;
- IMAP: Internet Message Access Protocol – o protocolo para comunicar com um servidor de armazenamento de e-mail (servidor IMAP). Este protocolo tem vindo a substituir gradualmente o antigo protocolo POP;
- FTP: File Transfer Protocol — o protocolo para comunicação com um servidor de armazenamento de ficheiros (servidor FTP);
Todos estes protocolos são baseados em texto: o cliente e o servidor trocam linhas de texto. Se tiver um cliente capaz de:
- estabelecer uma ligação com um servidor TCP;
- exibir as linhas de texto enviadas pelo servidor na consola;
- enviar as linhas de texto que um utilizador digitaria no teclado para o servidor;
então podemos comunicar com um servidor TCP utilizando um protocolo baseado em texto, desde que conheçamos as regras desse protocolo.
16.2.2. Utilitários TCP

No código associado a este documento, existem dois utilitários de comunicação TCP:
- [RawTcpClient] permite-lhe ligar-se à porta P de um servidor S;
- [RawTcpServer] cria um servidor que escuta clientes na porta P;
O servidor TCP [RawTcpServer] é chamado utilizando a sintaxe [RawTcpServer porta] para criar um serviço TCP na porta [porta] da máquina local (o computador em que está a trabalhar):
- o servidor pode atender vários clientes simultaneamente;
- o servidor executa 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-name=y]. O campo [id] é utilizado para identificar os clientes;
- send x [text]: envia texto para o cliente #x (id=x). Os colchetes [] não são enviados. São obrigatórios no comando. São utilizados para delimitar visualmente o texto enviado ao cliente;
- close x: encerra a ligação com o cliente #x;
- quit: encerra todas as ligações e interrompe o serviço;
- As linhas enviadas pelo cliente para o servidor são exibidas na consola;
- Todas as trocas são registadas num ficheiro de texto denominado [machine-portService.txt], onde
- [machine] é o nome da máquina na qual o código está a ser executado;
- [port] é a porta de serviço que responde aos pedidos do cliente;
O cliente TCP [RawTcpClient] é chamado utilizando a sintaxe [RawTcpClient servidor porta] para se ligar à porta [porta] no servidor [servidor]:
- as linhas digitadas pelo utilizador no teclado são enviadas para o servidor;
- as linhas enviadas pelo servidor são exibidas na consola;
- Toda a comunicação é registada num ficheiro de texto denominado [server-port.txt];
Vejamos um exemplo. Abra duas janelas do Prompt de Comando do Windows e aceda à pasta «utilities» em cada uma delas. Numa das janelas, inicie o servidor [RawTcpServer] na porta 100:

- em [1], estamos na pasta utilitários;
- em [2], iniciamos o servidor TCP na porta 100;
- em [3], o servidor aguarda um cliente TCP;
- em [4], o servidor aguarda um comando introduzido pelo utilizador através do teclado;
Na outra janela de comando, iniciamos o cliente TCP:

- Em [5], estamos na pasta de utilitários;
- em [6], iniciamos o cliente TCP: indicamos-lhe para se ligar à porta 100 na máquina local (aquela em que está a trabalhar);
- em [7], o cliente conectou-se com sucesso ao servidor. Os detalhes do cliente são exibidos: ele está na máquina [DESKTOP-528I5CU] (a máquina local neste exemplo) e usa a porta [50405] para comunicar com o servidor:
- Em [8], o cliente aguarda um comando introduzido pelo utilizador através do teclado;
Voltemos à janela do servidor. O seu conteúdo mudou:

- Em [9], foi detetado um cliente. O servidor atribuiu-lhe o número 1. O servidor identificou corretamente o cliente remoto (máquina e porta);
- em [10], o servidor volta a aguardar um novo cliente;
Voltemos à janela do cliente e enviemos um comando ao servidor:

- em [11], o comando enviado ao servidor;
Voltemos à janela do servidor. O seu conteúdo mudou:

- em [12], entre parênteses retos, a mensagem recebida pelo servidor;
Vamos enviar uma resposta ao cliente:

- em [13], a resposta enviada ao cliente 1. Apenas o texto entre os parênteses é enviado, não os próprios parênteses;
Vamos voltar à janela do cliente:

- em [14], a resposta recebida pelo cliente. O texto recebido é aquele entre colchetes;
Voltemos à janela do servidor para ver outros comandos:

- em [15], solicitamos a lista de clientes;
- em [16], a resposta;
- em [17], encerramos a ligação com o cliente n.º 1;
- em [18], a confirmação do servidor;
- em [19], desligamos o servidor;
- em [20], a confirmação do servidor;
Voltemos à janela do cliente:

- em [21], o cliente detectou o fim do serviço;
Foram criados dois ficheiros de registo, um para o servidor e outro para o cliente:

- em [25], os registos do servidor: o nome do ficheiro é o nome do cliente [máquina-porta];
- em [26], o cliente regista: o nome do ficheiro é o nome do servidor [máquina-porta];
Os registos do servidor são os seguintes:
Os registos do cliente são os seguintes:
16.3. Como encontrar 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. No entanto, em última análise, apenas o endereço IP é utilizado. Por isso, por vezes é necessário saber o endereço IP de um computador identificado pelo seu nome.
O script [ip-01.php] é o seguinte:
<?php
// strict adherence to declared function parameter types
declare (strict_types=1);
//
// error management
error_reporting(E_ALL & E_STRICT);
ini_set("display_errors", "on");
//
// constants
$HOTES = array("istia.univ-angers.fr", "www.univ-angers.fr", "www.ibm.com", "localhost", "", "xx");
// IP addresses and $HOTES machine names
for ($i = 0; $i < count($HOTES); $i++) {
getIPandName($HOTES[$i]);
}
// end
print "Terminé\n";
exit;
//------------------------------------------------
function getIPandName(string $nomMachine): void {
//$nomMachine: name of the machine whose address is required IP: name of the machine whose address is required IP: name of the machine whose address is required
//
// nomMachine-->adresse IP
$ip = gethostbyname($nomMachine);
print "---------------\n";
if ($ip !== $nomMachine) {
print "ip[$nomMachine]=$ip\n";
// address IP --> nomMachine
$name = gethostbyaddr($ip);
if ($name !== $ip) {
print "name[$ip]=$name\n";
} else {
print "Erreur, machine[$ip] non trouvée\n";
}
} else {
print "Erreur, machine[$nomMachine] non trouvée\n";
}
}
Comentários
- linhas 7-8: instruímos o PHP a reportar todos os erros (E_ALL e E_STRICT) e a exibi-los. Este modo é recomendado apenas no modo de desenvolvimento para melhorar o código utilizando os avisos do PHP. No modo de produção, na linha 8, definiríamos isso como «off». Desde o PHP 5.4, o nível E_STRICT está incluído no E_ALL;
- Linha 11: A lista de máquinas para as quais queremos o nome e o endereço IP;
As funções de rede do PHP são utilizadas na função getIpandName na linha 21.
- linha 25: a função gethostbyname($name) recupera o endereço IP "ip3.ip2.ip1.ip0" da máquina denominada $name. Se a máquina $name não existir, a função devolve $name como resultado;
- Linha 30: A função gethostbyaddr($ip) recupera o nome da máquina associado ao endereço IP $ip no formato "ip3.ip2.ip1.ip0". Se a máquina $ip não existir, a função devolve $ip como resultado;
Resultados:
---------------
ip[istia.univ-angers.fr]=193.49.144.41
name[193.49.144.41]=ametys-fo-2.univ-angers.fr
---------------
ip[www.univ-angers.fr]=193.49.144.41
name[193.49.144.41]=ametys-fo-2.univ-angers.fr
---------------
ip[www.ibm.com]=2.18.220.211
name[2.18.220.211]=a2-18-220-211.deploy.static.akamaitechnologies.com
---------------
ip[localhost]=127.0.0.1
name[127.0.0.1]=DESKTOP-528I5CU
---------------
ip[]=192.168.1.38
name[192.168.1.38]=DESKTOP-528I5CU.home
---------------
Erreur, machine[xx] non trouvée
Terminé
16.4. O HTTP (HyperText Transfer Protocol)
16.4.1. Exemplo 1

Quando um navegador exibe um URL, ele atua como cliente de um servidor web, ou, por outras palavras, de um servidor HTTP. Ele 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:

Em seguida, utilizando um navegador, solicitamos o URL [localhost:100], o que significa que especificamos que o servidor HTTP que estamos a consultar está a funcionar na porta 100 da máquina local:

Voltemos à janela do servidor:

- em [3], o cliente que se conectou;
- em [4-7], a série de linhas de texto que enviou:
- em [4]: esta linha tem o formato [GET URL HTTP/1.1]. Solicita a URL / e instrui o servidor a utilizar o protocolo HTTP 1.1;
- em [5]: esta linha tem o formato [Host: servidor:porta]. Não importa se o comando [Host] está em maiúsculas ou minúsculas. Note-se que o cliente está a consultar um servidor local a operar na porta 100;
- o comando [User-Agent] identifica o cliente;
- o comando [Accept] especifica quais os tipos de documentos aceites pelo cliente;
- o comando [Accept-Language] especifica o idioma em que se pretende receber os documentos solicitados, caso estes existam em vários idiomas;
- o comando [Connection] especifica o modo de ligação pretendido: [keep-alive] indica que a ligação deve ser mantida até que a troca esteja concluída;
- em [7]: o cliente termina os seus comandos com uma linha em branco;
Terminamos a ligação desligando o servidor:

16.4.2. Exemplo 2
Agora que conhecemos os comandos enviados por um navegador para solicitar um URL, vamos solicitar este URL utilizando o nosso cliente TCP [RawTcpClient]. O servidor Apache do Laragon será o nosso servidor web.
Vamos iniciar o Laragon e, em seguida, o servidor web Apache:


Agora, utilizando um navegador, vamos aceder ao URL [http://localhost:80]. Aqui, especificamos apenas o servidor [localhost:80] e nenhum URL de documento. Neste caso, é solicitado o URL /, ou seja, a raiz do servidor web:

- em [1], a URL solicitada. Inicialmente, digitámos [http://localhost:80], e o navegador (Firefox, neste caso) simplesmente converteu-a para [localhost], porque o protocolo [http] é implícito quando nenhum protocolo é especificado, e a porta [80] é implícita quando a porta não é especificada;
- em [2], a página raiz / do servidor web consultado;
Agora, vamos ver o texto recebido pelo navegador:

- Clique com o botão direito do rato na página e selecione a opção [2]. Verá 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.0i PHP/7.2.11<br />
PHP version: 7.2.11 <span><a title="phpinfo()" href="/?q=info">info</a></span><br />
Document Root: C:/myprograms/laragon-lite/www<br />
</div>
<div class="opt">
<div><a title="Getting Started" href="https://laragon.org/docs">Getting Started</a></div>
</div>
</div>
</div>
</body>
</HTML>
Agora vamos solicitar a URL [http://localhost:80] utilizando o nosso cliente TCP:
![]()
- Em [1], ligamo-nos à porta 80 no servidor localhost. É aqui que o servidor web Laragon é executado;
Agora digitamos os comandos que descobrimos no parágrafo anterior:

- em [1], o comando [GET]. Solicitamos o diretório raiz / do servidor web;
- em [2], o comando [Host];
- estes são os únicos dois comandos essenciais. Para os outros comandos, o servidor web utilizará valores predefinidos;
- em [3], a linha em branco que deve encerrar os comandos do cliente;
- abaixo da linha 3 vem a resposta do servidor web;
- em [4] até à linha em branco [5] encontram-se os cabeçalhos HTTP da resposta do servidor;
- após a linha [5] vem o documento HTML solicitado [6];
Digitamos [quit] para sair do cliente e descarregar o ficheiro de registo [localhost-80.txt]:
- Linhas 11–79: o documento HTML recebido. No exemplo anterior, o Firefox recebeu o mesmo;
Agora temos o básico para programar um cliente TCP que solicite um URL.
16.4.3. Exemplo 3

O script [http-01.php] é um cliente HTTP configurado pelo ficheiro JSON [config-http-01.json]. O conteúdo do ficheiro é o seguinte:
- linha 2: o nome da máquina que hospeda o servidor web a ser acedido;
- linha 3: a porta na qual este servidor web opera;
- linha 4: o URL do documento pretendido;
- linha 5: a máquina de destino no formato máquina:porta;
- linha 6: a identificação do cliente HTTP: pode introduzir o que quiser;
- linha 7: o tipo de documento aceite pelo cliente, neste caso texto HTML;
- linha 8: o idioma pretendido para o documento solicitado;
- linha 9: o caractere de fim de linha para comandos enviados pelo cliente: isto pode variar dependendo se o servidor está a ser executado numa máquina Unix (\n) ou numa máquina Windows (\r\n);
O script [http-01.php] é o seguinte:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
//
// error management
// error_reporting(E_ALL & E_STRICT);
// ini_set("display_errors", "on");
//
// constants
const CONFIG_FILE_NAME = "config-http-01.json";
//
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// otain the HTML text from the URL configuration file
foreach ($config as $site => $protocole) {
// read site index page $ite
$résultat = getURL($site, $protocole);
// result display
print "$résultat\n";
}//for
// end
exit;
//-----------------------------------------------------------------------
function getURL(string $site, array $protocole, $suivi = TRUE): string {
// reads the URL $site["GET"] and stores it in the $site.HTML file
// client/server dialog is based on the $protocole protocol
//
// open a connection on the $site port
$erreurNumber = 0;
$erreur = "";
$connexion = fsockopen($site, $protocole["port"], $erreurNumber, $erreur);
// return if error
if ($connexion === FALSE) {
return "Echec de la connexion au site (" . $site . " ," . $protocole["port"] . " : $erreur";
}
// $connexion represents a bidirectional communication flow
// between the client (this program) and the contacted web server
// this channel is used for the exchange of orders and information
// the dialog protocol is HTTP
//
// creation of the $site.HTML file
$HTML = fopen("output/$site.HTML", "w");
if ($HTML === FALSE) {
// close client/server connection
fclose($connexion);
// error return
return "Erreur lors de la création du fichier $site.HTML";
}
// the client will start the HTTP dialog with the server
if ($suivi) {
print "Client : début de la communication avec le serveur [$site] ----------------------------\n";
}
// depending on the server, client lines must end with \nor \r\n
$endOfLine = $protocole["endOfLine"];
// for simplicity's sake, we don't test for errors in client/server communication
// the customer sends the GET command to request the URL $protocole["GET"]
// syntax GET URL HTTP/1.1
$commande = "GET " . $protocole["GET"] . " HTTP/1.1$endOfLine";
// followed?
if ($suivi) {
print "--> $commande";
}
// send the command to the server
fputs($connexion, $commande);
// issue other headers HTTP
foreach ($protocole as $verb => $value) {
if ($verb !== "GET" && $verb != "port"" && $verb !="endOfLine") {
// we build the
$commande = "$verb: $value$endOfLine";
// followed?
if ($suivi) {
print "--> $commande";
}
// send the command to the server
fputs($connexion, $commande);
}
}
// protocol HTTP headers must end with an empty line
fputs($connexion, $endOfLine);
//
// the server will now respond on channel $connexion. It will send all
// then close the channel. The client therefore reads everything that arrives from $connexion
// until the channel closes
//
// we first read the HTTP headers sent by the server
// they also end with an empty line
if ($suivi) {
print "Réponse du serveur [$site] ----------------------------\n";
}
$fini = FALSE;
while (!$fini && $ligne = fgets($connexion, 1000)) {
// is there an empty line?
$champs = [];
preg_match("/^(.*?)\s+$/", $ligne, $champs);
if ($champs[1] !== "") {
if ($suivi) {
// header HTTP is displayed
print "<-- " . $champs[1] . "\n";
}
} else {
// this was the empty line - HTTP headers are finished
$fini = TRUE;
}
}
// we read the HTML document that will follow the empty line
while ($ligne = fgets($connexion, 1000)) {
// we save the line in the HTML file on the site
fputs($HTML, $ligne);
}
// the server has closed the connection - the client closes it in turn
fclose($connexion);
// close file $HTML
fclose($HTML);
// return
return "Fin de la communication avec le site [$site]. Vérifiez le fichier [$site.HTML]";
}
Comentários do código:
- linha 14: o ficheiro de configuração é utilizado para criar um dicionário:
- as chaves do dicionário são os servidores web a consultar;
- os valores especificam o protocolo HTTP a utilizar;
- linhas 16–21: percorremos a lista de servidores web na configuração;
- Linha 26: A função getURL($site, $protocol, $log) recupera um documento do site $site e guarda-o no ficheiro de texto $site.HTML. Por predefinição, as interações cliente/servidor são registadas na consola ($log=TRUE);
- Linha 33: A função fsockopen($site,$port,$errNumber,$error) cria uma ligação com um serviço TCP/IP em execução na porta $port na máquina $site. Se a ligação falhar, [$errNumber] é um código de erro e [$error] é a mensagem de erro associada. Assim que a ligação cliente/servidor estiver aberta, muitos serviços TCP/IP trocam linhas de texto. É o que acontece aqui com o HTTP (HyperText Transfer Protocol). O fluxo de dados do servidor para o cliente pode então ser tratado como um ficheiro de texto lido utilizando [fgets]. O mesmo se aplica ao fluxo de dados do cliente para o servidor, que pode ser gravado utilizando [fputs];
- linhas 44–50: criação do ficheiro [$site.HTML] no qual o documento HTML recebido será armazenado;
- linha 60: o primeiro comando do cliente deve ser [GET URL HTTP/1.1];
- linha 66: a função fputs permite que o cliente envie 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 utilizar a versão 1.1 do HTTP»;
- linhas 68–79: são enviadas as outras linhas do protocolo HTTP [Host, User-Agent, Accept, Accept-Language]. A sua ordem não importa;
- Linha 81: É enviada uma linha vazia ao servidor para indicar que o cliente terminou de enviar os seus cabeçalhos HTTP e está agora à espera do documento solicitado;
- linhas 92–106: O servidor enviará primeiro uma série de cabeçalhos HTTP que fornecem vários detalhes sobre o documento solicitado. Estes cabeçalhos terminam com uma linha vazia;
- linha 93: uma linha enviada pelo servidor é lida utilizando a função PHP [fgets];
- linha 96: recuperamos o corpo da linha sem os espaços (espaços em branco, caracteres de fim de linha) no final da linha;
- linha 97: verificamos se recuperámos a linha vazia que marca o fim dos cabeçalhos HTTP enviados pelo servidor;
- linhas 98–101: se estiver no modo [trace], o cabeçalho HTTP recebido é exibido na consola;
- linhas 108–111: as linhas de texto da resposta do servidor podem ser lidas linha a linha utilizando um ciclo while e guardadas no ficheiro de texto [output/$site.HTML]. Quando o servidor web tiver enviado toda a página solicitada, encerra a sua ligação com o cliente. No lado do cliente, isto será detetado como um fim de ficheiro;
Resultados:
A consola apresenta os seguintes registos:
Client : début de la communication avec le serveur [localhost] ----------------------------
--> GET / HTTP/1.1
--> Host: localhost:80
--> User-Agent: client PHP
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [localhost] ----------------------------
<-- HTTP/1.1 200 OK
<-- Date: Thu, 16 May 2019 15:43:18 GMT
<-- Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
<-- X-Powered-By: PHP/7.2.11
<-- Content-Length: 1781
<-- Content-Type: text/HTML; charset=UTF-8
Fin de la communication avec le site [localhost]. Vérifiez le fichier [localhost.HTML]
No nosso exemplo, 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.0i PHP/7.2.11<br />
PHP version: 7.2.11 <span><a title="phpinfo()" href="/?q=info">info</a></span><br />
Document Root: C:/myprograms/laragon-lite/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.
16.4.4. Exemplo 4
Neste exemplo, vamos demonstrar que o cliente HTTP que escrevemos é insuficiente. Modifique o ficheiro de configuração [config-http-01.json] da seguinte forma:
Aqui, vamos solicitar a URL [http://tahe.developpez.com:443/]. A porta 443 na máquina [tahe.developpez.com] é uma porta utilizada para o protocolo HTTP seguro, conhecido como HTTPS. Neste protocolo, a interação cliente-servidor começa com uma troca de informações que protege a ligação. O cliente deve, portanto, utilizar o protocolo [HTTPS] em vez do protocolo [HTTP], o que o nosso cliente não faz.
Com este ficheiro de configuração, a saída da consola é a seguinte:
Client : début de la communication avec le serveur [tahe.developpez.com] ----------------------------
--> GET / HTTP/1.1
--> Host: sergetahe.com:443
--> User-Agent: script PHP 7
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [tahe.developpez.com] ----------------------------
<-- HTTP/1.1 400 Bad Request
<-- Date: Fri, 17 May 2019 13:02:26 GMT
<-- Server: Apache/2.4.25 (Debian)
<-- Content-Length: 454
<-- Connection: close
<-- Content-Type: text/HTML; charset=iso-8859-1
Fin de la communication avec le site [tahe.developpez.com]. Vérifiez le fichier [output/tahe.developpez.com.HTML]
- linha 8: o servidor [tahe.developpez.com] respondeu que o pedido do cliente estava incorreto;
O conteúdo do ficheiro [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.25 (Debian) Server at 2eurocents.developpez.com Port 443</address>
</body></HTML>
O servidor indica claramente que não utilizámos o protocolo correto.
Agora, vamos utilizar o seguinte ficheiro de configuração:
A saída da consola é a seguinte:
Client : début de la communication avec le serveur [sergetahe.com] ----------------------------
--> GET /cours-tutoriels-de-programmation/ HTTP/1.1
--> Host: sergetahe.com:80
--> User-Agent: script PHP 7
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [sergetahe.com] ----------------------------
<-- HTTP/1.1 200 OK
<-- Date: Fri, 17 May 2019 13:36:06 GMT
<-- Content-Type: text/HTML; charset=UTF-8
<-- Transfer-Encoding: chunked
<-- Server: Apache
<-- X-Powered-By: PHP/7.0
<-- Vary: Accept-Encoding
<-- Set-Cookie: SERVERID68971=2621207|XN64y|XN64y; path=/
<-- Cache-control: private
<-- X-IPLB-Instance: 17106
Fin de la communication avec le site [sergetahe.com]. Vérifiez le fichier [output/sergetahe.com.HTML]
- A linha 11 indica que o servidor está a enviar o documento em partes;
Isto faz com que apareçam números no fluxo de dados enviado ao cliente: cada número indica ao cliente o número de caracteres do próximo bloco enviado pelo servidor. Eis como fica no ficheiro [output/sergetahe.com.HTML]:

- [1] e [2] representam o tamanho hexadecimal dos blocos 1 e 2 do documento;
Um cliente HTTP adequado não deve deixar estes números no documento HTML final.
Eis outro exemplo:
É semelhante ao exemplo anterior, mas o URL solicitado na linha 4 não termina com um /. Estes não são os mesmos URLs. A execução do cliente HTTP produz então a seguinte saída na consola:
Client : début de la communication avec le serveur [sergetahe.com] ----------------------------
--> GET /cours-tutoriels-de-programmation HTTP/1.1
--> Host: sergetahe.com:80
--> User-Agent: script PHP 7
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [sergetahe.com] ----------------------------
<-- HTTP/1.1 301 Moved Permanently
<-- Date: Fri, 17 May 2019 13:47:00 GMT
<-- Content-Type: text/HTML; charset=iso-8859-1
<-- Content-Length: 262
<-- Server: Apache
<-- Location: http://sergetahe.com:80/cours-tutoriels-de-programmation/
<-- Set-Cookie: SERVERID68971=2621207|XN67V|XN67V; path=/
<-- Cache-control: private
<-- X-IPLB-Instance: 17095
Fin de la communication avec le site [sergetahe.com]. Vérifiez le fichier [output/sergetahe.com.HTML]
- A linha 8 indica que o documento solicitado alterou o seu URL. O novo URL é indicado na linha 13. Repare que, desta vez, o novo URL termina com o caractere /;
O ficheiro [output/serge.tahe.com.HTML] fica então da seguinte forma:
<!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="http://sergetahe.com/cours-tutoriels-de-programmation/">here</a>.</p>
</body></HTML>
Um cliente HTTP deve ser capaz de seguir redirecionamentos. Neste caso, deve solicitar automaticamente o novo URL [http://sergetahe.com/cours-tutoriels-de-programmation/].
16.4.5. Exemplo 5
Os exemplos anteriores mostraram-nos que o nosso cliente HTTP era insuficiente. Vamos agora apresentar uma ferramenta chamada [curl] que permite recuperar documentos web enquanto lida com os desafios mencionados: protocolo HTTPS, documentos enviados em pedaços, redirecionamentos… A ferramenta [curl] foi instalada com o Laragon:

Vamos abrir um terminal Laragon [1]:

No terminal, digite o seguinte comando:

- em [1], o tipo de consola;
- em [2], o diretório atual. Este diretório é especial: é onde o servidor Apache do Laragon recupera os documentos que lhe são solicitados. Por isso, evitaremos sobrecarregar este diretório;
- em [3], o comando introduzido;
O comando [curl --help] pode gerar um erro. A causa mais provável é que não tenha o tipo de terminal correto. Neste caso, abra outro terminal com os comandos [4-6];
O comando [curl --help] exibe todas as opções de configuração do [curl]. Existem dezenas delas. Iremos utilizar muito poucas. Para solicitar um URL, basta digitar o comando [curl URL]. Este comando irá exibir o documento solicitado na consola. Se também quiser ver as trocas HTTP entre o cliente e o servidor, digite [curl --verbose URL]. Por fim, para guardar o documento HTML solicitado num ficheiro, digite [curl --verbose --output file URL].
Para evitar sobrecarregar a pasta [www] do Laragon, vamos mudar para outro local no sistema de ficheiros:

- em [1], navegue até à pasta [c:\temp]. Se esta pasta não existir, pode criá-la ou escolher outra;
- em [2], crie uma pasta chamada [curl];
- em [3], aceda a ela;
- em [4], liste o seu conteúdo. Está vazio;
Certifique-se de que o servidor Apache do Laragon está em execução e, utilizando o [curl], solicite o URL [http://localhost/] com o comando [curl –verbose –output localhost.HTML http://localhost/]. Obterá os seguintes resultados:
c:\Temp\curl
λ 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
* Connected to localhost (::1) port 80 (#0)
> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 17 May 2019 14:32:47 GMT
< Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
< X-Powered-By: PHP/7.2.11
< Content-Length: 1781
< Content-Type: text/HTML; charset=UTF-8
<
{ [1781 bytes data]
100 1781 100 1781 0 0 14248 0 --:--:-- --:--:-- --:--:-- 14248
* Connection #0 to host localhost left intact
- linhas 8-12: linhas enviadas pelo [curl] para o servidor [localhost]. O protocolo HTTP é reconhecido;
- linhas 13–19: linhas enviadas em resposta pelo servidor;
- linha 13: indica que o documento solicitado foi recebido com sucesso;
O ficheiro [localhost.HTML] contém o documento solicitado. Pode verificar isto abrindo o ficheiro num editor de texto.
Agora vamos solicitar a URL [https://tahe.developpez.com:443/]. Para recuperar esta URL, o cliente HTTP deve suportar HTTPS. É o caso do cliente [curl].
A saída da consola é a seguinte:
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
* 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-lite\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):
{ [108 bytes data]
* TLSv1.2 (IN), TLS handshake, Certificate (11):
{ [2558 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]
* 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 http/1.1
* Server certificate:
* subject: CN=*.developpez.com
* start date: Apr 4 08:25:09 2019 GMT
* expire date: Jul 3 08:25:09 2019 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]
< HTTP/1.1 200 OK
< Date: Fri, 17 May 2019 14:39:41 GMT
< Server: Apache/2.4.25 (Debian)
< X-Powered-By: PHP/5.3.29
< Vary: Accept-Encoding
< Transfer-Encoding: chunked
< Content-Type: text/HTML
<
{ [6 bytes data]
100 96559 0 96559 0 0 163k 0 --:--:-- --:--:-- --:--:-- 163k
* Connection #0 to host tahe.developpez.com left intact
- linhas 10–40: trocas entre cliente e servidor para proteger a ligação: esta será encriptada;
- linhas 42–45: os cabeçalhos HTTP enviados pelo cliente [curl] ao servidor;
- linha 48: o documento solicitado foi encontrado;
- linha 53: o documento é enviado em blocos;
O [curl] lida corretamente tanto com o protocolo HTTPS seguro como com o facto de o documento ser enviado em partes. O documento enviado pode ser encontrado aqui no ficheiro [tahe.developpez.com.HTML].
Agora, vamos solicitar a URL [http://sergetahe.com/cours-tutoriels-de-programmation]. Vimos que, para esta URL, havia um redirecionamento para a URL [http://sergetahe.com/cours-tutoriels-de-programmation/] (com um / no final).
A saída da consola é a seguinte:
c:\Temp\curl
λ curl --verbose --output sergetahe.com.HTML --location http://sergetahe.com/cours-tutoriels-de-programmation
% 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: Fri, 17 May 2019 15:13:03 GMT
< Content-Type: text/HTML; charset=iso-8859-1
< Content-Length: 262
< Server: Apache
< Location: http://sergetahe.com/cours-tutoriels-de-programmation/
< Set-Cookie: SERVERID68971=2621207|XN7Pg|XN7Pg; path=/
< Cache-control: private
< X-IPLB-Instance: 17095
<
* Ignoring the response-body
{ [262 bytes data]
100 262 100 262 0 0 1401 0 --:--:-- --:--:-- --:--:-- 1401
* Connection #0 to host sergetahe.com left intact
* Issue another request to this URL: 'http://sergetahe.com/cours-tutoriels-de-programmation/'
* Found bundle for host sergetahe.com: 0x1c88548 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) with host sergetahe.com
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
> GET /cours-tutoriels-de-programmation/ HTTP/1.1
> Host: sergetahe.com
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 17 May 2019 15:13:04 GMT
< Content-Type: text/HTML; charset=UTF-8
< Transfer-Encoding: chunked
< Server: Apache
< X-Powered-By: PHP/7.0
< Vary: Accept-Encoding
< Set-Cookie: SERVERID68971=2621207|XN7Pg|XN7Pg; path=/
< Cache-control: private
< X-IPLB-Instance: 17095
<
{ [14205 bytes data]
100 43101 0 43101 0 0 78795 0 --:--:-- --:--:-- --:--:-- 168k
* Connection #0 to host sergetahe.com left intact
- linha 2: a opção [--location] é utilizada para indicar que pretendemos seguir os redirecionamentos enviados pelo servidor;
- linha 13: o servidor indica que o documento solicitado alterou o seu URL;
- linha 18: indica a nova URL do documento solicitado;
- linha 27: o [curl] envia um novo pedido para a nova URL;
- linha 33: a nova URL é utilizada;
- linha 38: o servidor responde que encontrou o documento solicitado;
- linha 41: envia-o em partes;
O documento solicitado será encontrado no ficheiro [sergetahe.com.HTML].
16.4.6. Exemplo 6
O PHP possui uma extensão chamada [libcurl] que permite utilizar as funcionalidades da ferramenta [curl] num programa PHP. Primeiro, certifique-se de que esta extensão está ativada no ficheiro [php.ini], conforme descrito na secção de links:

Certifique-se de que a linha 889 acima não está comentada.
Vamos escrever um script [http-02.php] que utilizará o seguinte ficheiro de configuração JSON:
Cada entrada [chave, valor] no dicionário tem a seguinte estrutura:
- chave: o nome de um servidor web;
- valor é um dicionário com as seguintes chaves:
- timeout: tempo máximo de espera pela resposta do servidor. Após este tempo, o cliente irá desligar-se;
- url: URL do documento solicitado;
O código do script [http-02.php] é o seguinte:
<?php
// strict adherence to declared types of function parameters
declare (strict_types=1);
//
// error management
//error_reporting(E_ALL & E_STRICT);
//ini_set("display_errors", "on");
//
// constants
const CONFIG_FILE_NAME = "config-http-02.json";
//
// we retrieve the configuration
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// get the HTML text from the URL in the configuration file
foreach ($config as $site => $infos) {
// reading URL from site $ite
$résultat = getUrl($site, $infos["url"], $infos["timeout"]);
// result display
print "$résultat\n";
}//for
// end
exit;
//-----------------------------------------------------------------------
function getUrl(string $site, string $url, int $timeout, $suivi = TRUE): string {
// reads the URL $url and stores it in the file output/$site.HTML
//
// follow-up
print "Client : début de la communication avec le serveur [$site] ----------------------------\n";
// Session initialization cURL
$curl = curl_init($url);
if ($curl === FALSE) {
// there has been an error
return "Erreur lors de l'initialisation de la session cURL pour le site [$site]";
}
// curl options
$options = [
// verbose mode
CURLOPT_VERBOSE => true,
// new connection - no cache
CURLOPT_FRESH_CONNECT => true,
// request timeout (in seconds)
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => $timeout,
// do not check the validity of SSL certificates
CURLOPT_SSL_VERIFYPEER => false,
// track redirects
CURLOPT_FOLLOWLOCATION => true,
// retrieve the requested document as a character string
CURLOPT_RETURNTRANSFER => true
];
// curl settings
curl_setopt_array($curl, $options);
// Executing the request
$page_content = curl_exec($curl);
// Close session cURL
curl_close($curl);
// income statement
if ($page_content !== FALSE) {
// save result in $site.HTML
$result = file_put_contents("output/$site.HTML", $page_content);
if ($result === FALSE) {
// error return
return "Erreur lors de la création du fichier [output/$site.HTML]";
}
// successful comeback
return "Fin de la communication avec le serveur [$site]. Vérifiez le fichier [output/$site.HTML]";
} else {
// there has been a communication error
return "Erreur de communication avec le serveur [$site]";
}
}
Comentários
- linha 14: usamos o ficheiro de configuração para criar o dicionário [$config];
- linhas 17–22: percorremos a lista de sites encontrados na configuração;
- linha 19: para cada site, chamamos a função [getUrl], que irá descarregar o URL $infos["url"] com um tempo limite de $infos["timeout"];
- linha 34: iniciamos uma sessão [curl]. [curl_init] ainda não se liga ao servidor web. Devolve um recurso [$curl] que servirá de parâmetro para todas as funções [curl] subsequentes;
- linhas 35–38: se a inicialização da sessão [curl] falhar, a função [curl_init] retorna o booleano FALSE;
- linhas 40–54: o dicionário [$options] configura a ligação [curl] ao servidor;
- linha 57: as opções de ligação são passadas para o recurso [$curl];
- linha 59: liga-se ao URL solicitado com as opções definidas. Devido à opção [CURLOPT_RETURNTRANSFER => true], a função [curl_exec] devolve o documento enviado pelo servidor como uma cadeia de caracteres. A função [curl_exec] devolve FALSE se a ligação falhar;
- Linha 64: Analisamos o resultado de [curl_exec];
- linha 66: a página recebida é guardada num ficheiro local;
- linhas 69, 72, 75: o resultado da função [getUrl] é devolvido;
Ao executar o script [http-02.php], é apresentada a seguinte saída na consola:
* Rebuilt URL to: http://sergetahe.com/
Client : début de la communication avec le serveur [sergetahe.com] ----------------------------
* Trying 87.98.154.146…
* TCP_NODELAY set
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET / HTTP/1.1
Host: sergetahe.com
Accept: */*
< HTTP/1.1 302 Found
< Date: Sat, 18 May 2019 08:46:38 GMT
< Content-Type: text/HTML; charset=UTF-8
< Transfer-Encoding: chunked
< Server: Apache
< X-Powered-By: PHP/7.0
< Location: http://sergetahe.com/cours-tutoriels-de-programmation
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 to host sergetahe.com left intact
* Issue another request to this URL: 'http://sergetahe.com/cours-tutoriels-de-programmation'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) with 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
Accept: */*
< HTTP/1.1 301 Moved Permanently
< Date: Sat, 18 May 2019 08:46:38 GMT
< Content-Type: text/HTML; charset=iso-8859-1
< Content-Length: 262
< Server: Apache
< Location: http://sergetahe.com/cours-tutoriels-de-programmation/
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< Cache-control: private
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 to host sergetahe.com left intact
* Issue another request to this URL: 'http://sergetahe.com/cours-tutoriels-de-programmation/'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) with 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
Accept: */*
< HTTP/1.1 200 OK
< Date: Sat, 18 May 2019 08:46:39 GMT
< Content-Type: text/HTML; charset=UTF-8
< Transfer-Encoding: chunked
< Server: Apache
< X-Powered-By: PHP/7.0
< Link: <http://sergetahe.com/cours-tutoriels-de-programmation/wp-json/>; rel="https://api.w.org/"
< Link: <http://sergetahe.com/cours-tutoriels-de-programmation/>; rel=shortlink
< Vary: Accept-Encoding
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< Cache-control: private
< X-IPLB-Instance: 17097
<
Fin de la communication avec le serveur [sergetahe.com]. Vérifiez le fichier [output/sergetahe.com.HTML]
Client : début de la communication avec le serveur [tahe.developpez.com] ----------------------------
* Connection #0 to host sergetahe.com left intact
* Rebuilt URL to: https://tahe.developpez.com/
* Trying 87.98.130.52…
* TCP_NODELAY set
* Connected to tahe.developpez.com (87.98.130.52) port 443 (#0)
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: C:\myprograms\laragon-lite\etc\ssl\cacert.pem
CApath: none
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use http/1.1
* Server certificate:
* subject: CN=*.developpez.com
* start date: Apr 4 08:25:09 2019 GMT
* expire date: Jul 3 08:25:09 2019 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.
> GET / HTTP/1.1
Host: tahe.developpez.com
Accept: */*
< HTTP/1.1 200 OK
< Date: Sat, 18 May 2019 08:46:42 GMT
< Server: Apache/2.4.25 (Debian)
< X-Powered-By: PHP/5.3.29
< Vary: Accept-Encoding
< Transfer-Encoding: chunked
< Content-Type: text/HTML
<
Fin de la communication avec le serveur [tahe.developpez.com]. Vérifiez le fichier [output/tahe.developpez.com.HTML]
Client : début de la communication avec le serveur [www.polytech-angers.fr] ----------------------------
* Connection #0 to host tahe.developpez.com left intact
* Rebuilt URL to: http://www.polytech-angers.fr/
* Trying 193.49.144.41…
* 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
Accept: */*
< HTTP/1.1 301 Moved Permanently
< Date: Sat, 18 May 2019 08:46:45 GMT
< Server: Apache/2.4.29 (Ubuntu)
< Location: http://www.polytech-angers.fr/fr/index.HTML
< Cache-Control: max-age=1
< Expires: Sat, 18 May 2019 08:46:46 GMT
< Content-Length: 339
< Content-Type: text/HTML; charset=iso-8859-1
<
* Ignoring the response-body
* Connection #0 to host www.polytech-angers.fr left intact
* Issue another request to this URL: 'http://www.polytech-angers.fr/fr/index.HTML'
* Found bundle for host www.polytech-angers.fr: 0x1fee4ebe390 [can pipeline]
* Re-using existing connection! (#0) with 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
Accept: */*
< HTTP/1.1 200
< Date: Sat, 18 May 2019 08:46:46 GMT
< Server: Apache/2.4.29 (Ubuntu)
< X-Cocoon-Version: 2.1.13-dev
< Accept-Ranges: bytes
< Last-Modified: Sat, 18 May 2019 08:01:36 GMT
< Content-Type: text/HTML; charset=UTF-8
< Content-Length: 47372
< Vary: Accept-Encoding
< Cache-Control: max-age=1
< Expires: Sat, 18 May 2019 08:46:47 GMT
< Content-Language: fr
<
* Connection #0 to host www.polytech-angers.fr left intact
Fin de la communication avec le serveur [www.polytech-angers.fr]. Vérifiez le fichier [output/www.polytech-angers.fr.HTML]
Client : début de la communication avec le serveur [localhost] ----------------------------
* Rebuilt URL to: http://localhost/
* Trying ::1…
* TCP_NODELAY set
* Connected to localhost (::1) port 80 (#0)
> GET / HTTP/1.1
Host: localhost
Accept: */*
< HTTP/1.1 200 OK
< Date: Sat, 18 May 2019 08:46:47 GMT
< Server: Apache/2.4.35 (Win64) OpenSSL/1.1.0i PHP/7.2.11
< X-Powered-By: PHP/7.2.11
< Content-Length: 1781
< Content-Type: text/HTML; charset=UTF-8
<
* Connection #0 to host localhost left intact
Fin de la communication avec le serveur [localhost]. Vérifiez le fichier [output/localhost.HTML]
Comentários
- Recebemos as mesmas trocas de dados que com a ferramenta [curl];
- em verde, os registos do script;
- em azul, os comandos enviados ao servidor;
- em amarelo, os comandos recebidos em resposta pelo cliente;
16.4.7. Conclusão
Nesta secção, explorámos o protocolo HTTP e escrevemos um script [http-02.php] capaz de descarregar um URL da Web.
16.5. O SMTP (Simple Mail Transfer Protocol)
16.5.1. Introdução

Neste capítulo:
- [Servidor B] será um servidor SMTP local que iremos instalar;
- [Cliente A] será um cliente SMTP em várias formas:
- o cliente [RawTcpClient] para explorar o protocolo SMTP;
- um script PHP que emula o protocolo SMTP do cliente [RawTcpClient];
- um script PHP que utiliza a biblioteca [SwiftMailServer] para enviar todos os tipos de e-mails;
16.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 as mensagens. Para tal, vamos criar um endereço no Gmail:

- em [5], criamos o utilizador [php7parlexemple] (escolha outro nome);
- em [6], a palavra-passe será [PHP7parlexemple] (escolha outra);
- Em [7], confirmamos estas informações;

- preencha os campos [9-10] e, em seguida, confirme (11);
- aceite os termos de serviço do Google (12-13) e, em seguida, confirme (14);

- em [15], a caixa de entrada do utilizador [PHP7] (16);
- em [17], este utilizador tem uma caixa de entrada vazia;
- em [18-19], inicie sessão na conta Google do utilizador [php7parlexemple@gmail.com]. Iremos configurar a segurança da conta;

- em [21], permita que aplicações que não sejam do Google acedam à conta [php7parlexemple]. Se não fizer isto, o nosso servidor de e-mail local [hMailServer] não conseguirá comunicar com o servidor SMTP do Gmail;

16.5.3. Configurar um servidor SMTP
Para os nossos testes, iremos instalar o servidor de e-mail [hMailServer], que funciona como um servidor SMTP para o envio de e-mails, um servidor POP3 (Post Office Protocol) para a recuperação de e-mails armazenados no servidor e um servidor IMAP (Internet Message Access Protocol), que também permite recuperar e-mails armazenados no servidor, mas oferece funcionalidades adicionais. Em particular, permite-lhe gerir o armazenamento de 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-lhe-ã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 de administrador: anote-a, pois irá precisar dela;
O [hMailServer] instala-se como um serviço do Windows que é iniciado automaticamente quando o computador arranca. É preferível escolher um arranque manual:
- Em [3], digite [serviços] na caixa de pesquisa da barra de tarefas;

- Em [4-8], defina o serviço para o modo [Manual] (6) e, em seguida, inicie-o (7);
Depois de iniciado, o [hMailServer] deve ser configurado. O servidor foi instalado com um programa de administração [hMailServer Administrator]:

- em [2], no campo de entrada da barra de estado, digite [hmailserver];
- Em [3], inicie o administrador;
- Em [4], ligue o administrador ao servidor [hMailServer];
- em [5], digite a senha inserida durante a instalação do [hMailServer];

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


- em [15], configure o protocolo SMTP do servidor de e-mail;
- em [16], configuramos a entrega de e-mail;
- em [17], a configuração para o envio de e-mails para a máquina anfitriã (localhost);
- em [18], o nome da máquina local (localhost). O script na secção de links permite-lhe obter este nome;
- em [19], configuramos um servidor de retransmissão SMTP: este é o servidor que irá tratar da distribuição de e-mails não destinados à máquina local (localhost);
- em [20], o servidor SMTP do Gmail. Estamos a utilizar o Gmail porque criámos uma conta lá na secção de links;
- Em [21], a porta SMTP do Gmail;
- em [22], o serviço SMTP do Gmail é um serviço seguro: é necessária uma conta do Gmail para aceder ao mesmo;
- em [23], o utilizador [php7parlexemple] criado na secção «link»;
- em [24], a palavra-passe deste utilizador: [PHP7parlexemple], criada na secção «link»;
- em [25], especifique o tipo de protocolo de segurança utilizado pelo Gmail;

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

Vamos explorar o protocolo SMTP utilizando o seguinte ambiente:
- O Cliente A será o cliente TCP genérico [RawTcpClient];
- O servidor B será o servidor de e-mail [hMailServer];
- O Cliente A solicitará ao Servidor B que entregue um e-mail ao utilizador [php7parlexemple@gmail.com];
- vamos verificar se este utilizador recebeu efetivamente o e-mail enviado;
Iniciamos o cliente da seguinte forma:
![]()
- em [1], ligamo-nos à porta 25 na máquina local, onde o serviço SMTP do [hMailServer] está em execução. O argumento [--quit bye] indica que o utilizador sairá do programa digitando o comando [bye]. Sem este argumento, o comando para encerrar o programa é [quit]. No entanto, [quit] é também um comando do protocolo SMTP. Devemos, portanto, evitar esta ambiguidade;
- em [2], o cliente está conectado com sucesso;
- em [3], o cliente aguarda comandos introduzidos a partir do teclado;
- em [4], o servidor envia ao cliente a sua mensagem de boas-vindas;

- em [5], o cliente envia o comando [EHLO nome-da-máquina-do-cliente]. O servidor responde com uma série de mensagens no formato [250-xx] (6). O código [250] indica que o comando enviado pelo cliente foi bem-sucedido;
- em [7], o cliente especifica o remetente da mensagem, neste caso [guest@localhost]. Este utilizador deve existir no servidor de correio [hMailServer]. É o que acontece aqui, porque criámos este utilizador anteriormente;
- em [8], a resposta do servidor;
- em [9], é indicado o destinatário da mensagem, neste caso o utilizador do Gmail [php7parlexemple@gmail.com];
- em [10], a resposta do servidor;
- em [11], o comando [DATA] informa ao servidor que o cliente está prestes a enviar o conteúdo da mensagem;
- em [12], a resposta do servidor;
- em [13-16], o cliente deve enviar uma lista de linhas de texto terminando com uma linha contendo apenas um único ponto. A mensagem pode conter as linhas [Subject:, From:, To:] (13) para definir o assunto, o remetente e o destinatário da mensagem, respetivamente;
- em [14], os cabeçalhos anteriores devem ser seguidos por uma linha em branco;
- em [15], o texto da mensagem;
- em [16], a linha contendo apenas um único ponto, que indica o fim da mensagem;
- Em [17], assim que o servidor receber a linha contendo apenas um único ponto, coloca a mensagem na fila;
- em [18], o cliente informa ao servidor que terminou;
- em [19], a resposta do servidor;
- em [20], vemos que o servidor encerrou a ligação com o cliente;
Agora vamos verificar se o utilizador [php7parlexemple@gmail.com] recebeu efetivamente a mensagem:

- em [2], vemos que o utilizador [php7parlexemple@gmail.com] recebeu efetivamente a mensagem;



- em [7], o remetente do e-mail. Vemos que não é [guest@localhost]. Isto deve-se ao facto de o servidor de retransmissão definido na configuração do [hMailServer] ter entregue a mensagem. No entanto, este servidor de retransmissão é [smtp.gmail.com], associado às credenciais do utilizador do Gmail [php7parlexemple@gmail.com]. Qualquer e-mail proveniente de [hMailServer] parecerá ter sido enviado pelo utilizador [php7parlexemple@gmail.com]. Não era isto que pretendíamos, mas se não utilizarmos este servidor de retransmissão, o serviço SMTP do Gmail rejeita os e-mails enviados por [hMailServer], uma vez que o SMTP do Gmail requer uma autenticação que [hMailServer] não fornece. Provavelmente existe uma forma de contornar este problema, mas ainda não a encontrei;
- em [8], vemos que o e-mail foi recebido da máquina [DESKTOP-528I5CU] que hospeda o servidor de e-mail [hMailServer];
- em [9], o remetente da mensagem. Podemos ver que não é [guest@localhost];
- em [10], o remetente original da mensagem. Desta vez, é de facto [guest@localhost];
- em [11], o assunto;
- em [12], o destinatário;
- em [13], a mensagem;
Por fim, o nosso [RawTcpClient] enviou a mensagem com sucesso, apesar de termos encontrado um problema com o remetente. Agora temos os conceitos básicos para criar um cliente SMTP escrito em PHP.
16.5.5. Um cliente SMTP básico escrito em PHP
Vamos implementar em PHP o que aprendemos anteriormente sobre o protocolo SMTP.

O script [smtp-01.php] é configurado pelo seguinte ficheiro JSON [config-smtp-01.json]:
{
"mail to localhost via localhost": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "guest@localhost",
"subject": "to localhost via localhost",
"message": "ligne 1\nligne 2\nligne 3"
},
"mail to gmail via localhost": {
"smtp-server": "localhost",
"smtp-port": "25",
"from": "guest@localhost",
"to": "php7parlexemple@gmail.com",
"subject": "to gmail via localhost",
"message": "ligne 1\nligne 2\nligne 3"
},
"mail to gmail via gmail": {
"smtp-server": "smtp.gmail.com",
"smtp-port": "587",
"from": "guest@localhost",
"to": "php7parlexemple@gmail.com",
"subject": "to gmail via gmail",
"message": "ligne 1\nligne 2\nligne 3"
}
}
[config-smtp-01.json] é uma matriz em que cada elemento é um dicionário do tipo [nome=>informação]. O valor [informação] é, por sua vez, um dicionário com as seguintes chaves e valores:
- [smtp-server]: o nome do servidor SMTP a utilizar;
- [smtp-port]: o número da porta do serviço SMTP;
- [from]: o remetente da mensagem;
- [to]: o destinatário da mensagem;
- [subject]: o assunto da mensagem;
- [message]: a mensagem a enviar;
- O primeiro elemento utiliza o servidor SMTP [localhost] para enviar um e-mail a um utilizador em [localhost];
- o segundo elemento utiliza o servidor SMTP [localhost] para enviar um e-mail a um utilizador em [Gmail];
- o terceiro elemento utiliza o servidor SMTP [Gmail] para enviar um e-mail a um utilizador em [Gmail];
O código [smtp-01.php] para o cliente SMTP é o seguinte:
<?php
// client SMTP (SendMail Transfer Protocol) for sending a message
// communication protocol SMTP client-server
// -> client connects to smtp server port 25
// <- server sends him a welcome message
// -> customer sends command EHLO machine name
// <- server responds OK or not
// -> customer sends order MAIL FROM: <sender>
// <- server responds OK or not
// -> customer sends command RCPT TO: <recipient>
// <- server responds OK or not
// -> customer sends DATA command
// <- server responds OK or not
// -> client sends all the lines of its message and ends with a line containing the
// single character .
// <- server responds OK or not
// -> customer sends QUIT command
// <- server responds OK or not
// server responses have the form xxx text where xxx is a 3-digit number. All
// number xxx >=500 indicates an error.
// The answer may consist of several lines all beginning with xxx except the last one
// of the form xxx(space)
// text lines exchanged must end with the characters RC(#13) and LF(#10)
//
// client SMTP (SendMail Transfer Protocol) for sending a message
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// strict adherence to declared types of function parameters
declare (strict_types=1);
//
// mail settings
const CONFIG_FILE_NAME = "config-smtp-01.json";
// we retrieve the configuration
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// mail dispatch
foreach ($mails as $name => $infos) {
// follow-up
print "Envoi du mail [$name]\n";
// mail dispatch
$résultat = sendmail($name, $infos, TRUE);
// result display
print "$résultat\n";
}//for
// end
exit;
//sendmail
//-----------------------------------------------------------------------
function sendmail(string $name, array $infos, bool $verbose = TRUE): string {
// envoie message[$name,$infos]. If $verbose=TRUE , tracks client-server exchanges
// retrieve the customer's name
$client = gethostbyaddr(gethostbyname(""));
// open a connection with the SMTP server
$connexion = fsockopen($infos["smtp-server"], (int) $infos["smtp-port"]);
// return if error
if ($connexion === FALSE) {
return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
}
// $connexion represents a bidirectional communication flow
// between the client (this program) and the smtp server contacted
// this channel is used for the exchange of orders and information
// after connection, the server sends a welcome message which is read as follows
$erreur = sendCommand($connexion, "", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde EHLO
$erreur = sendCommand($connexion, "EHLO $client", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde MAIL FROM:
$erreur = sendCommand($connexion, sprintf("MAIL FROM: <%s>", $infos["from"]), $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde RCPT TO:
$erreur = sendCommand($connexion, sprintf("RCPT TO: <%s>", $infos["to"]), $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde DATA
$erreur = sendCommand($connexion, "DATA", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// prepare message to send
// it must contain the lines
// From: expéditeur
// To: recipient
// Subject:
// blank line
// Message
// .
$data = sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s\r\n.\r\n", $infos["from"], $infos["to"], $infos["subject"], $infos["message"]);
$erreur = sendCommand($connexion, $data, $verbose, FALSE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde quit
$erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// end
fclose($connexion);
return "Message envoyé";
}
// --------------------------------------------------------------------------
function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF): string {
// sends $commande to the $connexion channel
// verbose mode if $verbose=1
// if $withRCLF=1, adds sequence RCLF to exchange
// data
if ($withRCLF) {
$RCLF = "\r\n";
} else {
$RCLF = "";
}
// send cmde if $commande not empty
if ($commande!=="") {
fputs($connexion, "$commande$RCLF");
// possible echo
if ($verbose) {
affiche($commande, 1);
}
}//if
// reading response
$réponse = fgets($connexion, 1000);
// possible echo
if ($verbose) {
affiche($réponse, 2);
}
// error code recovery
$codeErreur = (int) substr($réponse, 0, 3);
// last line of the answer?
while (substr($réponse, 3, 1) === "-") {
// reading response
$réponse = fgets($connexion, 1000);
// possible echo
if ($verbose) {
affiche($réponse, 2);
}
}//while
// answer completed
// error returned by the server?
if ($codeErreur >= 500) {
return substr($réponse, 4);
}
// error-free return
return "";
}
// --------------------------------------------------------------------------
function affiche($échange, $sens) {
// displays $échange on screen
// if $sens=1 displays -->$echange
// if $sens=2 displays <-- $échange without the last 2 characters RCLF
switch ($sens) {
case 1:
print "--> [$échange]\n";
break;
case 2:
$L = strlen($échange);
print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
break;
}//switch
}
Comentários
- linha 39: o ficheiro de configuração é processado;
- linha 42: percorremos os elementos da matriz [mails]. Cada elemento é um dicionário [nome=>informações], em que [nome] é qualquer nome e [informações] é um dicionário que contém as informações necessárias para enviar um e-mail;
- linha 46: o e-mail é enviado utilizando a função [sendmail], que recebe três parâmetros:
- $name: o nome atribuído a este e-mail;
- $infos: o dicionário que contém as informações necessárias para enviar o e-mail;
- verbose: um valor booleano que indica se as trocas entre cliente e servidor devem ser registadas na consola;
- linha 46: a função [sendmail] devolve uma mensagem de erro que fica vazia se não ocorrer nenhum erro;
- linha 56: a função [sendmail] envia os vários comandos que um cliente SMTP deve enviar:
- linhas 77–84: o comando EHLO;
- linhas 85–92: o comando MAIL FROM:;
- linhas 93–100: o comando RCPT TO:;
- linhas 101–108: o comando DATA;
- linhas 117–124: envio da mensagem (De, Para, Assunto, texto);
- linhas 125–132: o comando QUIT;
- linha 140: a função [sendCommand] é responsável por enviar os comandos do cliente para o servidor SMTP. Aceita quatro parâmetros:
- [$connection]: a ligação que liga o cliente ao servidor;
- [$command]: o comando a enviar;
- [$verbose]: se TRUE, as trocas cliente/servidor são registadas na consola;
- [$withRCLF]: se TRUE, envia o comando terminado pela sequência \r\n. Isto é necessário para todos os comandos do protocolo SMTP, mas [sendCommand] também é usado para enviar a mensagem. Neste caso, a sequência \r\n não é adicionada;
- linhas 150–157: o comando é enviado para o servidor;
- linhas 158–163: lê a primeira linha da resposta. A resposta pode consistir em várias linhas. Cada linha tem o formato XXX-YYY, onde XXX é um código numérico, exceto a última linha da resposta, que tem o formato XXX YYY (sem o hífen);
- linhas 167–174: leitura de todas as linhas da resposta;
- linha 177: se o código numérico XXX for superior a 500, então o servidor devolveu um erro;
Resultados
A execução do script produz a seguinte saída na consola:
Envoi du mail [mail to localhost via localhost]
<-- [220 Bienvenue sur sergetahe@localhost]
--> [EHLO DESKTOP-528I5CU.home]
<-- [250-DESKTOP-528I5CU]
<-- [250-SIZE 20480000]
<-- [250-AUTH LOGIN]
<-- [250 HELP]
--> [MAIL FROM: <guest@localhost>]
<-- [250 OK]
--> [RCPT TO: <guest@localhost>]
<-- [250 OK]
--> [DATA]
<-- [354 OK, send.]
--> [From: guest@localhost
To: guest@localhost
Subject: to localhost via localhost
ligne 1
ligne 2
ligne 3
.
]
<-- [250 Queued (0.016 seconds)]
--> [QUIT]
<-- [221 goodbye]
Message envoyé
Envoi du mail [mail to gmail via localhost]
<-- [220 Bienvenue sur sergetahe@localhost]
--> [EHLO DESKTOP-528I5CU.home]
<-- [250-DESKTOP-528I5CU]
<-- [250-SIZE 20480000]
<-- [250-AUTH LOGIN]
<-- [250 HELP]
--> [MAIL FROM: <guest@localhost>]
<-- [250 OK]
--> [RCPT TO: <php7parlexemple@gmail.com>]
<-- [250 OK]
--> [DATA]
<-- [354 OK, send.]
--> [From: guest@localhost
To: php7parlexemple@gmail.com
Subject: to gmail via localhost
ligne 1
ligne 2
ligne 3
.
]
<-- [250 Queued (0.000 seconds)]
--> [QUIT]
<-- [221 goodbye]
Message envoyé
Envoi du mail [mail to gmail via gmail]
<-- [220 smtp.gmail.com ESMTP d9sm21623375wro.26 - gsmtp]
--> [EHLO DESKTOP-528I5CU.home]
<-- [250-smtp.gmail.com at your service, [90.93.230.110]]
<-- [250-SIZE 35882577]
<-- [250-8BITMIME]
<-- [250-STARTTLS]
<-- [250-ENHANCEDSTATUSCODES]
<-- [250-PIPELINING]
<-- [250-CHUNKING]
<-- [250 SMTPUTF8]
--> [MAIL FROM: <guest@localhost>]
<-- [530 5.7.0 Must issue a STARTTLS command first. d9sm21623375wro.26 - gsmtp]
5.7.0 Must issue a STARTTLS command first. d9sm21623375wro.26 - gsmtp
Done.
- linhas 1-26: o envio de um e-mail para [guest@localhost] através do servidor SMTP [hMailServer] funciona bem;
- linhas 27-52: utilizar o servidor SMTP [hMailServer] para enviar um e-mail para [php7parlexemple@gmail.com] funciona bem;
- linhas 53-65: utilizar o servidor SMTP [Gmail] para enviar um e-mail para [php7parlexemple@gmail.com] não funciona bem: na linha 65, o servidor SMTP devolve o código de erro 530 com a mensagem de erro. Isto indica que o cliente SMTP deve primeiro autenticar-se através de uma ligação segura. O nosso cliente não o fez e, por isso, é rejeitado;
16.5.6. Um segundo cliente SMTP escrito utilizando a biblioteca [SwiftMailer]
O cliente anterior tem pelo menos duas deficiências:
- não consegue utilizar uma ligação segura se o servidor a exigir;
- não consegue anexar ficheiros à mensagem;
No nosso novo script, utilizaremos a biblioteca [SwiftMailer] [https://swiftmailer.symfony.com/] (maio de 2019). O procedimento de instalação do [SwiftMailer] está descrito na URL [https://swiftmailer.symfony.com/docs/introduction.HTML] (maio de 2019).
Primeiro, inicie o Laragon:

- Em [1], abra um terminal;

- em [3], verifique se está na pasta [<laragon>/www], onde <laragon> é a pasta de instalação do Laragon;
- em [3], digite o comando apresentado (maio de 2019). Verifique o comando exato na URL [https://swiftmailer.symfony.com/docs/introduction.HTML];
- em [4], indica-se que não foi realizada nenhuma instalação ou atualização. Isto deve-se ao facto de a biblioteca já ter sido instalada nesta máquina;
- em [5], o diretório de instalação do [swiftmailer] [6];
- em [7], um ficheiro de que vamos precisar no nosso script;
Depois de fazer isto, verifique se a pasta [<laragon>/www/vendor] [5] está incluída no [Include Path] do NetBeans (consulte a secção com o link).
Por fim, a biblioteca [SwiftMailer] requer que a extensão [mbstring] do PHP esteja ativada. Para tal, verifique o ficheiro [php.ini] (ver secção em link):

O script [smtp-02.php] utilizará o seguinte ficheiro de configuração JSON [config-smtp-02.json]:
Estão presentes os mesmos campos que no ficheiro [config-smtp-01.json], com dois campos adicionais:
- [tls]: definido como TRUE indica que deve ser utilizada uma ligação segura com o servidor SMTP. Se [tls] estiver definido como TRUE, devem ser adicionados dois campos adicionais:
- [user]: o nome de utilizador utilizado para autenticar a ligação;
- [password]: a sua palavra-passe;
No nosso exemplo, utilizámos as credenciais do utilizador [php7parlexemple@gmail.com] para nos ligarmos ao servidor do Gmail. Utilize as suas próprias;
- [attachments]: especifica os nomes dos ficheiros a anexar ao e-mail;
O código do script [smtp-02.php] é o seguinte:
<?php
// client SMTP (SendMail Transfer Protocol) for sending a message
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
//
// mail settings
const CONFIG_FILE_NAME = "config-smtp-02.json";
// we retrieve the configuration
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// mail dispatch
foreach ($mails as $name => $infos) {
// follow-up
print "Envoi du mail [$name]\n";
// mail dispatch
$résultat = sendmail($name, $infos);
// result display
print "$résultat\n";
}//for
// end
exit;
//-----------------------------------------------------------------------
function sendmail($name, $infos) {
// sends $infos[message] to smtp server $infos[smtp-server] on port $infos[smt-port]
// if $infos[tls] is true, support TLS will be used
// the mail is sent from $infos[from]
// for the recipient $infos['to']
// Document $info[attachment] is attached to the message
// message has subject $infos[subject]
//
// message in HTML format
$messageHTML = str_replace("\n", "<br/>", $infos["message"]);
try {
// message creation
$message = (new \Swift_Message())
// message subject
->setSubject($infos["subject"])
// sender
->setFrom($infos["from"])
// recipients with a dictionary (setTo/setCc/setBcc)
->setTo($infos["to"])
// message text
->setBody($infos["message"])
// html variant
->addPart("<b>$messageHTML</b>", 'text/html')
;
// attachments
foreach ($infos["attachments"] as $attachment) {
// path of attachment
$fileName = __DIR__ . $attachment;
// check that the file exists
if (file_exists($fileName)) {
// attach the document to the message
$message->attach(\Swift_Attachment::fromPath($fileName));
} else {
// error
print "L'attachement [$fileName] n'existe pas\n";
}
}
// protocol TLS ?
if ($infos["tls"] === "TRUE") {
// TLS
$transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"], 'tls'))
->setUsername($infos["user"])
->setPassword($infos["password"]);
} else {
// no TLS
$transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"]));
}
// the shipment manager
$mailer = new \Swift_Mailer($transport);
// sending the message
$result = $mailer->send($message);
// end
return "Message [$name] envoyé";
} catch (\Throwable $ex) {
// error
return "Erreur lors de l'envoi du message [$name] : " . $ex->getMessage();
}
}
Comentários
- Linha 10: Carregamos o ficheiro [autoload.php] localizado na pasta [<laragon>/www/vendor], onde <laragon> é a pasta de instalação do Laragon. Este ficheiro irá carregar automaticamente os ficheiros de definição das classes do SwiftMailer na primeira vez que essas classes forem utilizadas. Isto evita-nos ter de incluir tantas instruções [require] quantas forem as classes e interfaces do SwiftMailer que iremos utilizar;
- Linha 32: a nova função [sendmail], que tem dois parâmetros:
- [$name], que é utilizada para distinguir entre mensagens;
- [$infos]: as informações necessárias para enviar a mensagem ao seu destinatário;
- linha 42: teremos duas versões da mensagem: uma em texto simples e outra em HTML. Aqui, substituímos as quebras de linha pelo código HTML <br/>;
- linhas 45–69: definimos a mensagem utilizando a classe [\SwiftMessage];
- linha 47: o método [SwiftMessage→setSubject] é utilizado para definir o assunto da mensagem;
- linha 49: o método [SwiftMessage→setFrom] é utilizado para definir o remetente da mensagem;
- linha 51: o método [SwiftMessage→setTo] é utilizado para definir o destinatário da mensagem;
- linha 53: o método [SwiftMessage→setBody] é utilizado para definir o corpo da mensagem;
- linha 55: o método [SwiftMessage→addPart] é utilizado para definir diferentes versões da mensagem, neste caso a mensagem em formato HTML. Quando a mensagem tem variantes, os clientes de e-mail apresentam a variante preferida do utilizador;
- linhas 58–69: o método [SwiftMessage→addAttachment] (64) permite anexar um ficheiro à mensagem;
- linhas 70–79: Depois de definida a mensagem a enviar, deve especificar como enviá-la. O modo de transporte da mensagem é definido pela classe [\Swift_SmtpTransport]. Devem ser fornecidas, pelo menos, duas informações: o nome e a porta do servidor SMTP. Há ainda uma terceira: o servidor SMTP requer autenticação segura?
- linhas 73–75: a instância [\Swift_SmtpTransport] para uma ligação segura ao servidor SMTP;
- linha 78: a instância [\Swift_SmtpTransport] para uma ligação não segura ao servidor SMTP;
- linha 81: a classe [\SwiftMailer] envia as mensagens. Deve passar-lhe o modo de transporte escolhido;
- linha 83: a mensagem [\SwiftMessage] é enviada através do [\Swift_SmtpTransport] selecionado. O método [SwiftMailer→send] retorna FALSE se a mensagem não puder ser enviada;
- linhas 86–89: a biblioteca [SwiftMailer] lança uma exceção assim que algo corre mal;
Nota: Note que o namespace das classes na biblioteca [SwiftMailer] é a raiz \. Indicámos explicitamente as classes [\SwiftMessage, \Swift_SmtpTransport, \SwiftMailer] para o lembrar disso;
Resultados
Ao executar o script [smtp-02.php], é apresentada a seguinte saída na consola:
Se verificarmos a conta Gmail do utilizador [php7parlexemple], vemos o seguinte:

- em [1], o assunto;
- em [2], o remetente;
- em [3], o destinatário;
- em [4], a mensagem;
- em [5-10], os anexos;
Se solicitar a visualização da mensagem original, obtém o seguinte documento:
Return-Path: <php7parlexemple@gmail.com>
Received: from [127.0.0.1] (lfbn-1-11924-110.w90-93.abo.wanadoo.fr. [90.93.230.110])
by smtp.gmail.com with ESMTPSA id e14sm7773816wma.41.2019.05.26.03.11.53
for <php7parlexemple@gmail.com>
(version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
Sun, 26 May 2019 03:11:54 -0700 (PDT)
Message-ID: <e613c47a421a66e2cf7f8e319616ec49@swift.generated>
Date: Sun, 26 May 2019 10:11:53 +0000
Subject: test-gmail-via-gmail
From: php7parlexemple@gmail.com
To: php7parlexemple@gmail.com
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: multipart/alternative; boundary="_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_"
--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
ligne 1
ligne 2
ligne 3
--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_
Content-Type: text/HTML; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<b>ligne 1<br/>ligne 2<br/>ligne 3</b>
--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_--
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document; name="Hello from SwiftMailer.docx"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.docx"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: application/pdf; name="Hello from SwiftMailer.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.pdf"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: application/vnd.oasis.opendocument.text; name="Hello from SwiftMailer.odt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.odt"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: image/png; name="Cours-Tutoriels-Serge-Tahé-1568x268.png"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Cours-Tutoriels-Serge-Tahé-1568x268.png"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: message/rfc822; name=test-localhost.eml
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=test-localhost.eml
Return-Path: guest@localhost
Received: from [127.0.0.1] (localhost [127.0.0.1]) by DESKTOP-528I5CU with ESMTP ; Sat, 25 May 2019 09:48:23 +0200
Message-ID: <620f4628882b011feebe4faa30b45092@swift.generated>
Date: Sat, 25 May 2019 07:48:22 +0000
Subject: test-localhost
From: guest@localhost
To: guest@localhost
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_"
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: multipart/alternative; boundary="_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_"
--_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
j'ai =C3=A9t=C3=A9 invit=C3=A9 =C3=A0 d=C3=A9je=C3=BBner
--_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_
Content-Type: text/HTML; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<b>j'ai =C3=A9t=C3=A9 invit=C3=A9 =C3=A0 d=C3=A9je=C3=BBner</b>
--_=_swift_1558770503_3561ca315f33bd15ef6556e98db4a5b8_=_--
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document; name="Hello from SwiftMailer.docx"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.docx"
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: application/pdf; name="Hello from SwiftMailer.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.pdf"
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: application/vnd.oasis.opendocument.text; name="Hello from SwiftMailer.odt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Hello from SwiftMailer.odt"
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_
Content-Type: image/png; name="Cours-Tutoriels-Serge-Tahé-1568x268.png"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="Cours-Tutoriels-Serge-Tahé-1568x268.png"
--_=_swift_1558770502_c4b808c99c27ded04595bd11f4bad11b_=_--
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_--
- linha 9: o assunto;
- linha 10: o remetente;
- linha 11: o destinatário;
- linha 13: a mensagem contém várias secções delimitadas por tags [--_=_swift_xx];
- linhas 19–24: a mensagem em texto simples;
- linhas 27–30: a mensagem em HTML;
- linhas 34–36: o ficheiro anexado [Hello from SwiftMailer.docx];
- linhas 40–42: o ficheiro anexado [Hello from SwiftMailer.pdf];
- linhas 46–48: o ficheiro anexado [Hello from SwiftMailer.odt];
- linhas 58–60: o ficheiro anexado [Cours-Tutoriels-Serge-Tahé-1568x268.png];
- linhas 58–60: o ficheiro anexado [test-localhost.eml];
- linhas 62–114: o ficheiro anexado [test-localhost.eml] é, ele próprio, uma mensagem cujo conteúdo é apresentado nas linhas 62–114. Note-se que esta mensagem contém, por sua vez, anexos;
16.6. Os protocolos POP3 (Post Office Protocol) e IMAP (Internet Message Access Protocol)
16.6.1. Introdução
Para ler e-mails armazenados num servidor de e-mail, existem dois protocolos:
- o protocolo POP3 (Post Office Protocol), historicamente o primeiro protocolo, mas raramente utilizado atualmente;
- o protocolo IMAP (Internet Message Access Protocol), que é mais recente do que o POP3 e atualmente o mais utilizado;
Para explorar o protocolo POP3, utilizaremos a seguinte arquitetura:

- [Servidor B] será um servidor POP3/IMAP local, implementado pelo servidor de e-mail [hMailServer];
- [Cliente A] será um cliente POP3/IMAP em várias formas:
- o cliente [RawTcpClient] para explorar o protocolo POP3;
- um script PHP que emula o protocolo POP3 do cliente [RawTcpClient];
- um script PHP que utiliza a biblioteca IMAP do PHP, o que permite a implementação de clientes IMAP e POP3;
16.6.2. Explorando o protocolo POP3
Primeiro, utilizamos o script [smtp-01.php] para enviar um e-mail ao utilizador [guest@localhost]. Se tiver executado os testes associados ao script, este utilizador deverá ter recebido e-mails, mas não conseguimos verificar isso. Para enviar-lhe um novo e-mail, utilize o seguinte ficheiro de configuração [config-smtp-01.json], por exemplo:
Agora vamos ver como podemos ler a caixa de correio do utilizador [guest@localhost] utilizando o cliente [RawTcpClient]:
C:\Data\st-2019\dev\php7\php5-exemples\exemples\inet\utilitaires>RawTcpClient --quit bye localhost 110
Client [DESKTOP-528I5CU:55593] connecté au serveur [localhost-110]
Tapez vos commandes (bye pour arrêter) :
<-- [+OK Bienvenue sur sergetahe@localhost]
USER guest@localhost
<-- [+OK Send your password]
PASS guest
<-- [+OK Mailbox locked and ready]
LIST
<-- [+OK 2 messages (610 octets)]
<-- [1 305]
<-- [2 305]
<-- [.]
RETR 1
<-- [+OK 305 octets]
<-- [Return-Path: guest@localhost]
<-- [Received: from DESKTOP-528I5CU.home (localhost [127.0.0.1])]
<-- [ by DESKTOP-528I5CU with ESMTP]
<-- [ ; Tue, 21 May 2019 12:59:11 +0200]
<-- [Message-ID: <1356373A-33C9-4F31-BA43-2B119E128CE3@DESKTOP-528I5CU>]
<-- [From: guest@localhost]
<-- [To: guest@localhost]
<-- [Subject: to localhost via localhost]
<-- []
<-- [ligne 1]
<-- [ligne 2]
<-- [ligne 3]
<-- [.]
DELE 1
<-- [+OK msg deleted]
LIST
<-- [+OK 1 messages (305 octets)]
<-- [2 305]
<-- [.]
DELE 2
<-- [+OK msg deleted]
LIST
<-- [+OK 0 messages (0 octets)]
<-- [.]
QUIT
<-- [+OK POP3 server saying goodbye…]
Perte de la connexion avec le serveur…
- linha 1: o servidor POP3 utiliza normalmente a porta 110. É o que acontece aqui;
- linha 5: o comando [USER] é utilizado para especificar o utilizador cuja caixa de correio pretende ler;
- linha 7: o comando [PASS] é utilizado para especificar a palavra-passe;
- linha 9: o comando [LIST] solicita uma lista de mensagens na caixa de correio do utilizador;
- linha 14: o comando [RETR] solicita a mensagem especificada pelo número;
- linha 29: o comando [DELE] apaga a mensagem cujo número é especificado;
- linha 40: o comando [QUIT] indica ao servidor que terminou;
A resposta do servidor pode assumir várias formas:
- uma única linha começando com [+OK] para indicar que o comando anterior do cliente foi bem-sucedido;
- uma única linha começando com [-ERR] para indicar que o comando anterior do cliente falhou;
- várias linhas em que:
- a primeira linha começa com [+OK];
- a última linha consiste num único ponto;
16.6.3. Um script básico que implementa o protocolo POP3

Uma vez que o protocolo POP3 tem a mesma estrutura que o protocolo SMTP, o script [pop3-01.php] é uma adaptação do script [smtp-01.php]. Terá o seguinte ficheiro de configuração [config-pop3-01.json]:
- linhas 3-4: o servidor POP3 a ser consultado é o servidor local [hMailServer];
- linhas 5-6: queremos ler a caixa de correio do utilizador [guest@localhost];
- linha 7: vamos ler até 5 e-mails;
O script [pop3-01.php] é o seguinte:
<?php
// client POP3 (Post Office Protocol) for reading mailbox messages
// POP3 client-server communication protocol
// -> client connects to smtp server port 110
// <- server sends him a welcome message
// -> customer sends command USER user
// <- server responds OK or not
// -> customer sends PASS mot_de_passe order
// <- server responds OK or not
// -> customer sends LIST command
// <- server responds OK or not
// -> customer sends command RETR n° for each email
// <- server responds OK or not. If OK sends the requested mail content
// -> server sends all the mail lines and ends with a line containing the
// single character .
// -> customer sends command DELE n° to delete an e-mail
// <- server responds OK or not
// // -> client sends QUIT command to end dialog with server
// <- server responds OK or not
// server responses have the form +OK text where -ERR text
// The answer may consist of several lines. In this case, the last line consists of a single dot
// text lines exchanged must end with the characters RC(#13) and LF(#10)
//
// POP3 client (SendMail Transfer Protocol) for reading e-mails
//
// error management
//ini_set("error_reporting", E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// strict adherence to declared types of function parameters
declare (strict_types=1);
//
// mail settings
const CONFIG_FILE_NAME = "config-pop3-01.json";
// we retrieve the configuration
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// letterbox reading
foreach ($mailboxes as $name => $infos) {
// follow-up
print "Lecture de la boîte à lettres [$name]\n";
// letterbox reading
$résultat = readmail($name, $infos, TRUE);
// result display
print "$résultat\n";
}//for
// end
exit;
//readmail
//-----------------------------------------------------------------------
function readmail(string $name, array $infos, bool $verbose = TRUE): string {
// reads the contents of the mailbox [$name]
// import all messages
// each message is deleted afterb being read
// If $verbose=1, tracks client-server exchanges
//
// open a connection with the SMTP server
$connexion = fsockopen($infos["server"], (int) $infos["port"]);
// return if error
if ($connexion === FALSE) {
return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
}
// $connexion represents a bidirectional communication flow
// between the client (this program) and the pop3 server contacted
// this channel is used for the exchange of orders and information
// after connection, the server sends a welcome message which is read as follows
$erreur = sendCommand($connexion, "", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde USER
$erreur = sendCommand($connexion, "USER {$infos["user"]}", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde PASS
$erreur = sendCommand($connexion, "PASS {$infos["password"]}", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde LIST
$premièreLigne = "";
$erreur = sendCommand($connexion, "LIST", $verbose, TRUE, $premièreLigne);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// analyze 1st line to determine number of messages
$champs = [];
preg_match("/^\+OK (\d+)/", $premièreLigne, $champs);
$nbMessages = (int) $champs[1];
// we loop on the messages
$iMessage = 0;
while ($iMessage < $nbMessages && $iMessage < $infos["maxmails"]) {
// cmde RETR
$erreur = sendCommand($connexion, "RETR " . ($iMessage + 1), $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// cmde DELE
$erreur = sendCommand($connexion, "DELE " . ($iMessage + 1), $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// next msg
$iMessage++;
}
// cmde QUIT
$erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
if ($erreur !== "") {
// closing the connection
fclose($connexion);
// return
return $erreur;
}
// end
fclose($connexion);
return "Terminé";
}
// --------------------------------------------------------------------------
function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF, string &$premièreLigne = ""): string {
// sends $commande to the $connexion channel
// verbose mode if $verbose=1
// if $withRCLF=1, adds sequence RCLF to exchange
// puts the 1st line of the answer in [$premièreLigne]
// ]
// data
if ($withRCLF) {
$RCLF = "\r\n";
} else {
$RCLF = "";
}
// send cmde if $commande not empty
if ($commande !== "") {
fputs($connexion, "$commande$RCLF");
// possible echo
if ($verbose) {
affiche($commande, 1);
}
}//if
// reading response
$réponse = fgets($connexion, 1000);
// memorize the 1st line
$premièreLigne = $réponse;
// possible echo
if ($verbose) {
affiche($réponse, 2);
}
// error code recovery
$codeErreur = substr($réponse, 0, 1);
if ($codeErreur === "-") {
// there has been an error
return substr($réponse, 5);
}
// special cases of cmdes RETR and LIST with multi-line responses
$commande = substr(strtolower($commande), 0, 4);
if ($commande === "list" || $commande === "retr") {
// last line of the answer?
$champs = [];
$match = preg_match("/^\.\s+$/", $réponse, $champs);
while (!$match) {
// reading response
$réponse = fgets($connexion, 1000);
// possible echo
if ($verbose) {
affiche($réponse, 2);
}
// response analysis
$champs = [];
$match = preg_match("/^\.\s+$/", $réponse, $champs);
}//while
}
// error-free return
return "";
}
// --------------------------------------------------------------------------
function affiche($échange, $sens) {
// displays $échange on screen
// if $sens=1 displays -->$echange
// if $sens=2 displays <-- $échange without last 2 characters RCLF
switch ($sens) {
case 1:
print "--> [$échange]\n";
break;
case 2:
$L = strlen($échange);
print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
break;
}//switch
}
Comentários
Como mencionámos, [pop3-01.php] é uma adaptação do script [smtp-01.php] que já discutimos. Iremos apenas comentar as principais diferenças:
- Linha 55: A função [readmail] é responsável por ler os e-mails da caixa de correio. As credenciais de login para esta caixa de correio estão armazenadas no dicionário [$infos];
- linhas 61–66: estabelecem uma ligação com o servidor POP3;
- linhas 71–77: lê a mensagem de boas-vindas enviada pelo servidor;
- linhas 78–85: o comando [USER] é enviado para identificar o utilizador cujos e-mails são pretendidos;
- linhas 86–93: envio do comando [PASS] para fornecer a palavra-passe do utilizador;
- linhas 94-102: envio do comando [LIST] para determinar quantos e-mails existem na caixa de correio deste utilizador.
- linha 96: adiciona o parâmetro [$firstLine] aos parâmetros da função [readmail]. Na primeira linha da sua resposta ao comando LIST, o servidor indica quantas mensagens existem na caixa de correio;
- linhas 104–106: recuperar o número de mensagens da primeira linha da resposta;
- linhas 109–128: percorremos cada mensagem. Para cada uma, emitimos dois comandos:
- RETR i: para recuperar a mensagem n.º i (linhas 111–117);
- DELE i: para a eliminar depois de lida (linhas 118–125);
- linhas 129–136: o comando [QUIT] é enviado para informar ao servidor que terminámos;
- linhas 178–194: para os comandos [LIST] e [RETR], a resposta do servidor ocupa várias linhas, sendo que a última linha consiste num único ponto;
Resultados
Após a execução, obtêm-se os seguintes resultados:
Lecture de la boîte à lettres [localhost:110]
<-- [+OK Bienvenue sur sergetahe@localhost]
--> [USER guest@localhost]
<-- [+OK Send your password]
--> [PASS guest]
<-- [+OK Mailbox locked and ready]
--> [LIST]
<-- [+OK 1 messages (305 octets)]
<-- [1 305]
<-- [.]
--> [RETR 1]
<-- [+OK 305 octets]
<-- [Return-Path: guest@localhost]
<-- [Received: from DESKTOP-528I5CU.home (localhost [127.0.0.1])]
<-- [ by DESKTOP-528I5CU with ESMTP]
<-- [ ; Tue, 21 May 2019 14:25:39 +0200]
<-- [Message-ID: <5F912826-F9C4-41B6-BDA7-4A29537781C9@DESKTOP-528I5CU>]
<-- [From: guest@localhost]
<-- [To: guest@localhost]
<-- [Subject: to localhost via localhost]
<-- []
<-- [ligne ]
<-- [ligne ]
<-- [ligne 3]
<-- [.]
--> [DELE 1]
<-- [+OK msg deleted]
--> [QUIT]
<-- [+OK POP3 server saying goodbye…]
Terminé
Done.
Aqui temos um cliente POP3 básico que carece de certas funcionalidades:
- a capacidade de comunicar com um servidor POP3 seguro;
- a capacidade de ler anexos numa mensagem;
Vamos implementar a primeira funcionalidade utilizando as funções [imap] do PHP.
16.6.4. Cliente POP3/IMAP implementado utilizando as funções [imap] do PHP
Primeiro, precisamos de verificar se as funções [imap] estão disponíveis na versão do PHP que estamos a utilizar. Abrimos o ficheiro [php.ini] descrito na secção com o link e procuramos as linhas que mencionam [imap]:

Linha 895: Verifique se a extensão [imap] está ativada.
O script [imap-01.php] utilizará o seguinte ficheiro JSON [config-imap-01.json]:
O ficheiro [config-imap-01.json] define uma matriz de servidores IMAP/POP3 a contactar. Cada elemento é uma estrutura [chave:valor], em que:
- [chave]: é o servidor a contactar. Temos dois aqui:
- [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]: refere-se ao servidor [imap.gmail.com] a escutar na porta 993. O protocolo cliente/servidor é IMAP. O parâmetro /ssl indica que a comunicação cliente/servidor é segura. O parâmetro /novalidate-cert instrui o cliente a não verificar o certificado de segurança que o servidor irá enviar. Por fim, um servidor IMAP gere um conjunto de caixas de correio para um único utilizador. Ao especificar INBOX no URL do servidor IMAP, indicamos que estamos interessados na caixa de correio denominada INBOX, que é normalmente onde as novas mensagens chegam;
- [{localhost:110/pop3}INBOX]: refere-se ao servidor [localhost] a escutar na porta 110. O protocolo cliente/servidor aqui é o POP3;
- [value]: é um dicionário que especifica os seguintes pontos:
- [imap-server]: o nome do servidor IMAP ou POP3;
- [imap-port]: a porta do servidor IMAP ou POP3;
- [user]: o proprietário cuja caixa de correio pretende ler;
- [password]: a sua palavra-passe;
- [directório-de-saída]: a pasta onde as mensagens devem ser guardadas;
- [prefix]: o prefixo do nome de ficheiro para as mensagens, que terá o formato prefixN, em que N é o número da mensagem;
- [pop3]: um valor booleano definido como TRUE para indicar que o protocolo utilizado é o POP3. Neste caso, após a leitura de uma mensagem, esta será eliminada. É assim que os servidores POP3 funcionam normalmente: uma mensagem lida não é mantida no servidor;
O script [imap-01.php] é o seguinte:
<?php
// IMAP (Internet Message Access Protocol) client for reading e-mails
//
// strict adherence to declared types of function parameters
declare (strict_types=1);
// error management
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
//
// mail reading parameters
const CONFIG_FILE_NAME = "config-imap-01.json";
// we retrieve the configuration
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// reading mailboxes
foreach ($mailboxes as $name => $infos) {
// follow-up
print "------------Lecture de la boîte à lettres [$name]\n";
// reading the mailbox
readmailbox($name, $infos);
}
// end
exit;
//-----------------------------------------------------------------------
function readmailbox(string $name, array $infos): void {
// Connection attempt
$imapResource = imap_open($name, $infos["user"], $infos["password"]);
// Test on the return of the imap_open() function
if (!$imapResource) {
// Failure
print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
} else {
// Connection established
print "Connexion établie avec le serveur [$name].\n";
// total messages in mailbox
$nbmsg = imap_num_msg($imapResource);
print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
// unread messages in current mailbox
if ($nbmsg > 0) {
print "Récupération de la liste des messages non lus de la boîte à lettres [$name]\n";
$msgNumbers = imap_search($imapResource, 'UNSEEN');
if ($msgNumbers === FALSE) {
print "Il n'y a pas de nouveaux messages dans la boîte à lettres [$name]\n";
} else {
foreach ($msgNumbers as $msgNumber) {
// we retrieve information on message n° $msgNumber
$infosMail = imap_headerinfo($imapResource, $msgNumber);
if ($infosMail === FALSE) {
print "Statut du message n° [$msgNumber] de la boîte à lettres [$name] non récupéré : " . imap_last_error() . "\n";
} else {
print "Statut du message n° [$msgNumber] de la boîte à lettres [$name]\n";
print_r($infosMail);
}
// we retrieve the body of message n° $msgNumber
getMailBody($imapResource, $msgNumber, $infos);
// if the protocol is POP3, we delete the message
$pop3 = $infos["pop3"];
if ($pop3 !== NULL) {
// delete the message in two steps
imap_delete($imapResource, $msgNumber);
imap_expunge($imapResource);
}
}
}
}
}
// closing the connection
$imapClose = imap_close($imapResource);
if (!$imapClose) {
// Failure
print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
} else {
// success
print "Fermeture de la connexion réussie.\n";
}
}
function getMailBody($imapResource, int $msgNumber, array $infos): void {
// we retrieve the body of message n° $msgNumber
$corpsMail = imap_body($imapResource, $msgNumber);
print "Enregistrement du message dans le fichier {$infos["output-dir"]}/{$infos["prefix"]}$msgNumber\n";
// create the folder if necessary
if (!file_exists($infos["output-dir"])) {
mkdir($infos["output-dir"]);
}
// record the message
if (!file_put_contents($infos["output-dir"] . "/" . $infos["prefix"] . $msgNumber, $corpsMail)) {
print "Echec de l'enregistrement\n";
}
}
Comentários
- linhas 19–24: percorre todos os servidores encontrados no ficheiro de configuração;
- linha 32: a função [readmailbox] lê a caixa de correio especificada em [$name];
- linha 32: abre uma ligação IMAP;
- o primeiro parâmetro é o URL IMAP da caixa de correio a ser lida;
- o segundo parâmetro é o nome de utilizador do proprietário da caixa de correio;
- o terceiro parâmetro é a sua palavra-passe;
A função [imap_open] protege a ligação se o URL IMAP da caixa de correio incluir o parâmetro /ssl;
- linha 41: a função [imap_num_msg] devolve o número total de mensagens na caixa de correio;
- linha 46: a função [imap_search] permite pesquisar mensagens específicas. Aqui, estamos a pesquisar mensagens que ainda não foram lidas (UNSEEN). O segundo parâmetro é um critério de seleção. Existem cerca de vinte deles. A função [imap_search] devolve uma matriz de IDs de mensagens. Estas podem assumir duas formas: números de sequência ou UIDs de mensagens. Por predefinição, a função [imap_search] devolve uma matriz de números de sequência. Se adicionarmos um terceiro parâmetro [SE_UID], obteremos os UIDs das mensagens;
- linha 47: a função [imap_search] retorna o valor booleano FALSE se não encontrar mensagens;
- linha 50: percorremos todas as mensagens não lidas;
- linha 52: Uma mensagem possui cabeçalhos que podem ser recuperados utilizando a função [imap_headerinfo]. O seu segundo parâmetro é normalmente um número de sequência da mensagem. Se pretender utilizar um UID da mensagem, defina o terceiro parâmetro como [FT_UID];
- linha 53: a função [imap_headerinfo] retorna FALSE se não conseguir concluir a sua tarefa. Caso contrário, retorna um objeto complexo que exibimos usando a função [print_r], linha 57;
- linha 60: após recuperar os cabeçalhos, recuperamos agora o corpo da mensagem utilizando a função [imap_body]. Esta função devolve NULL se não conseguir concluir a sua tarefa;
- linhas 84–87: guardamos o corpo da mensagem num ficheiro local;
- linhas 63–68: se o protocolo utilizado foi POP3, eliminamos a mensagem que acabou de ser lida:
- a função [imap_delete] marca a mensagem como «a ser eliminada», mas não a elimina;
- A função [imap_expunge] elimina fisicamente todas as mensagens que foram marcadas para eliminação;
- Linha 74: Encerramos a ligação ao servidor IMAP. Para tal, utilizamos a função [imap_close];
- Linha 86: A função [imap_body] recupera o corpo de uma mensagem identificada pelo seu ID;
Vamos executar o script [smtp-02.json] para que o utilizador do Gmail [php7parlexemple] e o utilizador do [localhost] [guest] tenham novas mensagens. Depois de feito isso, vamos executar o script [imap-01.php] para ler as suas caixas de correio.
A saída da consola é a seguinte:
------------Lecture de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Connexion établie avec le serveur [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX].
Il y a [27] messages dans la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Récupération de la liste des messages non lus de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Statut du message n° [26] de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
stdClass Object
(
[date] => Wed, 22 May 2019 10:08:24 +0000
[Date] => Wed, 22 May 2019 10:08:24 +0000
[subject] => test-gmail-via-gmail
[Subject] => test-gmail-via-gmail
[message_id] => <d8405cac62d57bd9c531ea79c146c72d@swift.generated>
[toaddress] => php7parlexemple@gmail.com
[to] => Array
(
[0] => stdClass Object
(
[mailbox] => php7parlexemple
[host] => gmail.com
)
)
[fromaddress] => php7parlexemple@gmail.com
[from] => Array
(
[0] => stdClass Object
(
[mailbox] => php7parlexemple
[host] => gmail.com
)
)
[reply_toaddress] => php7parlexemple@gmail.com
[reply_to] => Array
(
[0] => stdClass Object
(
[mailbox] => php7parlexemple
[host] => gmail.com
)
)
[senderaddress] => php7parlexemple@gmail.com
[sender] => Array
(
[0] => stdClass Object
(
[mailbox] => php7parlexemple
[host] => gmail.com
)
)
[Recent] =>
[Unseen] => U
[Flagged] =>
[Answered] =>
[Deleted] =>
[Draft] =>
[Msgno] => 26
[MailDate] => 22-May-2019 10:08:29 +0000
[Size] => 19086
[udate] => 1558519709
)
Enregistrement du message dans le fichier output/gmail-imap/message-26
Statut du message n° [27] de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
stdClass Object
(
…
)
Enregistrement du message dans le fichier output/gmail-imap/message-27
Fermeture de la connexion réussie.
------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [1] messages dans la boîte à lettres [{localhost:110/pop3}]
Récupération de la liste des messages non lus de la boîte à lettres [{localhost:110/pop3}]
Statut du message n° [1] de la boîte à lettres [{localhost:110/pop3}]
stdClass Object
(
…
)
Enregistrement du message dans le fichier output/localhost-pop3/message-1
Fermeture de la connexion réussie.
Done.
Se voltarmos a executar o script [imap-01.php] imediatamente após estes resultados, os resultados são os seguintes:
------------Lecture de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Connexion établie avec le serveur [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX].
Il y a [27] messages dans la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Récupération de la liste des messages non lus de la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Il n'y a pas de nouveaux messages dans la boîte à lettres [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]
Fermeture de la connexion réussie.
------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [0] messages dans la boîte à lettres [{localhost:110/pop3}]
Fermeture de la connexion réussie.
- Linha 3: Continua a haver o mesmo número de mensagens na caixa de correio do Gmail, mas já não há novas mensagens por ler (linha 5). Isto mostra que a execução anterior alterou o estado das mensagens lidas de «não lidas» para «lidas»;
- linha 9: já não há mensagens na caixa de correio do utilizador [guest@localhost]. Isto deve-se ao facto de, na execução anterior, as mensagens lidas em [localhost] terem sido posteriormente eliminadas;
As mensagens foram guardadas localmente:

Se olharmos, por exemplo, para o conteúdo da mensagem n.º 26 no Gmail, vemos o seguinte:
--_=_swift_1558519704_f31b373d6e416dc88eb4db0e45fb3a95_=_
Content-Type: multipart/alternative;
boundary="_=_swift_1558519706_9bffb48891232e50ab645383ca62242d_=_"
--_=_swift_1558519706_9bffb48891232e50ab645383ca62242d_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
ligne 1
ligne 2
ligne 3
--_=_swift_1558519706_9bffb48891232e50ab645383ca62242d_=_
Content-Type: text/HTML; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<b>ligne 1<br/>ligne 2<br/>ligne 3</b>
--_=_swift_1558519706_9bffb48891232e50ab645383ca62242d_=_--
--_=_swift_1558519704_f31b373d6e416dc88eb4db0e45fb3a95_=_
Content-Type: application/pdf; name=Hello.pdf
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=Hello.pdf
JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl
Y29kZT4+CnN0cmVhbQp4nHWPuQoCQQyG+3mK1MKMyThHFoaAq7uF3cKAhdh5gIXgNr6+swcWshII
……………………………….…
OTQwODU4RDUzRDVENjU0QzJCNTM3Mjc+IF0KL0RvY0NoZWNrc3VtIC9DMjU3MUY1MUNDRjgwQ0Ex
ODU0OUI0RTQ4NDkwMDM3OAo+PgpzdGFydHhyZWYKMTIzMjYKJSVFT0YK
--_=_swift_1558519704_f31b373d6e416dc88eb4db0e45fb3a95_=_--
- linhas 11–13: a mensagem em texto simples;
- linha 19: a mensagem HTML;
- linha 25: o anexo;
Vamos tentar melhorar este script para que os diferentes tipos de mensagens e os anexos sejam armazenados em ficheiros separados.
16.6.5. Cliente POP3/IMAP melhorado
No script [imap-01.php], apresentamos o corpo da mensagem #i como um ficheiro de texto que contém tanto os diferentes tipos de mensagem como o conteúdo codificado dos vários anexos. É possível obter a estrutura da mensagem para identificar estas diferentes partes. No script [imap-02.php], modificamos a função [getMailBody] da seguinte forma:
function getMailBody($imapResource, int $msgNumber, array $infos): void {
// we retrieve the message structure
$structure=imap_fetchstructure($imapResource, $msgNumber);
// we display it
print_r($structure);
}
- linha 3: solicitamos a estrutura da mensagem;
- linha 5: exibimo-la;
O objetivo é compreender as informações contidas na estrutura de uma mensagem para ver como podemos extrair as suas várias partes. No nosso exemplo, a mensagem é enviada pelo script [smtp-02.php] com a seguinte configuração [config-smtp-02.json]:
Assim, é enviada uma mensagem com cinco anexos para [guest@localhost] (linhas 11–15). O script [imap-02.php] é executado com a seguinte configuração [config-imap-01.json]:
É, portanto, utilizada a caixa de correio de [guest@localhost] (linha 5). O script [imap-02.php] apresenta então a estrutura da mensagem enviada pelo [smtp-02.php]. Esta estrutura, apresentada na consola, é a seguinte:
stdClass Object
(
[type] => 1
[encoding] => 0
[ifsubtype] => 1
[subtype] => MIXED
[ifdescription] => 0
[ifid] => 0
[bytes] => 253599
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => BOUNDARY
[value] => _=_swift_1558872295_5bc8ee2ca8b3723c0b39ca8bbfbebdeb_=_
)
)
[parts] => Array
(
[0] => stdClass Object
(
[type] => 1
[encoding] => 0
[ifsubtype] => 1
[subtype] => ALTERNATIVE
[ifdescription] => 0
[ifid] => 0
[bytes] => 429
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => BOUNDARY
[value] => _=_swift_1558872296_1e51aae79dfca4e7e0af112489fe8734_=_
)
)
[parts] => Array
(
[0] => stdClass Object
(
[type] => 0
[encoding] => 4
[ifsubtype] => 1
[subtype] => PLAIN
[ifdescription] => 0
[ifid] => 0
[lines] => 3
[bytes] => 27
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => CHARSET
[value] => utf-8
)
)
)
[1] => stdClass Object
(
[type] => 0
[encoding] => 4
[ifsubtype] => 1
[subtype] => HTML
[ifdescription] => 0
[ifid] => 0
[lines] => 1
[bytes] => 40
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => CHARSET
[value] => utf-8
)
)
)
)
)
[1] => stdClass Object
(
[type] => 3
[encoding] => 3
[ifsubtype] => 1
[subtype] => VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
[ifdescription] => 0
[ifid] => 0
[bytes] => 16302
[ifdisposition] => 1
[disposition] => ATTACHMENT
[ifdparameters] => 1
[dparameters] => Array
(
[0] => stdClass Object
(
[attribute] => FILENAME
[value] => Hello from SwiftMailer.docx
)
)
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => NAME
[value] => Hello from SwiftMailer.docx
)
)
)
[2] => stdClass Object
(
[type] => 3
[encoding] => 3
[ifsubtype] => 1
[subtype] => PDF
[ifdescription] => 0
[ifid] => 0
[bytes] => 17514
[ifdisposition] => 1
[disposition] => ATTACHMENT
[ifdparameters] => 1
[dparameters] => Array
(
[0] => stdClass Object
(
[attribute] => FILENAME
[value] => Hello from SwiftMailer.pdf
)
)
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => NAME
[value] => Hello from SwiftMailer.pdf
)
)
)
[3] => stdClass Object
(
…
)
[4] => stdClass Object
(
…
)
[5] => stdClass Object
(
[type] => 2
[encoding] => 3
[ifsubtype] => 1
[subtype] => RFC822
[ifdescription] => 0
[ifid] => 0
[lines] => 1881
[bytes] => 146682
[ifdisposition] => 1
[disposition] => ATTACHMENT
[ifdparameters] => 1
[dparameters] => Array
(
[0] => stdClass Object
(
[attribute] => FILENAME
[value] => test-localhost.eml
)
)
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => NAME
[value] => test-localhost.eml
)
)
[parts] => Array
(
…
)
)
)
)
Comentários
- A documentação PHP para a função [imap_fetch_structure] explica o significado dos vários campos no objeto devolvido pela função:

Os valores numéricos do campo [type] têm os seguintes significados:

Os valores numéricos do campo [encoding] têm os seguintes significados:

A mensagem registada pelo [imap-01.php] começava com o seguinte texto:
Return-Path: <php7parlexemple@gmail.com>
Received: from [127.0.0.1] (lfbn-1-11924-110.w90-93.abo.wanadoo.fr. [90.93.230.110])
by smtp.gmail.com with ESMTPSA id e14sm7773816wma.41.2019.05.26.03.11.53
for <php7parlexemple@gmail.com>
(version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);
Sun, 26 May 2019 03:11:54 -0700 (PDT)
Message-ID: <e613c47a421a66e2cf7f8e319616ec49@swift.generated>
Date: Sun, 26 May 2019 10:11:53 +0000
Subject: test-gmail-via-gmail
From: php7parlexemple@gmail.com
To: php7parlexemple@gmail.com
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_"
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
Content-Type: multipart/alternative; boundary="_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_"
--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
ligne 1
ligne 2
ligne 3
--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_
Content-Type: text/HTML; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<b>ligne 1<br/>ligne 2<br/>ligne 3</b>
--_=_swift_1558865513_43c6d2a54065e4917fb06e3327f8d927_=_--
--_=_swift_1558865513_a3a939017128a4cfb867e968bce5df49_=_
- As linhas 15) e 33) delimitam a mensagem [multipart/mixed] (linha m);
- as linhas 18) e 16) delimitam a primeira parte da mensagem: a mensagem de texto simples;
- as linhas 26) e 32) delimitam a segunda parte da mensagem: a mensagem HTML;
Encontramos as várias informações da mensagem acima no objeto devolvido por [imap_fetchstructure]:
stdClass Object
(
[type] => 1
[encoding] => 0
[ifsubtype] => 1
[subtype] => MIXED
[ifdescription] => 0
[ifid] => 0
[bytes] => 253599
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => BOUNDARY
[value] => _=_swift_1558872295_5bc8ee2ca8b3723c0b39ca8bbfbebdeb_=_
)
)
[parts] => Array
(
[0] => stdClass Object
(
[type] => 1
[encoding] => 0
[ifsubtype] => 1
[subtype] => ALTERNATIVE
[ifdescription] => 0
[ifid] => 0
[bytes] => 429
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => BOUNDARY
[value] => _=_swift_1558872296_1e51aae79dfca4e7e0af112489fe8734_=_
)
)
[parts] => Array
(
[0] => stdClass Object
(
[type] => 0
[encoding] => 4
[ifsubtype] => 1
[subtype] => PLAIN
[ifdescription] => 0
[ifid] => 0
[lines] => 3
[bytes] => 27
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => CHARSET
[value] => utf-8
)
)
)
[1] => stdClass Object
(
[type] => 0
[encoding] => 4
[ifsubtype] => 1
[subtype] => HTML
[ifdescription] => 0
[ifid] => 0
[lines] => 1
[bytes] => 40
[ifdisposition] => 0
[ifdparameters] => 0
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => CHARSET
[value] => utf-8
)
)
)
)
)
- linha 3: a mensagem é do tipo MIME (Multipurpose Internet Mail Extensions) [multipart];
- linha 4: a mensagem está codificada em 7 bits;
- linha 5: [ifsubtype]=1 indica que existe um campo [subtype] na estrutura;
- linha 6: o campo [subtype] designa um subtipo MIME, neste caso o tipo [mixed]. No geral, o tipo MIME do documento é [multipart/mixed];
- Linha 7: [ifdescription]=0 indica que não existe um campo [description] na estrutura;
- linha 8: [ifid]=0 indica que não existe um campo [id] na estrutura;
- linha 10: [ifdisposition]=0 indica que não existe o campo [disposition] na estrutura;
- linha 11: [ifdparameters]=0 indica que não existe o campo [dparameters] na estrutura;
- linha 12: [ifparameters]=1 indica que existe um campo [parameters] na estrutura;
- linha 13: o campo [parameters] descreve os parâmetros da mensagem. Aqui, existe apenas um;
- linhas 15–19: este objeto descreve a linha seguinte da mensagem de texto:
Estas linhas são utilizadas para delimitar a mensagem. Na mensagem recuperada por [imap-01.php], a parte da mensagem que acabámos de descrever corresponde à linha m). O atributo [boundary] não é o mesmo porque as capturas de ecrã correspondem à mesma mensagem, mas foram enviadas em momentos diferentes;
- linha 23: a estrutura das diferentes partes da mensagem começa aqui;
- linhas 25–45: esta primeira parte é do tipo [multipart/alternative]. Corresponde à linha p) do texto da mensagem;
- linha 47: esta primeira parte, por sua vez, tem subpartes;
- linhas 47–70: esta primeira subparte é do tipo [text/plain] (linhas 51, 54), está codificada como [QUOTED-PRINTABLE] (linha 52) e possui um parâmetro [charset=utf-8] (linhas 66–67);
- As linhas 49–72 descrevem as linhas s–x da mensagem de texto;
- linhas 74–99: descrevem a segunda subparte da parte [multipart/alternative];
- linhas 74–99: esta segunda subparte é do tipo [text/HTML] (linhas 76, 79), está codificada em [ENCQUOTEDPRINTABLE] (linha 77) e tem um parâmetro [charset=utf-8] (linhas 89–93);
- as linhas 74–99 descrevem as linhas aa–ad da mensagem de texto;
A secção [multipart/alternative] está agora completa. A secção [application/vnd.openxmlformats-officedocument.wordprocessingml.document] começa, descrita pelo seguinte texto:
Mais uma vez, esta informação encontra-se no objeto devolvido pela função [imap_fetchstructure]:
[1] => stdClass Object
(
[type] => 3
[encoding] => 3
[ifsubtype] => 1
[subtype] => VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
[ifdescription] => 0
[ifid] => 0
[bytes] => 16302
[ifdisposition] => 1
[disposition] => ATTACHMENT
[ifdparameters] => 1
[dparameters] => Array
(
[0] => stdClass Object
(
[attribute] => FILENAME
[value] => Hello from SwiftMailer.docx
)
)
[ifparameters] => 1
[parameters] => Array
(
[0] => stdClass Object
(
[attribute] => NAME
[value] => Hello from SwiftMailer.docx
)
)
)
- linha 1: esta é a segunda parte da mensagem global. Recorde-se que a primeira parte era do tipo [multipart/alternative];
- linhas 3–6: esta segunda parte é do tipo [application/vnd.openxmlformats-officedocument.wordprocessingml.document] (linhas 3 e 6) e está codificada em Base64 (linha 4);
- linha 11: esta segunda parte é um anexo (linha 11) e tem dois parâmetros: [filename=Hello from SwiftMailer.docx] (linhas 15–21) e [name=Hello from SwiftMailer.docx] (linhas 26–32). Note que este último parâmetro não existe na mensagem de texto. Por isso, foi adicionado na função [imap_fetchstructure];
As linhas 1–36 são repetidas para cada um dos cinco anexos da mensagem.
A função [imap_fetch_structure] permite-nos, assim, obter a estrutura de uma mensagem. Esta estrutura define partes, que por sua vez podem ter subpartes. Para recuperar o texto de uma parte ou subparte, utilizamos a função [imap_fetchbody].
Modificamos a função [getMailBody], que nos permite recuperar o corpo de uma mensagem, da seguinte forma:
function getMailBody($imapResource, int $msgNumber, array $infos, object $infosMail): void {
// on récupère la structure du message
$structure = imap_fetchstructure($imapResource, $msgNumber);
if ($structure !== FALSE) {
// on récupère ces différentes parties
getParts($imapResource, $msgNumber, $infos, $infosMail, $structure);
}
}
function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
// calcul du n° de section
if (substr($sectionNumber, 0, 2) === "0.") {
$sectionNumber = substr($sectionNumber, 2);
}
print "-----contenu de la partie n° [$sectionNumber]\n";
// type de contenu
print "Content-Type: ";
switch ($part->type) {
case TYPETEXT:
print "TEXT/{$part->subtype}\n";
break;
case TYPEMULTIPART:
print "MULTIPART/{$part->subtype}\n";
break;
case TYPEAPPLICATION:
print "APPLICATION/{$part->subtype}\n";
break;
case TYPEMESSAGE:
print "MESSAGE/{$part->subtype}\n";
break;
default:
print "UNKNOWN/{$part->subtype}\n";
break;
}
// type de codage
$encodings=["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
print "Transfer-Encoding : ".$encodings[$part->encoding]."\n";
// on passe aux sous-parties éventuelles
if (isset($part->parts)) {
for ($i = 1; $i <= count($part->parts); $i++) {
// une nouvelle partie du message
$subpart = $part->parts[$i - 1];
// appel récursif - on demande le corps de la partie [$subpart]
getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
}
}
}
Comentários
- linha 3: recuperamos a estrutura da mensagem;
- linha 6: solicitamos a visualização das suas diferentes partes, que se encontram na matriz [parts] da estrutura;
- linha 10: a função [getParts] recebe os seguintes parâmetros:
- [$imapResource]: a ligação ao servidor IMAP;
- [$msgNumber]: o número de sequência da mensagem cujas partes pretendemos;
- [$infos]: informações sobre onde armazenar as partes que iremos encontrar no sistema de ficheiros local;
- [$infosMail]: informações gerais sobre o e-mail (remetente, destinatário(s), assunto, etc.);
- [$part]: um objeto que representa uma parte da mensagem;
- [$sectionNumber]: um número de secção (ou parte) da mensagem;
- linhas 17–34: exibe o tipo de conteúdo da secção [$section] da mensagem. Para tal, utilizamos os campos [$part→type] e [$part→subtype] da secção [$part];
- linhas 36-37: é apresentado o tipo de codificação da parte [$sectionNumber];
- linhas 40-47: talvez a parte cujas informações acabaram de ser exibidas tenha subpartes próprias;
- linhas 41-46: se for esse o caso, solicitamos o tipo de conteúdo das várias subpartes da parte que acabámos de exibir. Aqui, fazemos uma chamada recursiva à função [getParts];
Mais uma vez, enviamos um e-mail para o utilizador do Gmail [php7parlexemple@gmail.com] utilizando o script [smtp-02.php] e lemos esse e-mail com o script anterior [imap-02.php]. Isto gera a seguinte saída na consola:
------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [1] messages dans la boîte à lettres [{localhost:110/pop3}]
Récupération de la liste des messages non lus de la boîte à lettres [{localhost:110/pop3}]
-----contenu de la partie n° [0]
Content-Type: MULTIPART/MIXED
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1]
Content-Type: MULTIPART/ALTERNATIVE
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : quoted-printable
-----contenu de la partie n° [1.2]
Content-Type: TEXT/HTML
Transfer-Encoding : quoted-printable
-----contenu de la partie n° [2]
Content-Type: APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
Transfer-Encoding : base 64
-----contenu de la partie n° [3]
Content-Type: APPLICATION/PDF
Transfer-Encoding : base 64
-----contenu de la partie n° [4]
Content-Type: APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT
Transfer-Encoding : base 64
-----contenu de la partie n° [5]
Content-Type: UNKNOWN/PNG
Transfer-Encoding : base 64
-----contenu de la partie n° [6]
Content-Type: MESSAGE/RFC822
Transfer-Encoding : base 64
-----contenu de la partie n° [6.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : 7 bits
Fermeture de la connexion réussie.
Conseguimos recuperar os diferentes tipos de conteúdo das mensagens, bem como os seus tipos de codificação. A numeração das partes segue esta regra:
- linhas 6-7: a parte [multipart/mixed], que representa a mensagem na sua totalidade, é numerada como 0. As diferentes partes deste objeto são então numeradas como 1, 2…
A mensagem tem um total de cinco partes:
- linhas 9-10: a parte [multipart/alternative], numerada como 1;
- linhas 17–18: a parte [APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT], que é numerada como 2. Trata-se de um anexo de ficheiro Word;
- linhas 20–21: a secção [APPLICATION/PDF], numerada com o número 3. Trata-se do anexo de um ficheiro PDF;
- linhas 23–24: a secção [APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT], numerada com o número 4. Trata-se de um ficheiro anexo do OpenOffice;
- linhas 26–27: a secção [UNKNOWN/PNG] identificada com o n.º 5. Trata-se de um anexo de ficheiro de imagem;
- Linhas 30–31: a secção [MESSAGE/RFC822], numerada com o número 6. Trata-se de um anexo de e-mail;
Quando uma parte tem subpartes, estas são numeradas x.1, x.2… em que x é o número da parte que as engloba. Assim:
- linhas 11-12: a primeira parte da secção [multipart/alternative] é numerada 1.1. Trata-se de conteúdo [text/plain]: a mensagem de e-mail;
- linhas 14–15: a segunda parte da secção [multipart/alternative] é numerada como 1.2. É do tipo [text/HTML]: a mensagem de e-mail em HTML;
- linhas 32–33: a primeira parte do anexo [MESSAGE/RFC822] tem o número 6.1. É do tipo [text/plain]. De facto, de acordo com a norma MIME, a numeração das partes de um anexo de e-mail [MESSAGE/RFC822] difere da regra descrita acima. Assim, a primeira parte do anexo [MESSAGE/RFC822] não é numerada como 6.1, mas tem um número diferente;
Agora que sabemos como identificar as diferentes partes e subpartes de um e-mail, precisamos de recuperar o seu conteúdo.
O código do script evolui da seguinte forma:
function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
// calcul du n° de section
if (substr($sectionNumber, 0, 2) === "0.") {
$sectionNumber = substr($sectionNumber, 2);
}
print "-----contenu de la partie n° [$sectionNumber]\n";
// type de contenu
print "Content-Type: ";
switch ($part->type) {
case TYPETEXT:
print "TEXT/{$part->subtype}\n";
break;
case TYPEMULTIPART:
print "MULTIPART/{$part->subtype}\n";
break;
case TYPEAPPLICATION:
print "APPLICATION/{$part->subtype}\n";
break;
case TYPEMESSAGE:
print "MESSAGE/{$part->subtype}\n";
break;
default:
print "UNKNOWN/{$part->subtype}\n";
break;
}
// type de codage
$encodings = ["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
print "Transfer-Encoding : " . $encodings[$part->encoding] . "\n";
// est-ce un message ?
if ($part->type === TYPEMESSAGE) {
// on ne va pas gérer les sous-parties de ce message (mail attaché)
// on affiche le corps du mail attaché
print imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
} else {
// on passe aux sous-parties éventuelles
if (isset($part->parts)) {
for ($i = 1; $i <= count($part->parts); $i++) {
// une nouvelle partie du message
$subpart = $part->parts[$i - 1];
// appel récursif - on demande le corps de la partie [$subpart]
getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
}
} else {
// il n'y a pas de sous-parties - on affiche alors le corps du message
print imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
}
}
}
Comentários
- linha 46: a função [imap_fetchbody] recupera o corpo da parte #[$sectionNumber] da mensagem. A numeração das partes da mensagem segue a regra explicada anteriormente;
- linha 1: começamos com a secção «0»;
- linha 41: as subsecções desta secção serão então numeradas como “0.1”, “0.2”, quando deveriam ser numeradas como “1”, “2”…
- linhas 3–5: corrigimos esta anomalia;
- linhas 37–43: se a secção atual tiver subsecções, percorremos cada uma delas (linhas 38–43). O número da secção é [$sectionNumber.$i];
- linhas 44-47: quando não houver mais subsecções, exibimos o corpo da secção atual utilizando a função [imap_fetchbody]. No nosso exemplo, estas são as secções [text/plain], [text/HTML] e os anexos;
A execução deste script produz os seguintes resultados:
------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [1] messages dans la boîte à lettres [{localhost:110/pop3}]
Récupération de la liste des messages non lus de la boîte à lettres [{localhost:110/pop3}]
-----contenu de la partie n° [0]
Content-Type: MULTIPART/MIXED
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1]
Content-Type: MULTIPART/ALTERNATIVE
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : quoted-printable
ligne 1
ligne 2
ligne 3
-----contenu de la partie n° [1.2]
Content-Type: TEXT/HTML
Transfer-Encoding : quoted-printable
<b>ligne 1<br/>ligne 2<br/>ligne 3</b>
-----contenu de la partie n° [2]
Content-Type: APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
Transfer-Encoding : base 64
UEsDBBQABgAIAAAAIQDfpNJsWgEAACAFAAATAAgCW0NvbnRlbnRfVHlwZXNdLnhtbCCiBAIooAAC
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
…
AAAAAAAAAF0mAABkb2NQcm9wcy9jb3JlLnhtbFBLAQItABQABgAIAAAAIQCdxkmwcgEAAMcCAAAQ
AAAAAAAAAAAAAAAAAAgpAABkb2NQcm9wcy9hcHAueG1sUEsFBgAAAAALAAsAwQIAALArAAAAAA==
-----contenu de la partie n° [3]
Content-Type: APPLICATION/PDF
Transfer-Encoding : base 64
JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl
Y29kZT4+CnN0cmVhbQp4nHWNvQoCMRCE+zzF1sLF2WSTSyAEPD0Lu4OAhdj5AxaC1/j6Rk4s5GSa
…
PDcxQUJGQ0JGQURGODYxM0NBNUJDODNFMDNDNjI1QkQwPgo8NzFBQkZDQkZBREY4NjEzQ0E1QkM4
M0UwM0M2MjVCRDA+IF0KL0RvY0NoZWNrc3VtIC9DMTRCN0Q5N0YwNUU1OTYxQzhDODg0NEI3NkNF
OEIwRQo+PgpzdGFydHhyZWYKMTIzMTQKJSVFT0YK
-----contenu de la partie n° [4]
Content-Type: APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT
Transfer-Encoding : base 64
UEsDBBQAAAgAAAs9uU5exjIMJwAAACcAAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlvbi92bmQub2Fz
aXMub3BlbmRvY3VtZW50LnRleHRQSwMEFAAACAAACz25TgAAAAAAAAAAAAAAABwAAABDb25maWd1
…
AQIUABQACAgIAAs9uU42l0SORAQAABIRAAALAAAAAAAAAAAAAAAAAI8bAABjb250ZW50LnhtbFBL
AQIUABQACAgIAAs9uU4Uf52+LgEAACUEAAAVAAAAAAAAAAAAAAAAAAwgAABNRVRBLUlORi9tYW5p
ZmVzdC54bWxQSwUGAAAAABEAEQBlBAAAfSEAAAAA
-----contenu de la partie n° [5]
Content-Type: UNKNOWN/PNG
Transfer-Encoding : base 64
iVBORw0KGgoAAAANSUhEUgAABiAAAAEMCAYAAABN1n5OAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAg
AElEQVR4nOy9e5TdV3Xn+Zm7aqprlBq1Rq1Wq7XU6opGrXaMMI6jAcfj9ihu4hAehkAghBASICF0
…
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAA2Mb8f9Q5r2ohJn6/AAAAAElFTkSuQmCC
-----contenu de la partie n° [6]
Content-Type: MESSAGE/RFC822
Transfer-Encoding : base 64
UmV0dXJuLVBhdGg6IGd1ZXN0QGxvY2FsaG9zdA0KUmVjZWl2ZWQ6IGZyb20gWzEyNy4wLjAuMV0g
KGxvY2FsaG9zdCBbMTI3LjAuMC4xXSkNCglieSBERVNLVE9QLTUyOEk1Q1Ugd2l0aCBFU01UUA0K
…
cjJvaEpuNi9BQUFBQUVsRlRrU3VRbUNDDQotLV89X3N3aWZ0XzE1NTg3NzA1MDJfYzRiODA4Yzk5
YzI3ZGVkMDQ1OTViZDExZjRiYWQxMWJfPV8tLQ0K
Fermeture de la connexion réussie.
Comentários
- linhas 14–16: o conteúdo da mensagem de texto codificado em [quoted-printable] (linha 13);
- linha 20: o conteúdo da mensagem HTML codificado em [quoted-printable] (linha 19);
- linhas 24–28: o conteúdo do ficheiro Word codificado em [base64] (linha 23);
- linhas 32–37: o conteúdo do ficheiro PDF codificado em [base64] (linha 31);
- linhas 41–45: o conteúdo do ficheiro OpenOffice codificado em [base64] (linha 40);
- linhas 50–55: o conteúdo do ficheiro de imagem codificado em [base64] (linha 49);
- linhas 59–63: o conteúdo do e-mail anexado codificado em [base64] (linha 58);
Agora que:
- sabemos como recuperar o texto das diferentes partes de um e-mail;
- sabemos a codificação desses textos;
podemos guardar esses textos em ficheiros.
O código evolui da seguinte forma:
function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
// calcul du n° de section
if (substr($sectionNumber, 0, 2) === "0.") {
$sectionNumber = substr($sectionNumber, 2);
}
print "-----contenu de la partie n° [$sectionNumber]\n";
// type de contenu
print "Content-Type: ";
switch ($part->type) {
case TYPETEXT:
print "TEXT/{$part->subtype}\n";
break;
case TYPEMULTIPART:
print "MULTIPART/{$part->subtype}\n";
break;
case TYPEAPPLICATION:
print "APPLICATION/{$part->subtype}\n";
break;
case TYPEMESSAGE:
print "MESSAGE/{$part->subtype}\n";
break;
default:
print "UNKNOWN/{$part->subtype}\n";
break;
}
// type de codage
$encodings = ["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
print "Transfer-Encoding : " . $encodings[$part->encoding] . "\n";
// est-ce un message ?
if ($part->type === TYPEMESSAGE) {
// on ne va pas gérer les sous-parties de ce message
savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
} else {
// on passe aux sous-parties éventuelles
if (isset($part->parts)) {
for ($i = 1; $i <= count($part->parts); $i++) {
// une nouvelle partie du message
$subpart = $part->parts[$i - 1];
// appel récursif - on demande le corps de la partie [$subpart]
getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
}
} else {
// il n'y a pas de sous-parties - on sauvegarde alors le corps du message
savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
}
}
}
- linhas 33 e 45: a exibição do texto de uma parte [$imapResource, $msgNumber, $sectionNumber] do e-mail é agora substituída pelo seu armazenamento num ficheiro;
A função [savePart] é a seguinte:
// sauvegarde d'une partie de message
function savePart($imapResource, int $msgNumber, string $sectionNumber, array $infos, object $infosMail): void {
// dossier de sauvegarde
$outputDir = $infos["output-dir"] . "/message-$msgNumber";
// si le dossier n'existe pas, on le crée
if (!file_exists($outputDir)) {
mkdir($outputDir);
}
// structure de la partie à sauvegarder
$struct = imap_bodystruct($imapResource, $msgNumber, $sectionNumber);
// type de document
$type = $struct->type;
// sous-type de document
$subtype = "";
if (isset($struct->subtype)) {
$subtype = strtolower($struct->subtype);
}
// on analyse le type de la partie
switch ($type) {
case TYPETEXT:
// cas du message texte : text/xxx
switch ($subtype) {
case plain:
saveText("$outputDir/message.txt", 0, imap_fetchBody($imapResource, $msgNumber, $sectionNumber), $infosMail, $struct);
break;
case HTML:
saveText("$outputDir/message.HTML", 1, imap_fetchBody($imapResource, $msgNumber, $sectionNumber), $infosMail, $struct);
break;
}
break;
default:
// autres cas - on ne s'intéresse qu'aux attachements
if (isset($struct->disposition)) {
$disposition = strtolower($struct->disposition);
if ($disposition === "attachment") {
// on a affaire à un attachement - on le sauvegarde
saveAttachment($imapResource, $msgNumber, $sectionNumber, $outputDir, $struct);
}
} else {
// on ne traitera pas cette partie
print "Partie [$sectionNumber] ignorée\n";
}
break;
}
}
- linhas 3-8: criação da pasta de cópia de segurança. Esta pasta recebe o nome do número da mensagem cujas secções estão a ser analisadas;
- linha 10: a parte da mensagem a ser guardada é definida de forma única pelos três parâmetros [$imapResource, $msgNumber, $sectionNumber]. Recuperamos a estrutura desta parte utilizando a função [imap_bodystruct];
- linha 12: recuperação do tipo principal da secção da mensagem;
- linhas 13–17: o seu subtipo é recuperado;
- linhas 20–30: processamos os dois tipos de conteúdo: [text/plain] (linhas 23–25) e [text/HTML] (linhas 26–28). Outros tipos [text/xx] são ignorados;
- linha 24: o texto da parte [text/plain] será guardado num ficheiro chamado [message.txt];
- linha 27: o texto da secção [text/HTML] será guardado num ficheiro [message.HTML];
- linhas 31–43: tratamos os casos em que o tipo principal não é [text];
- linha 35: apenas os anexos da mensagem são considerados;
- linha 37: estes são guardados num ficheiro utilizando a função [saveAttachment];
Para resumir o código anterior:
- guarda as partes [text/plain] e [text/HTML] utilizando a função [saveText]. Estas partes representam o conteúdo do e-mail;
- guarda os vários anexos utilizando a função [saveAttachment];
A função [saveText] funciona da seguinte forma:
// sauvegarde du texte [$text] du message
function saveText(string $fileName, int $type, string $text, object $infosMail, object $struct) {
// préparation du texte à sauvegarder
// $text est encodé - on le décode
switch ($struct->encoding) {
case ENCBASE64:
$text = base64_decode($text);
break;
case ENCQUOTEDPRINTABLE:
$text = quoted_printable_decode($text);
break;
}
// entêtes du message
// from
$from = "From: ";
foreach ($infosMail->from as $expéditeur) {
$from .= $expéditeur->mailbox . "@" . $expéditeur->host . ";";
}
// to
$to = "To: ";
foreach ($infosMail->to as $destinataire) {
$to .= $destinataire->mailbox . "@" . $destinataire->host . ";";
}
// subject
$subject = "Subject: " . $infosMail->subject;
// création du texte à enregistrer
switch ($type) {
case 0:
// text/plain
$contents = "$from\n$to\n$subject\n\n$text";
break;
case 1:
// text/HTML
$contents = "$from<br/>\n$to<br/>\n$subject<br/>\n<br/>\n$text";
break;
}
// création du fichier
print "sauvegarde d'un message dans [$fileName]\n";
// création du fichier
if (! file_put_contents($fileName, $contents)) {
// échec de la création du fichier
print "Impossible de créer le fichier [$fileName]\n";
}
}
Comentários
- linha 1:
- [$fileName] é o nome do ficheiro no qual o texto [$text] será guardado;
- [$type]: é 0 para um ficheiro de texto, 1 para um ficheiro HTML;
- [$text]: é o texto a ser guardado. Mas tem de ser primeiro descodificado, porque está codificado;
- [$infosMail]: contém informações gerais sobre o e-mail. Iremos utilizar os campos [from, to, subject];
- [$struct]: é a estrutura que descreve a parte do e-mail que estamos a guardar. Isto permitir-nos-á determinar o tipo de codificação do texto a ser guardado;
- linhas 4–12: descodificamos o texto a ser guardado;
- linhas 13–25: recuperamos as informações [from, to, subject] do e-mail;
- linhas 27–36: dependendo do tipo (0 ou 1) do texto a ser guardado, construímos texto simples (linha 30) ou texto HTML (linha 34);
- linha 40: todo o texto é guardado no ficheiro [$fileName];
Os anexos são guardados utilizando a seguinte função [saveAttachment]:
// sauvegarde d'un attachement
function saveAttachment($imapResource, int $msgNumber, string $sectionNumber, string $outputDir, object $struct) {
// on analyse la structure de l'attachement
// on cherche à récupérer le nom du fichier dans lequel sauvegarder l'attachement
// ce nom se trouve dans les [dparameters] de la structure
if (isset($struct->dparameters)) {
// on récupère les [dparameters]
$dparameters = $struct->dparameters;
$fileName = "";
// on parcourt le tableau des [dparameters]
foreach ($dparameters as $dparameter) {
// chaque [dparameter] est un objet avec deux attributs [attribute, value]
$attribute = strtolower($dparameter->attribute);
// l'attribut [filename] correspond au nom du fichier à créer
// dans ce cas le nom du fichier est dans [$dparameter->value]
if ($attribute === "filename") {
$fileName = $dparameter->value;
break;
}
}
// si on n'a pas trouvé de nom de fichier, on regarde dans l'attribut [parameters] de la structure
if ($fileName === "" && isset($struct->parameters)) {
// on récupère les [parameters]
$parameters = $struct->parameters;
foreach ($parameters as $parameter) {
// chaque paramètre est un dictionnaire à deux clés [attribute, value]
$attribute = strtolower($parameter->attribute);
// si l'attribut est [name], alors le [value] est le nom du fichier
if ($attribute === "name") {
$fileName = $parameter->value;
// le nom du fichier peut être encodé
// par exemple =?utf-8?Q?Cours-Tutoriels-Serge-Tah=C3=A9-1568x268=2Ep
// on récupère l'encodage avec une expression régulière
$champs = [];
$match = preg_match("/=\?(.+?)\?/", $fileName, $champs);
// si concordance, alors on décode le nom du fichier
if ($match) {
$fileName = iconv_mime_decode($fileName, 0, $champs[1]);
}
break;
}
}
}
}
// si on a trouvé un nom de fichier, alors on sauvegarde l'attachement
if ($fileName !== "") {
// sauvegarde de l'attachement
$fileName = "$outputDir/$fileName";
print "sauvegarde de l'attachement dans [$fileName]\n";
// création fichier
if ($file = fopen($fileName, "w")) {
// on récupère le texte encodé de l'attachement
$text = imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
// l'attachement est encodé - on le décode
switch ($struct->encoding) {
// base 64
case ENCBASE64:
$text = base64_decode($text);
break;
// quoted printable
case ENCQUOTEDPRINTABLE:
$text = quoted_printable_decode($text);
break;
default:
// on ignore les autres cas
break;
}
// écriture du texte dans le fichier
fputs($file, $text);
// fermeture fichier
fclose($file);
} else {
// échec de la création du fichier
print "L'attachement n'a pu être sauvegardé dans [$fileName]\n";
}
}
}
Comentários
- linha 2: a função [saveAttachment] aceita os seguintes parâmetros:
- [$imapResource, int $msgNumber, string $sectionNumber] identificam de forma única a secção IMAP a ser guardada;
- [string $outputDir] é o diretório de gravação;
- [object $struct] descreve a estrutura da parte da mensagem a ser guardada;
- linhas 6–44: procuramos o nome do ficheiro associado ao anexo. Utilizaremos este mesmo nome de ficheiro para o guardar. O nome do ficheiro do anexo encontra-se na matriz [$struct→dparameters] ou na matriz [$struct→parameters], ou em ambas;
- linhas 30–40: se o nome do ficheiro contiver caracteres não codificados em 7 bits, então foi codificado em [quoted-printable]. Neste caso, em [$struct→dparameters], o atributo chama-se [fileName*] em vez de [fileName]. Isto significa que não satisfez a condição na linha 16. O nome do ficheiro é então procurado na matriz [$struct→parameters];
- linha 32: um exemplo de um nome de ficheiro codificado. Tem a seguinte forma: =?codificação_original?codificação_atual?nome_codificado. Assim, o nome [=?utf-8?Q?Cours-Tutoriels-Serge-Tah=C3=A9-1568x268=2Ep] significa que o nome do ficheiro estava em UTF-8 e está atualmente em [quoted-printable] (Q);
- Linha 38: O nome do ficheiro é descodificado utilizando a função [iconv_mime_decode], que aqui recebe três parâmetros:
- a cadeia de caracteres a descodificar;
- definido como 0 por predefinição;
- o conjunto de caracteres a utilizar para representar a cadeia descodificada. Este parâmetro está presente na cadeia a descodificar. É obtido através de uma expressão regular nas linhas 34–35;
- linhas 45–75: o anexo é guardado num ficheiro com o nome que foi encontrado;
Para testar o script [imap-02.php], envie primeiro um e-mail para [guest@localhost] com a seguinte configuração:
Existem, portanto, cinco anexos.
Lemos o e-mail enviado utilizando [imap-02.php] e a seguinte configuração:
A saída da consola é a seguinte:
------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [1] messages dans la boîte à lettres [{localhost:110/pop3}]
Récupération de la liste des messages non lus de la boîte à lettres [{localhost:110/pop3}]
-----contenu de la partie n° [0]
Content-Type: MULTIPART/MIXED
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1]
Content-Type: MULTIPART/ALTERNATIVE
Transfer-Encoding : 7 bits
-----contenu de la partie n° [1.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : quoted-printable
sauvegarde d'un message dans [output/localhost-pop3/message-1/message.txt]
-----contenu de la partie n° [1.2]
Content-Type: TEXT/HTML
Transfer-Encoding : quoted-printable
sauvegarde d'un message dans [output/localhost-pop3/message-1/message.HTML]
-----contenu de la partie n° [2]
Content-Type: APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/Hello from SwiftMailer.docx]
-----contenu de la partie n° [3]
Content-Type: APPLICATION/PDF
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/Hello from SwiftMailer.pdf]
-----contenu de la partie n° [4]
Content-Type: APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/Hello from SwiftMailer.odt]
-----contenu de la partie n° [5]
Content-Type: UNKNOWN/PNG
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/Cours-Tutoriels-Serge-Tahé-1568x268.png]
-----contenu de la partie n° [6]
Content-Type: MESSAGE/RFC822
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/test-localhost.eml]
Fermeture de la connexion réussie.
Done.
Os ficheiros guardados podem ser encontrados na pasta [output/localhost-pop3/message-N]:

16.6.6. Cliente POP3/IMAP utilizando a biblioteca [php-mime-mail-parser]
No script anterior [imap-02.php], conseguimos guardar:
- o conteúdo [text/plain] e [text/HTML] do e-mail;
- os anexos do e-mail;
Para um anexo do tipo [message/rfc822], também guardámos o conteúdo do anexo. No entanto, este tipo de anexo é, ele próprio, um e-mail, que, por sua vez, tem conteúdo [text/plain] e [text/HTML], bem como anexos. Podemos então deparar-nos com a seguinte situação:
- um [e-mail 1] cuja estrutura é análoga à de um anexo [message/rfc822];
- um [e-mail 2] anexado ao e-mail 1;
- um [e-mail 3] anexado ao e-mail 2;
- etc…
O script [imap-02.php] guarda o conteúdo do [e-mail 1] (texto e anexos). Guarda o [e-mail 2] como um documento anexado, mas fica por aí. Não tenta analisar o [e-mail 2] para extrair o texto e os anexos. Poder-se-ia pensar que bastaria simplesmente aplicar ao [e-mail 2] o que foi feito para o [e-mail 1]. Uma chamada recursiva ao método que processou o [e-mail 1] poderia então ser suficiente para obter o conteúdo de todos os e-mails aninhados. Infelizmente, as partes do [e-mail 2] são numeradas utilizando uma lógica diferente da utilizada para o [e-mail 1], o que impede a utilização do mesmo algoritmo em ambos os casos, a menos que se empregue uma lógica bastante complexa para calcular os números das partes de um e-mail, independentemente da sua posição dentro do conjunto de e-mails aninhados.
O script [imap-02.php] já era complexo. Para evitar torná-lo ainda mais complexo ao lidar com o conteúdo de e-mails aninhados, utilizaremos a biblioteca [php-mime-mail-parser] disponível no GitHub (maio de 2019) no URL [https://github.com/php-mime-mail-parser/php-mime-mail-parser] e escrita por Vincent Dauce.
16.6.6.1. Instalação da biblioteca [php-mime-mail-parser]
A página de visão geral da biblioteca explica como instalá-la no Windows:

Existem dois passos para o Windows:
télécharger une DLL ;
modifier le fichier [php.ini] qui configure PHP ;
A biblioteca DLL [mailparse] está disponível no URL [http://pecl.php.net/package/mailparse] (maio de 2019);

- Em [2], escolha a versão mais recente e estável da biblioteca;

- em [3], selecione a versão do PHP que está a utilizar (neste documento, é o PHP 7.2);
- Em [4], selecione a versão do seu sistema operativo Windows (aqui é o Windows de 64 bits). Escolha a versão [Thread Safe];
Para saber qual a versão do PHP que foi descarregada com o Laragon, abra um [Terminal] a partir da janela do Laragon e digite o seguinte comando:
C:\myprograms\laragon-lite\www
λ php -v
PHP 7.2.11 (cli) (built: Oct 10 2018 02:04:07) ( ZTS MSVC15 (Visual C++ 2017) x64 )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
A versão PHP 7.2.11 está indicada na linha 3. A mesma linha indica a versão do Windows utilizada para a compilação (32 bits ou 64 bits).
Depois de obter a DLL, deve copiá-la para a pasta [<laragon>/bin/php/<php-version>/ext] [5]:

Depois de fazer isso, deve ativar esta extensão no ficheiro [php.ini] que configura o PHP (consulte a secção indicada):

É provável que a linha [7] não exista e que tenha de a adicionar manualmente.
Depois de ativar a extensão, pode verificar a sua validade digitando o seguinte comando num terminal do Laragon:
C:\myprograms\laragon-lite\www
λ php --ini
Configuration File (php.ini) Path: C:\windows
Loaded Configuration File: C:\myprograms\laragon-lite\bin\php\php-7.2.11-Win32-VC15-x64\php.ini
Scan for additional .ini files in: (none)
Additional .ini files parsed: (none)
O comando [php –-ini] carrega o ficheiro de configuração a partir da linha 4. Em seguida, carregará as DLLs para todas as extensões ativadas no [php.ini]. Se alguma delas estiver incorreta, isso será reportado. Assim, a validade da DLL adicionada [php_mailparse.dll] será verificada. Pode ser declarada incorreta por várias razões, sendo as mais comuns as seguintes:
- baixou uma DLL que não corresponde à versão do PHP que está a ser utilizada;
- baixou uma DLL de 32 bits quando tem um PHP de 64 bits, ou vice-versa;
Assim que a extensão estiver ativada e verificada, pode prosseguir com a instalação da biblioteca [php-mime-mail-parser]:

Introduza o comando [8] num terminal Laragon (ver link no parágrafo):

- Em [1], verifique se está no diretório [<laragon>/www];
- Em [2], o comando para instalar a biblioteca [php-mime-mail-parser];
- em [3], nada foi instalado aqui porque a biblioteca [php-mime-mail-parser] já estava instalada;
A biblioteca [php-mime-mail-parser] está instalada na pasta [<laragon>/www/vendor]:


- em [2-3], os ficheiros-fonte da biblioteca [php-mime-mail-parser];
Agora que o ambiente de trabalho está configurado, podemos passar à escrita do script [imap-03.php].
16.6.6.2. O script [imap-03.php]
O script [imap-03.php] utiliza o mesmo ficheiro de configuração [config-imap-01.json] que os scripts anteriores:
O script [imap-03.php] é o seguinte:
<?php
// IMAP (Internet Message Access Protocol) client for reading e-mails
// written with the [php-mime-mail-parser] library
// available at URL [https://github.com/php-mime-mail-parser/php-mime-mail-parser] (May 2019)
//
// strict adherence to declared types of function parameters
declare (strict_types=1);
// error management
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencies
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
// mail reading parameters
const CONFIG_FILE_NAME = "config-imap-01.json";
// we retrieve the configuration
if (!file_exists(CONFIG_FILE_NAME)) {
print "Le fichier de configuration " . CONFIG_FILE_NAME . " n'existe pas";
exit;
}
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// letterbox reading
foreach ($mailboxes as $name => $infos) {
// follow-up
print "------------Lecture de la boîte à lettres [$name]\n";
// reading the mailbox
readmailbox($name, $infos);
}
// end
exit;
Comentários
- linhas 18–23: o conteúdo do ficheiro de configuração é colocado no dicionário [$mailboxes];
- linhas 26–31: Cada caixa de correio é lida pela função [readmailbox] (linha 30). Esta função lê efetivamente as mensagens não lidas da caixa de correio. Uma caixa de correio corresponde ao endereço de e-mail de um determinado utilizador;
A função [readmailbox] é a seguinte:
function readmailbox(string $name, array $infos): void {
// we connect
$imapResource = imap_open($name, $infos["user"], $infos["password"]);
if (!$imapResource) {
// failure
print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
exit;
}
// Connection established
print "Connexion établie avec le serveur [$name].\n";
// total messages in mailbox
$nbmsg = imap_num_msg($imapResource);
print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
// unread messages in current mailbox
if ($nbmsg > 0) {
print "Récupération de la liste des messages non lus de la boîte à lettres [$name]\n";
$msgNumbers = imap_search($imapResource, 'UNSEEN');
if ($msgNumbers === FALSE) {
print "Il n'y a pas de nouveaux messages dans la boîte à lettres [$name]\n";
} else {
// browse the list of unread messages
foreach ($msgNumbers as $msgNumber) {
print "---message n° [$msgNumber]\n";
// we retrieve the body of message n° $msgNumber
getMailBody($imapResource, $msgNumber, $infos);
// if the protocol is POP3, we delete the message after retrieving it
$pop3 = $infos["pop3"];
if ($pop3 !== NULL) {
// mark the message as "to be deleted
imap_delete($imapResource, $msgNumber);
}
}
// end unread messages
if ($pop3 !== NULL) {
// messages marked as "to be deleted" are deleted
imap_expunge($imapResource);
}
}
}
// closing the connection
$imapClose = imap_close($imapResource);
if (!$imapClose) {
// failure
print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
} else {
// success
print "Fermeture de la connexion réussie.\n";
}
}
Comentários
O código da função [readmailbox] é o mesmo dos scripts anteriores.
A função [getMailBody] (linha 25), que analisa o corpo de uma mensagem (conteúdo + anexos), é a seguinte:
// analyse du corps du message
function getMailBody($imapResource, int $msgNumber, array $infos): void {
// on récupère le texte entier du message
$text = imap_fetchbody($imapResource, $msgNumber, "");
if ($text === FALSE) {
print "Le corps du message [$msgNumber] n'a pu être récupéré";
return;
}
// on crée un parseur qui va analyser le texte du message
$parser = (new PhpMimeMailParser\Parser())->setText($text);
// on récupère les différentes parties du message
$outputDir = $infos["output-dir"] . "/message-$msgNumber";
getParts($parser, $msgNumber, $outputDir);
}
Comentários
- linha 2: a função [getMailBody] aceita três parâmetros:
- [$imapResource]: o recurso IMAP ao qual está ligado;
- [$msgNumber]: o número da mensagem (na caixa de correio) a ser processada;
- [$infos]: várias informações sobre a caixa de correio que está a ser processada;
- linha 4: a mensagem completa #[$msgNumber] é recuperada;
- linhas 5–8: caso em que o conteúdo da mensagem não pôde ser recuperado;
- linha 10: começamos a utilizar a biblioteca [php-mime-mail-parser]. O objeto [$parser] será responsável por analisar o texto da mensagem;
- Linha 12: [$outputDir] será a pasta onde o conteúdo de texto e os anexos da mensagem #[$msgNumber] serão guardados;
- linha 13: solicitamos à função [getParts] que localize as diferentes partes (conteúdo de texto e anexos) da mensagem #[$msgNumber] e as guarde na pasta [$outputDir];
A função [getParts] é a seguinte:
// récupération des différentes parties d'un message
function getParts(PhpMimeMailParser\Parser $parser, int $msgNumber, string $outputDir): void {
// on crée le dossier de sauvegarde du message si besoin est
if (!file_exists($outputDir)) {
if (!mkdir($outputDir)) {
print "Le dossier [$outputDir] n'a pu être créé\n";
return;
}
}
// on récupère les entêtes du message
$arrayHeaders = $parser->getHeaders();
// on sauvegarde les messages texte
$parts = $parser->getInlineParts("text");
for ($i = 1; $i <= count($parts); $i++) {
print "-- Sauvegarde d'un message de type [text/plain]\n";
saveMessage($parts[$i - 1], 0, $arrayHeaders, "$outputDir/message_$i.txt");
}
// on sauvegarde les messages html
$parts = $parser->getInlineParts("html");
for ($i = 1; $i <= count($parts); $i++) {
print "-- Sauvegarde d'un message de type [text/html]\n";
saveMessage($parts[$i - 1], 1, $arrayHeaders, "$outputDir/message_$i.html");
}
// on récupère les attachements du message
$attachments = $parser->getAttachments();
// n° de l'attachement
$iAttachment = 0;
// on parcourt la liste des attachements
foreach ($attachments as $attachment) {
// type d'attachement
$fileType = $attachment->getContentType();
print "-- Sauvegarde d'un attachement de type [$fileType] dans le fichier [$outputDir/{$attachment->getFilename()}]\n";
// on sauvegarde l'attachement
try {
$attachment->save($outputDir, PhpMimeMailParser\Parser::ATTACHMENT_DUPLICATE_SUFFIX);
} catch (Exception $e) {
print "L'attachement n'a pu être sauvegardé : " . $e->getMessage() . "\n";
}
// cas particulier du type message/rfc822
if ($fileType === "message/rfc822") {
// l'attachement est lui-même un message - on va le parser lui aussi
// on change de répertoire de sauvegarde
$iAttachment++;
$outputDir = $outputDir . "/rfc822-$iAttachment";
// on change le contenu à parser
$parser->setText($attachment->getContent());
// on analyse le message de façon récursive
getParts($parser, $msgNumber, $outputDir);
}
}
}
Comentários
- linha 2: a função [getParts] recebe três parâmetros:
- um analisador [$parser] ao qual foi passado todo o texto da mensagem a ser analisada;
- [$msgNumber] é o número da mensagem atualmente a ser analisada;
- [$outputDir] é o diretório onde o conteúdo da mensagem e os anexos devem ser guardados;
- linhas 4–9: criação da pasta [$outputDir];
- linha 11: recuperação dos cabeçalhos da mensagem que está a ser analisada (de, para, assunto, etc.);
- linha 13: recuperar as partes do e-mail com o tipo [text/plain]. Recuperar uma matriz;
- linhas 14–17: guardar todos os elementos da matriz recuperada, atribuindo a cada um um nome de ficheiro diferente;
- linha 19: recuperar as partes do e-mail com o tipo [text/html]. É recuperada uma tabela;
- linhas 20–23: guardamos todos os elementos da matriz recuperada, atribuindo a cada um um nome de ficheiro diferente;
- linha 25: recuperar a lista de anexos da mensagem analisada;
- linha 29: percorremos esta lista;
- linha 24: recupera o tipo de anexo (atributo Content-Type);
- linhas 34–38: guardamos o anexo na pasta [$outputDir]. O segundo parâmetro [PhpMimeMailParser\Parser::ATTACHMENT_DUPLICATE_SUFFIX] é uma estratégia de nomenclatura para anexos. Se [$attachment→getFilename()] for X e o ficheiro X já existir, então a biblioteca [php-mime-mail-parser] tenta os nomes [X_1], [X_2], etc., até encontrar um nome de ficheiro que não exista;
- linha 40: verificamos se o anexo é um e-mail;
- linhas 41–48: se for o caso, este e-mail é, por sua vez, analisado para extrair o seu conteúdo e anexos;
- Linha 44: Se [$outputDir] estiver definido como X e a mensagem analisada contiver dois anexos de e-mail, então o primeiro será guardado na pasta [$outputDir/rfc822-1] e o segundo na pasta [$outputDir/rfc822-2];
- linha 46: o conteúdo do e-mail anexado torna-se o novo texto a ser analisado;
- linha 48: a função [getParts] é chamada recursivamente para analisar o novo texto;
A função [saveMessage] guarda o conteúdo de texto da mensagem a ser analisada:
// sauvegarde d'un message texte
function saveMessage(string $text, int $type, array $arrayHeaders, string $filename): void {
// contenu à sauvegarder
$contents = "";
// ajout des entêtes
switch ($type) {
case 0:
// text/plain
foreach ($arrayHeaders as $key => $value) {
$contents .= "$key: $value\n";
}
$contents .= "\n";
break;
case 1:
// text/HTML
foreach ($arrayHeaders as $key => $value) {
$contents .= "$key: $value<br/>\n";
}
$contents .= "<br/>\n";
}
// ajout du texte du message
$contents .= $text;
// sauvegarde du tout
if (!file_put_contents($filename, $contents)) {
// échec
print "Le message n'a pu être sauvegardé dans le fichier [$filename]\n";
} else {
// réussite
print "Le message a été sauvegardé dans le fichier [$filename]\n";
}
}
Comentários
- A função [saveMessage] aceita os seguintes parâmetros:
- [$text]: o texto a ser guardado;
- [$type]: o tipo de texto (0: text/plain, 1: text/HTML);
- [$arrayHeaders]: os cabeçalhos da mensagem analisada;
- [$filename]: o nome do ficheiro no qual [$text] deve ser guardado;
- linha 4: [$contents] representará todo o texto a ser guardado;
- linhas 6–20: primeiro, todos os cabeçalhos da mensagem (de, para, assunto, etc.) serão guardados;
- linhas 16–19: para texto HTML, cada linha termina com a tag <br/> para que cada cabeçalho apareça numa linha separada no navegador;
- linha 22: o texto da mensagem a ser guardado é adicionado aos cabeçalhos;
- linhas 24–30: todo o conjunto é guardado no ficheiro [$filename];
A utilização da biblioteca [php-mime-mail-parser] simplifica consideravelmente a escrita do script de leitura de e-mail.
O script [smtp-02.php] é utilizado para enviar um e-mail ao utilizador [guest@localhost] com a seguinte configuração:
- Linhas 11–15: Existem cinco anexos;
- linha 15: [test-localhost-2.eml] é um e-mail estruturado da seguinte forma:
- [test-localhost-2.eml] contém 4 anexos (os mesmos das linhas 11–14) e um e-mail anexado;
- o e-mail anexado a [test-localhost-2.eml] contém 4 anexos (os mesmos das linhas 11–14);
O script [imap-03.php] é utilizado para ler a caixa de correio do utilizador [guest@localhost] com a seguinte configuração:
Após a execução, a estrutura de diretórios da pasta [output/localhost-pop3] ficou da seguinte forma:

- em [1], os 5 anexos do e-mail recebido por [guest@localhost];
- em [2], os 5 anexos do e-mail [test-localhost-2.eml] em [1];
- em [3], os 4 anexos do e-mail [test-localhost.eml] em [2];
A saída da consola é a seguinte:
------------Lecture de la boîte à lettres [{localhost:110/pop3}]
Connexion établie avec le serveur [{localhost:110/pop3}].
Il y a [1] messages dans la boîte à lettres [{localhost:110/pop3}]
Récupération de la liste des messages non lus de la boîte à lettres [{localhost:110/pop3}]
---message n° [1]
-- Sauvegarde d'un message de type [text/plain]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/message_1.txt]
-- Sauvegarde d'un message de type [text/html]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/message_1.html]
-- Sauvegarde d'un attachement de type [application/vnd.openxmlformats-officedocument.wordprocessingml.document] dans le fichier [output/localhost-pop3/message-1/Hello from SwiftMailer.docx]
-- Sauvegarde d'un attachement de type [application/pdf] dans le fichier [output/localhost-pop3/message-1/Hello from SwiftMailer.pdf]
-- Sauvegarde d'un attachement de type [application/vnd.oasis.opendocument.text] dans le fichier [output/localhost-pop3/message-1/Hello from SwiftMailer.odt]
-- Sauvegarde d'un attachement de type [image/png] dans le fichier [output/localhost-pop3/message-1/Cours-Tutoriels-Serge-Tahé-1568x268.png]
-- Sauvegarde d'un attachement de type [message/rfc822] dans le fichier [output/localhost-pop3/message-1/test-localhost-2.eml]
-- Sauvegarde d'un message de type [text/plain]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/message_1.txt]
-- Sauvegarde d'un message de type [text/html]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/message_1.html]
-- Sauvegarde d'un attachement de type [application/vnd.openxmlformats-officedocument.wordprocessingml.document] dans le fichier [output/localhost-pop3/message-1/rfc822-1/Hello from SwiftMailer.docx]
-- Sauvegarde d'un attachement de type [application/pdf] dans le fichier [output/localhost-pop3/message-1/rfc822-1/Hello from SwiftMailer.pdf]
-- Sauvegarde d'un attachement de type [application/vnd.oasis.opendocument.text] dans le fichier [output/localhost-pop3/message-1/rfc822-1/Hello from SwiftMailer.odt]
-- Sauvegarde d'un attachement de type [image/png] dans le fichier [output/localhost-pop3/message-1/rfc822-1/Cours-Tutoriels-Serge-Tahé-1568x268.png]
-- Sauvegarde d'un attachement de type [message/rfc822] dans le fichier [output/localhost-pop3/message-1/rfc822-1/test-localhost.eml]
-- Sauvegarde d'un message de type [text/plain]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/message_1.txt]
-- Sauvegarde d'un message de type [text/html]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/message_1.html]
-- Sauvegarde d'un attachement de type [application/vnd.openxmlformats-officedocument.wordprocessingml.document] dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Hello from SwiftMailer.docx]
-- Sauvegarde d'un attachement de type [application/pdf] dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Hello from SwiftMailer.pdf]
-- Sauvegarde d'un attachement de type [application/vnd.oasis.opendocument.text] dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Hello from SwiftMailer.odt]
-- Sauvegarde d'un attachement de type [image/png] dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Cours-Tutoriels-Serge-Tahé-1568x268.png]
Fermeture de la connexion réussie.
Se visualizar [message_1.HTML] a partir de [3] num navegador, obtém o seguinte:
