16. Funciones de red
Ahora abordaremos las funciones de red de PHP que nos permiten programar TCP / IP (Protocolo de control de transferencia / Protocolo de Internet).

16.1. Fundamentos de la programación en Internet
16.1.1. Generalidades
Consideremos la comunicación entre dos máquinas remotas A y B:

Cuando una aplicación AppA de la máquina A quiere comunicarse con una aplicación AppB de la máquina B en Internet, debe conocer varios datos:
- la dirección IP (Protocolo de Internet) o el nombre de la máquina B;
- el número del puerto con el que opera la aplicación AppB. De hecho, la máquina B puede albergar numerosas aplicaciones que operan en Internet. Cuando recibe información procedente de la red, debe saber a qué aplicación va dirigida dicha información. Las aplicaciones de la máquina B tienen acceso a la red a través de «ventanillas», también llamadas puertos de comunicación. Esta información se encuentra en el paquete recibido por la máquina B para que pueda entregarse a la aplicación correcta;
- los protocolos de comunicación que comprende la máquina B. En nuestro estudio, utilizaremos únicamente los protocolos TCP-IP;
- el protocolo de comunicación aceptado por la aplicación AppB. De hecho, las máquinas A y B se «comunicarán» entre sí. Lo que se comunicarán quedará encapsulado en los protocolos TCP-IP. Sin embargo, cuando, al final de la cadena, la aplicación AppB reciba la información enviada por la aplicación AppA, deberá ser capaz de interpretarla. Esto es similar a la situación en la que dos personas, A y B, se comunican por teléfono: su diálogo se transmite a través del teléfono. El habla será codificada en forma de señales por el teléfono de A, transportada por las líneas telefónicas, llegará al teléfono de B para ser decodificada allí. La persona B entonces escucha palabras. Aquí es donde entra en juego el concepto de protocolo de diálogo: si A habla francés y B no entiende ese idioma, A y B no podrán dialogar de manera efectiva;
Por lo tanto, las dos aplicaciones que se comunican deben ponerse de acuerdo sobre el tipo de diálogo que van a adoptar. Por ejemplo, el diálogo con un servicio ftp no es el mismo que con un servicio pop: estos dos servicios no aceptan los mismos comandos. Tienen un protocolo de diálogo diferente;
16.1.2. Las características del protocolo TCP
Aquí solo analizaremos las comunicaciones de red que utilizan el protocolo de transporte TCP, cuyas características principales son las siguientes:
- el proceso que desea transmitir establece primero una conexión con el proceso destinatario de la información que va a enviar. Esta conexión se realiza entre un puerto de la máquina emisora y un puerto de la máquina receptora. Entre ambos puertos se crea así una ruta virtual que quedará reservada exclusivamente para los dos procesos que han establecido la conexión;
- todos los paquetes enviados por el proceso de origen siguen esta ruta virtual y llegan en el orden en que fueron enviados;
- la información transmitida tiene un carácter continuo. El proceso emisor envía información a su propio ritmo. Esta información no se envía necesariamente de inmediato: el protocolo TCP espera tener suficiente cantidad para enviarla. Se almacena en una estructura llamada segmento TCP. Una vez que este segmento se llene, se transmitirá a la capa IP, donde se encapsulará en un paquete IP;
- cada segmento enviado por el protocolo TCP está numerado. El protocolo TCP receptor verifica que reciba los segmentos en orden. Por cada segmento recibido correctamente, envía un acuse de recibo al remitente;
- cuando este último lo recibe, se lo indica al proceso emisor. De esta manera, este último puede saber que un segmento ha llegado a su destino;
- si, al cabo de cierto tiempo, el protocolo TCP que ha enviado un segmento no recibe un acuse de recibo, vuelve a transmitir el segmento en cuestión, garantizando así la calidad del servicio de transmisión de la información;
- el circuito virtual establecido entre los dos procesos que se comunican es full-duplex: esto significa que la información puede circular en ambos sentidos. De este modo, el proceso de destino puede enviar acuses de recibo incluso mientras el proceso de origen continúa enviando información. Esto permite, por ejemplo, que el protocolo de origen TCP envíe varios segmentos sin esperar un acuse de recibo. Si, tras un cierto tiempo, se da cuenta de que no ha recibido el acuse de recibo de un segmento específico n.º n, reanudará la transmisión de los segmentos a partir de ese punto;
16.1.3. La relación cliente-servidor
A menudo, la comunicación en Internet es asimétrica: la máquina A inicia una conexión para solicitar un servicio a la máquina B; especifica que desea establecer una conexión con el servicio SB1 de la máquina B. Esta última acepta o rechaza la solicitud. Si acepta, la máquina A puede enviar sus solicitudes al servicio SB1. Estas deben cumplir con el protocolo de comunicación que entiende el servicio SB1. De esta manera, se establece un diálogo de solicitud-respuesta entre la máquina A, a la que llamamos máquina cliente, y la máquina B, a la que llamamos máquina servidor. Uno de los dos participantes cerrará la conexión.
16.1.4. Arquitectura de un cliente
La arquitectura de un programa de red que solicita los servicios de una aplicación servidor será la siguiente:
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. Arquitectura de un servidor
La arquitectura de un programa que ofrece servicios será la siguiente:
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
El programa servidor procesa de manera diferente la solicitud de conexión inicial de un cliente y sus solicitudes posteriores para obtener un servicio. El programa no proporciona el servicio por sí mismo. Si lo hiciera, durante el tiempo que durara el servicio ya no estaría atento a las solicitudes de conexión y, por lo tanto, los clientes no recibirían el servicio. Por lo tanto, procede de otra manera: tan pronto como se recibe una solicitud de conexión en el puerto de escucha y esta es aceptada, el servidor crea una tarea encargada de prestar el servicio solicitado por el cliente. Este servicio se presta en otro puerto de la máquina servidora llamado puerto de servicio. De esta manera, se puede atender a varios clientes al mismo tiempo.
Una tarea de servicio tendrá la siguiente estructura:
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. Descubre los protocolos de comunicación de Internet
16.2.1. Introducción
Cuando un cliente se conecta a un servidor, se establece un diálogo entre ambos. La naturaleza de este diálogo constituye lo que se conoce como el protocolo de comunicación del servidor. Entre los protocolos más comunes de Internet se encuentran los siguientes:
- HTTP: Protocolo de Transferencia HyperText —el protocolo de comunicación con un servidor web (servidor HTTP);
- SMTP: Protocolo simple de transferencia de correo (Simple Mail Transfer Protocol) —el protocolo de comunicación con un servidor de envío de correos electrónicos (servidor SMTP);
- POP: Protocolo de oficina postal (POP) —el protocolo de comunicación con un servidor de almacenamiento de correo electrónico (servidor POP). Se trata de recuperar los correos electrónicos recibidos y no de enviarlos;
- IMAP: Protocolo de acceso a mensajes de Internet (IMAP) —el protocolo de comunicación con un servidor de almacenamiento de correo electrónico (servidor IMAP). Este protocolo ha ido reemplazando gradualmente al protocolo POP, que es más antiguo;
- FTP: Protocolo de transferencia de archivos (FTP) —el protocolo de comunicación con un servidor de almacenamiento de archivos (servidor FTP);
Todos estos protocolos tienen la particularidad de ser protocolos de líneas de texto: el cliente y el servidor intercambian líneas de texto. Si contamos con un cliente capaz de:
- establecer una conexión con un servidor TCP;
- mostrar en la consola las líneas de texto que el servidor le envía;
- enviar al servidor las líneas de texto que un usuario escribiría en el teclado;
por lo que es posible comunicarse con un servidor TCP que utilice un protocolo de líneas de texto, siempre y cuando se conozcan las reglas de dicho protocolo.
16.2.2. Utilidades TCP

En los códigos asociados a este documento, se encuentran dos utilidades de comunicación TCP:
- [RawTcpClient] permite conectarse al puerto P de un servidor S;
- [RawTcpServer] permite crear un servidor que espera clientes en un puerto P;
El servidor TCP [RawTcpServer] sese invoca con la sintaxis [RawTcpServeur port] para crear un servicio TCP en el puerto [port] de la máquina local (la computadora en la que estás trabajando):
- el servidor puede atender a varios clientes al mismo tiempo;
- el servidor ejecuta los comandos que el usuario ingresa con el teclado. Estos son los siguientes:
- list: muestra una lista de los clientes actualmente conectados al servidor. Estos se muestran en el formato [id=x-nom=y]. El campo [id] sirve para identificar a los clientes;
- send x [texte]: envía texto al cliente n.º x (id=x). Los corchetes [] no se envían. Son necesarios en el comando. Sirven para delimitar visualmente el texto enviado al cliente;
- close x: cierra la conexión con el cliente n.º x;
- quit: cierra todas las conexiones y detiene el servicio;
- las líneas enviadas por el cliente al servidor se muestran en la consola;
- todo el intercambio se registra en un archivo de texto con el nombre [machine-portService.txt], donde
- [machine] es el nombre de la máquina en la que se ejecuta el código;
- [port] es el puerto del servicio que responde a las solicitudes del cliente;
El cliente TCP [RawTcpClient] se invoca con la sintaxis [RawTcpClient serveur port] para conectarse al puerto [port] del servidor [serveur]:
- las líneas que el usuario escribe en el teclado se envían al servidor;
- las líneas enviadas por el servidor se muestran en la consola;
- todo el intercambio se registra en un archivo de texto con el nombre [serveur-port.txt];
Veamos un ejemplo. Abrimos dos ventanas de comandos de Windows y nos ubicamos en cada una de ellas en la carpeta de utilidades. En una de las ventanas iniciamos el servidor [RawTcpServer] en el puerto 100:

- en [1], estamos en la carpeta de utilidades;
- en [2], iniciamos el servidor TCP en el puerto 100;
- en [3], el servidor queda a la espera de un cliente TCP;
- en [4], el servidor espera un comando que el usuario escriba en el teclado;
En la otra ventana de comandos, se inicia el cliente TCP:

- en [5], nos encontramos en la carpeta de utilidades;
- en [6], iniciamos el cliente TCP: le indicamos que se conecte al puerto 100 de la máquina local (aquella con la que estás trabajando);
- en [7], el cliente logró conectarse al servidor. Se indican los datos del cliente: se encuentra en la máquina [DESKTOP-528I5CU] (la máquina local en este ejemplo) y utiliza el puerto [50405] para comunicarse con el servidor:
- en [8], el cliente espera un comando que el usuario escriba en el teclado;
Volvamos a la ventana del servidor. Su contenido ha cambiado:

- a [9], se ha detectado un cliente. El servidor le ha asignado el n.º 1. El servidor ha identificado correctamente al cliente remoto (equipo y puerto);
- en [10], el servidor vuelve a quedar en espera de un nuevo cliente;
Volvamos a la ventana del cliente y enviemos un comando al servidor:

- en [11], el comando enviado al servidor;
Volvamos a la ventana del servidor. Su contenido ha cambiado:

- en [12], entre corchetes, el mensaje recibido por el servidor;
Enviemos una respuesta al cliente:

- a [13], la respuesta enviada al cliente 1. Solo se envía el texto entre corchetes, no los corchetes mismos;
Volvamos a la ventana del cliente:

- en [14], la respuesta recibida por el cliente. El texto recibido es el que está entre corchetes;
Volvamos a la ventana del servidor para ver otros comandos:

- en [15], solicitamos la lista de clientes;
- en [16], la respuesta;
- en [17], cerramos la conexión con el cliente n.º 1;
- en [18], la confirmación del servidor;
- en [19], apagamos el servidor;
- en [20], la confirmación del servidor;
Volvamos a la ventana del cliente:

- en [21], el cliente ha detectado el fin del servicio;
Se han creado dos archivos de registro, uno para el servidor y otro para el cliente:

- en [25], los registros del servidor: el nombre del archivo es el nombre del cliente [machine-port];
- en [26], los registros del cliente: el nombre del archivo es el nombre del servidor [machine-port];
Los registros del servidor son los siguientes:
Los registros del cliente son los siguientes:
16.3. Obtener el nombre o la dirección IP de una máquina en Internet

Los equipos de Internet se identifican mediante una dirección IP (IPv4 o IPv6) y, en la mayoría de los casos, mediante un nombre. Sin embargo, al final solo se utiliza la dirección IP. Por lo tanto, a veces es necesario conocer la dirección IP de una máquina identificada por su nombre.
El script [ip-01.php] es el siguiente:
<?php
// cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
//
// manejo de errores
error_reporting(E_ALL & E_STRICT);
ini_set("display_errors", "on");
//
// constantes
$HOTES = array("istia.univ-angers.fr", "www.univ-angers.fr", "www.ibm.com", "localhost", "", "xx");
// direcciones IP y nombres de las máquinas de $HOTES
for ($i = 0; $i < count($HOTES); $i++) {
getIPandName($HOTES[$i]);
}
// fin
print "Terminé\n";
exit;
//------------------------------------------------
function getIPandName(string $nomMachine): void {
//$nomMachine: nombre de la máquina cuya dirección se desea obtener IP
//
// nomMachine-->dirección IP
$ip = gethostbyname($nomMachine);
print "---------------\n";
if ($ip !== $nomMachine) {
print "ip[$nomMachine]=$ip\n";
// dirección 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";
}
}
Comentarios
- líneas 7-8: se le pide a PHP que reporte todos los errores (E_ALL y E_STRICT) y que estos se muestren. Este modo solo se recomienda en modo de desarrollo para mejorar el código con las advertencias de PHP. En modo de producción, en la línea 8, se pondría «off». Desde la versión 5.4 de PHP, el nivel E_STRICT está incluido en E_ALL;
- línea 11: la lista de máquinas de las que se desea obtener el nombre y la dirección IP;
Las funciones de red de PHP se utilizan en la función getIpandName de la línea 21.
- línea 25: la función gethostbyname($nom) permite obtener la dirección IP «ip3.ip2.ip1.ip0» de la máquina llamada $nom. Si la máquina $nom no existe, la función devuelve $nom como resultado;
- línea 30: la función gethostbyaddr($ip) permite obtener el nombre de la máquina con la dirección $ip en el formato «ip3.ip2.ip1.ip0». Si la máquina $ip no existe, la función devuelve $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. El protocolo HTTP (Protocolo de transferencia HyperText)
16.4.1. Ejemplo 1

Cuando un navegador muestra un URL, actúa como cliente de un servidor web o, dicho de otra manera, de un servidor HTTP. Es él quien toma la iniciativa y comienza enviando una serie de comandos al servidor. Para este primer ejemplo:
- el servidor será la utilidad [RawTcpServer];
- el cliente será un navegador;
Primero iniciamos el servidor en el puerto 100:

Luego, con un navegador, solicitamos el URL [localhost:100], es decir, indicamos que el servidor HTTP al que nos dirigimos opera en el puerto 100 de la máquina local:

Volvamos a la ventana del servidor:

- en [3], el cliente que se conectó;
- en [4-7], la serie de líneas de texto que envió:
- en [4]: esta línea tiene el formato [GET URL HTTP/1.1]. Solicita el URL / y le pide al servidor que utilice el protocolo HTTP 1.1;
- en [5]: esta línea tiene el formato [Host: serveur:port]. Las mayúsculas y minúsculas del comando [Host] no importan. Cabe recordar aquí que el cliente consulta a un servidor local que opera en el puerto 100;
- el comando [User-Agent] proporciona la identidad del cliente;
- el comando [Accept] indica qué tipos de documentos acepta el cliente;
- el comando [Accept-Language] indica en qué idioma se desean los documentos solicitados, en caso de que existan en varios idiomas;
- el comando [Connection] indica el modo de conexión deseado: [keep-alive] indica que la conexión debe mantenerse hasta que finalicen los intercambios;
- en [7]: el cliente termina sus comandos con una línea en blanco;
Terminamos la conexión cerrando el servidor:

16.4.2. Ejemplo 2
Ahora que conocemos los comandos que envía un navegador para solicitar un URL, vamos a solicitar este URL con nuestro cliente TCP [RawTcpClient]. El servidor Apache de Laragon será nuestro servidor web.
Iniciemos Laragon y luego el servidor web Apache:


Ahora, con un navegador, solicitemos la página URL [http://localhost:80]. Aquí solo especificamos el servidor [localhost:80] y no el documento URL. En este caso, se solicita el URL, es decir, la raíz del servidor web:

- en [1], el URL solicitado. Inicialmente se escribió [http://localhost:80] y el navegador (Firefox en este caso) la transformó simplemente en [localhost], ya que el protocolo [http] se supone implícito cuando no se menciona ningún protocolo, y el puerto [80] se supone implícito cuando no se especifica el puerto;
- en [2], la página raíz / del servidor web consultado;
Ahora, veamos el texto recibido por el navegador:

- Haz clic con el botón derecho en la página recibida y selecciona la opción [2]. Obtendrás el siguiente código fuente:
Ahora solicitemos el URL y el [http://localhost:80] con nuestro cliente TCP:
![]()
- En [1], nos conectamos al puerto 80 del servidor localhost. Ahí es donde opera el servidor web de Laragon;
Ahora escribimos los comandos que descubrimos en el párrafo anterior:

- en [1], el comando [GET]. Solicitamos el directorio raíz / del servidor web;
- en [2], el comando [Host];
- estos son los únicos dos comandos imprescindibles. Para los demás comandos, el servidor web utilizará valores por defecto;
- en [3], la línea en blanco que debe cerrar los comandos del cliente;
- debajo de la línea 3, viene la respuesta del servidor web;
- desde [4] hasta la línea en blanco [5] se encuentran los encabezados HTTP de la respuesta del servidor;
- después de la línea [5] viene el documento HTML solicitado en [6];
Escribimos [quit] para cerrar el cliente y cargamos el archivo de registros [localhost-80.txt]:
- líneas 11-79: el documento HTML recibido. En el ejemplo anterior, Firefox había recibido el mismo;
Ahora contamos con los fundamentos para programar un cliente TCP que solicite un URL.
16.4.3. Ejemplo 3

El script [http-01.php] es un cliente HTTP configurado por el archivo jSON [config-http-01.json]. El contenido de este es el siguiente:
- línea 2: el nombre de la máquina que aloja el servidor web al que se desea acceder;
- línea 3: el puerto en el que opera este servidor web;
- línea 4: el URL del documento deseado;
- línea 5: la máquina de destino en el formato máquina:puerto;
- línea 6: la identificación del cliente HTTP: se puede poner lo que se quiera;
- línea 7: el tipo de documento aceptado por el cliente, en este caso texto HTML;
- línea 8: el idioma deseado para el documento solicitado;
- línea 9: el carácter de fin de línea para los comandos enviados por el cliente: de hecho, puede variar dependiendo de si el servidor está en una máquina Unix (\n) o Windows (\r\n);
El script [http-01.php] es el siguiente:
<?php
// cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
//
// manejo de errores
// error_reporting(E_ALL & E_STRICT);
// ini_set("display_errors", "on");
//
// constantes
const CONFIG_FILE_NAME = "config-http-01.json";
//
// se recupera la configuración
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// obtener el texto HTML de URL del archivo de configuración
foreach ($config as $site => $protocole) {
// lectura de la página de inicio del sitio $ite
$résultat = getURL($site, $protocole);
// Mostrar el resultado
print "$résultat\n";
}//para
// fin
exit;
//-----------------------------------------------------------------------
function getURL(string $site, array $protocole, $suivi = TRUE): string {
// lee el archivo URL $site["GET"] y lo guarda en el archivo $site.HTML
// el diálogo entre el cliente y el servidor se realiza según el protocolo $protocole
//
// apertura de una conexión en el puerto de $site
$erreurNumber = 0;
$erreur = "";
$connexion = fsockopen($site, $protocole["port"], $erreurNumber, $erreur);
// retorno en caso de error
if ($connexion === FALSE) {
return "Echec de la connexion au site (" . $site . " ," . $protocole["port"] . " : $erreur";
}
// $connexion representa un flujo de comunicación bidireccional
// entre el cliente (este programa) y el servidor web contactado
// este canal se utiliza para el intercambio de comandos e información
// el protocolo de comunicación es HTTP
//
// creación del archivo $site.HTML
$HTML = fopen("output/$site.HTML", "w");
if ($HTML === FALSE) {
// Cierre de la conexión cliente/servidor
fclose($connexion);
// retorno de error
return "Erreur lors de la création du fichier $site.HTML";
}
// el cliente iniciará el diálogo HTTP con el servidor
if ($suivi) {
print "Client : début de la communication avec le serveur [$site] ----------------------------\n";
}
// dependiendo del servidor, las líneas del cliente deben terminar con \n o \r\n
$endOfLine = $protocole["endOfLine"];
// Para simplificar, no se comprueban los casos de error en la comunicación entre el cliente y el servidor
// El cliente envía el comando GET para solicitar el $protocolo["GET"]
// sintaxis GET URL HTTP/1.1
$commande = "GET " . $protocole["GET"] . " HTTP/1.1$endOfLine";
// ¿Seguimiento?
if ($suivi) {
print "--> $commande";
}
// se envía la orden al servidor
fputs($connexion, $commande);
// emisión de los demás encabezados HTTP
foreach ($protocole as $verb => $value) {
if ($verb !== "GET" && $verb != "port"" && $verb !="endOfLine") {
// se construye el comando
$commande = "$verb: $value$endOfLine";
// ¿Siguiente paso?
if ($suivi) {
print "--> $commande";
}
// se envía el comando al servidor
fputs($connexion, $commande);
}
}
// los encabezados (headers) del protocolo HTTP deben terminar con una línea en blanco
fputs($connexion, $endOfLine);
//
// el servidor responderá ahora en el canal $connexion. Enviará todos
// sus datos y luego cerrará el canal. Por lo tanto, el cliente lee todo lo que llega desde $connexion
// hasta que se cierre el canal
//
//; primero se leen los encabezados HTTP enviados por el servidor
// estas también terminan con una línea en blanco
if ($suivi) {
print "Réponse du serveur [$site] ----------------------------\n";
}
$fini = FALSE;
while (!$fini && $ligne = fgets($connexion, 1000)) {
// ¿Hay una línea en blanco?
$champs = [];
preg_match("/^(.*?)\s+$/", $ligne, $champs);
if ($champs[1] !== "") {
if ($suivi) {
// se muestra el encabezado HTTP
print "<-- " . $champs[1] . "\n";
}
} else {
// esa era la línea en blanco; las cabeceras HTTP han terminado
$fini = TRUE;
}
}
// se lee el documento HTML que seguirá a la línea en blanco
while ($ligne = fgets($connexion, 1000)) {
// se guarda la línea en el archivo HTML del sitio
fputs($HTML, $ligne);
}
// el servidor cerró la conexión; el cliente la cierra a su vez
fclose($connexion);
// cierre del archivo $HTML
fclose($HTML);
// regreso
return "Fin de la communication avec le site [$site]. Vérifiez le fichier [$site.HTML]";
}
Comentarios del código:
- línea 14: el archivo de configuración se utiliza para crear un diccionario:
- las claves del diccionario son los servidores web a los que se debe consultar;
- los valores establecen el protocolo HTTP que se debe seguir;
- líneas 16-21: se recorre la lista de servidores web de la configuración;
- línea 26: la función getURL($site,$protocole,$suivi) solicita un documento del sitio web $site y lo almacena en el archivo de texto $site.HTML.Par: por defecto, las comunicaciones entre el cliente y el servidor se registran en la consola ($suivi=TRUE);
- línea 33: la función fsockopen($site,$port,$errNumber,$erreur) permite establecer una conexión con un servicio TCP / IP que opera en el puerto $port de la máquina $site. Si la conexión falla, [$errNumber] es un número de error y [$erreur] es el mensaje de error asociado. Una vez establecida la conexión entre el cliente y el servidor, numerosos servicios TCP / IP intercambian líneas de texto. Este es el caso del protocolo HTTP (Protocolo de transferencia HyperText). El flujo del servidor que llega al cliente puede entonces procesarse como un archivo de texto leído con [fgets]. Lo mismo ocurre con el flujo que va del cliente al servidor, el cual puede escribirse con [fputs];
- líneas 44-50: creación del archivo [$site.HTML], en el que se almacenará el documento HTML recibido;
- línea 60: el primer comando del cliente debe ser el comando [GET URL HTTP/1.1];
- línea 66: la función fputs permite al cliente enviar datos al servidor. En este caso, la línea de texto enviada tiene el siguiente significado: «Quiero (GET) la página [URL] del sitio web al que estoy conectado. Trabajo con el protocolo HTTP, versión 1.1»;
- líneas 68-79: se envían las demás líneas del protocolo HTTP [Host, User-Agent, Accept, Accept-Language]. Su orden no importa;
- línea 81: se envía una línea vacía al servidor para indicar que el cliente ha terminado de enviar sus encabezados HTTP y que ahora espera el documento solicitado;
- líneas 92-106: el servidor enviará primero una serie de encabezados HTTP que proporcionarán diversa información sobre el documento solicitado. Estos encabezados terminan con una línea en blanco;
- línea 93: se lee una línea enviada por el servidor con la función PHP [fgets];
- línea 96: se recupera el cuerpo de la línea sin los espacios (espacios en blanco, carácter de fin de línea) del final de la línea;
- línea 97: se verifica si se ha recuperado la línea en blanco que marca el final de los encabezados HTTP enviados por el servidor;
- líneas 98-101: si estamos en modo [suivi], se muestra en la consola el encabezado HTTP recibido;
- líneas 108-111: las líneas de texto de la respuesta del servidor se pueden leer línea por línea con un bucle while y guardar en el archivo de texto [output/$site.HTML]. Cuando el servidor web ha enviado la página completa que se le solicitó, cierra su conexión con el cliente. Del lado del cliente, esto se detectará como un fin de archivo;
Resultados:
La consola muestra los siguientes registros:
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]
En nuestro ejemplo, el archivo [output/localhost.HTML] recibido es el siguiente:
<!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>
Efectivamente, obtuvimos el mismo documento que con el navegador Firefox.
16.4.4. Ejemplo 4
En este ejemplo, demostraremos que el cliente HTTP que escribimos no es suficiente. Modifiquemos el archivo de configuración [config-http-01.json] de la siguiente manera:
Aquí, vamos a solicitar el URL [http://tahe.developpez.com:443/]. El puerto 443 de la máquina [tahe.developpez.com] es un puerto utilizado para el protocolo HTTP seguro, conocido como HTTPS. En este protocolo, la comunicación entre el cliente y el servidor comienza con un intercambio de información que asegura la conexión. El cliente debe utilizar el protocolo [HTTPS] y no el protocolo [HTTP], lo cual no hace nuestro cliente.
Con este archivo de configuración, los resultados de la consola son los siguientes:
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]
- línea 8: el servidor [tahe.developpez.com] respondió que la solicitud del cliente era incorrecta;
El contenido del archivo [output/tahe.developpez.com.HTML] es entonces el siguiente:
<!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>
El servidor indica claramente que no hemos utilizado el protocolo correcto.
Ahora utilicemos el siguiente archivo de configuración:
Los resultados en la consola son los siguientes:
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]
- la línea 11 indica que el servidor envía el documento por partes;
Esto se traduce en la presencia de números en el flujo enviado al cliente: cada número le indica al cliente la cantidad de caracteres del siguiente fragmento que enviará el servidor. Así es como se ve en el archivo [output/sergetahe.com.HTML]:

- en [1] y [2], el tamaño en hexadecimal de los fragmentos 1 y 2 del documento;
Un cliente HTTP que funcione correctamente no debería dejar estos números en el documento HTML final.
He aquí otro ejemplo:
Se parece al ejemplo anterior, pero el URL solicitado en la línea 4 no tiene el carácter / para terminarlo. No son los mismos URL. Al ejecutar el cliente HTTP, se obtienen los siguientes resultados en la 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/cursos-tutoriales-de-programación/
<-- 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]
- la línea 8 indica que el documento solicitado ha cambiado a URL. El nuevo URL aparece en la línea 13. Observe esta vez el carácter / que termina el nuevo URL;
El archivo [output/serge.tahe.com.HTML] queda entonces así:
<!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>
Un cliente HTTP debería poder seguir las redirecciones. En este caso, debería volver a solicitar automáticamente el nuevo URL [http://sergetahe.com/cours-tutoriels-de-programmation/].
16.4.5. Ejemplo 5
Los ejemplos anteriores nos han demostrado que nuestro cliente HTTP era insuficiente. Ahora presentaremos una herramienta llamada [curl] que permite recuperar documentos web al manejar las dificultades mencionadas: protocolo HTTPS, documento enviado por partes, redirecciones… La herramienta [curl] se instaló con Laragon:

Abramos un terminal de Laragon [1]:

En el terminal, escribimos el siguiente comando:

- en [1], el tipo de consola;
- en [2], la carpeta actual. Esta carpeta es especial: es de ahí de donde el servidor Apache de Laragon obtiene los documentos que se le solicitan. Por lo tanto, evitaremos ensuciar esta carpeta;
- en [3], el comando ingresado;
Es posible que el comando [curl --help] genere un error. La causa más probable es que no tengas el tipo correcto de terminal. En ese caso, abre otra terminal con los comandos [4-6];
El comando [curl --help] muestra todas las opciones de configuración de [curl]. Hay varias decenas. Usaremos muy pocas de ellas. Para solicitar un URL, basta con escribir el comando [curl URL]. Este comando mostrará en la consola el documento solicitado. Si además queremos ver los intercambios HTTP entre el cliente y el servidor, escribiremos [curl --verbose URL]. Por último, para guardar el documento HTML solicitado en un archivo, escribiremos [curl --verbose --output fichier URL].
Para evitar saturar la carpeta [www] de Laragon, cambiemos a otra ubicación del sistema de archivos:

- en [1], nos desplazamos a la carpeta [c:\temp]. Si esta carpeta no existe, puedes crearla o elegir otra;
- en [2], creamos una carpeta llamada [curl];
- en [3], nos posicionamos sobre ella;
- en [4], se muestra su contenido. Está vacío;
Asegúrate de que el servidor Apache de Laragon esté en ejecución y, desde [curl], solicita los directorios URL y [http://localhost/] con el comando [curl –verbose –output localhost.HTML http://localhost/]. Se obtienen los siguientes 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 para mantener intacto el host localhost
- líneas 8-12: líneas enviadas por [curl] al servidor [localhost]. Se reconoce el protocolo HTTP;
- líneas 13-19: líneas enviadas como respuesta por el servidor;
- línea 13: indica que se recibió correctamente el documento solicitado;
El archivo [localhost.HTML] contiene el documento solicitado. Puede verificarlo abriendo el archivo en un editor de texto.
Ahora solicitemos el URL [https://tahe.developpez.com:443/]. Para obtener este URL, el cliente HTTP debe saber comunicarse en HTTPS. Este es el caso del cliente [curl].
Los resultados de la consola son los siguientes:
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 al host tahe.developpez.com, se mantuvo intacto
- líneas 10-40: los intercambios entre el cliente y el servidor para asegurar la conexión; esta será cifrada;
- líneas 42-45: los encabezados HTTP enviados por el cliente [curl] al servidor;
- línea 48: se ha encontrado el documento solicitado;
- línea 53: el documento se envía por partes;
[curl] maneja correctamente tanto el protocolo seguro HTTPS como el hecho de que el documento se envíe por partes. El documento enviado se encontrará aquí en el archivo [tahe.developpez.com.HTML].
Ahora solicitemos el URL [http://sergetahe.com/cours-tutoriels-de-programmation]. Habíamos visto que para este URL, había una redirección hacia el URL [http://sergetahe.com/cours-tutoriels-de-programmation/] (con una barra al final).
Los resultados en la consola son los siguientes:
c:\Temp\curl
λ curl --verbose --output sergetahe.com.HTML --location http://sergetahe.com/cursos-tutoriales-de-programación
% 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/cursos-y-tutoriales-de-programación/
< 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 para que el servidor sergetahe.com permanezca intacto
* Issue another request to this URL: 'http://sergetahe.com/cursos-tutoriales-de-programación/'
* Found bundle for host sergetahe.com: 0x1c88548 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) con el 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 al host sergetahe.com, que se mantuvo intacto
- línea 2: se utiliza la opción [--location] para indicar que se desea seguir las redirecciones enviadas por el servidor;
- línea 13: el servidor indica que el documento solicitado ha cambiado a URL;
- línea 18: indica la nueva URL del documento solicitado;
- línea 27: [curl] envía una nueva solicitud, esta vez a la nueva URL;
- línea 33: se utiliza el nuevo URL;
- línea 38: el servidor responde que ha encontrado el documento solicitado;
- línea 41: lo envía por partes;
El documento solicitado se encontrará en el archivo [sergetahe.com.HTML].
16.4.6. Ejemplo 6
PHP cuenta con una extensión llamada [libcurl] que permite utilizar las capacidades de la herramienta [curl] en un programa PHP. En primer lugar, hay que asegurarse de que esta extensión esté activada en el archivo [php.ini] descrito en el párrafo del enlace:

Asegúrese de que la línea 889 anterior no esté comentada.
Vamos a escribir un script [http-02.php] que utilizará el siguiente archivo de configuración jSON:
Cada elemento del diccionario [clé, valeur] tiene la siguiente estructura:
- clé: el nombre de un servidor web;
- valeur es un diccionario con las siguientes claves:
- timeout: tiempo máximo de espera para la respuesta del servidor. Pasado este tiempo, el cliente se desconectará;
- url: URL del documento solicitado;
El código del script [http-02.php] es el siguiente:
<?php
// cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
//
// manejo de errores
//error_reporting(E_ALL & E_STRICT);
//ini_set("display_errors", "on");
//
// constantes
const CONFIG_FILE_NAME = "config-http-02.json";
//
// se recupera la configuración
$config = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// obtener el texto HTML de URL del archivo de configuración
foreach ($config as $site => $infos) {
// lectura de URL del sitio $ite
$résultat = getUrl($site, $infos["url"], $infos["timeout"]);
// visualización de resultados
print "$résultat\n";
}//para
// fin
exit;
//-----------------------------------------------------------------------
function getUrl(string $site, string $url, int $timeout, $suivi = TRUE): string {
// lee el archivo URL $url y lo guarda en el archivo output/$site.HTML
//
// seguimiento
print "Client : début de la communication avec le serveur [$site] ----------------------------\n";
// Inicialización de una sesión cURL
$curl = curl_init($url);
if ($curl === FALSE) {
// se produjo un error
return "Erreur lors de l'initialisation de la session cURL pour le site [$site]";
}
// opciones de curl
$options = [
// modo detallado
CURLOPT_VERBOSE => true,
// nueva conexión: sin caché
CURLOPT_FRESH_CONNECT => true,
// tiempo de espera de la solicitud (en segundos)
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => $timeout,
// no verificar la validez de los certificados SSL
CURLOPT_SSL_VERIFYPEER => false,
// seguir las redirecciones
CURLOPT_FOLLOWLOCATION => true,
// recuperar el documento solicitado como una cadena de caracteres
CURLOPT_RETURNTRANSFER => true
];
// Configuración de curl
curl_setopt_array($curl, $options);
// Ejecución de la solicitud
$page_content = curl_exec($curl);
// Cierre de la sesión cURL
curl_close($curl);
// Análisis del resultado
if ($page_content !== FALSE) {
// Almacenamiento del resultado en $site.HTML
$result = file_put_contents("output/$site.HTML", $page_content);
if ($result === FALSE) {
// respuesta de error
return "Erreur lors de la création du fichier [output/$site.HTML]";
}
// respuesta exitosa
return "Fin de la communication avec le serveur [$site]. Vérifiez le fichier [output/$site.HTML]";
} else {
// Se produjo un error de comunicación
return "Erreur de communication avec le serveur [$site]";
}
}
Comentarios
- línea 14: se utiliza el archivo de configuración para crear el diccionario [$config];
- líneas 17-22: se recorre la lista de sitios encontrados en la configuración;
- línea 19: para cada uno de los sitios, se llama a la función [getUrl], que descargará elURL $infos[«url»] con un tiempo de espera $infos[«timeout»];
- línea 34: se inicia una sesión [curl]. [curl_init] aún no se conecta al servidor web. Devuelve un recurso [$curl] que servirá como parámetro para todas las funciones [curl] siguientes;
- líneas 35-38: si falla la inicialización de la sesión [curl], la función [curl_init] devuelve el valor booleano FALSE;
- líneas 40-54: el diccionario [$options] configurará la conexión [curl] con el servidor;
- línea 57: las opciones de la conexión se transmiten al recurso [$curl];
- línea 59: se solicita la conexión a URL con las opciones definidas. Debido a la opción [CURLOPT_RETURNTRANSFER => true], la función [curl_exec] devuelve como resultado el documento enviado por el servidor como una cadena de caracteres. La función [curl_exec] devuelve el valor booleano FALSE en caso de que falle la conexión;
- línea 64: se analiza el resultado de [curl_exec];
- línea 66: se guarda la página recibida en un archivo local;
- líneas 69, 72, 75: se devuelve el resultado de la función [getUrl];
Al ejecutar el script [http-02.php], se obtienen los siguientes resultados en la 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/cursos-tutoriales-de-programación
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 para que el servidor sergetahe.com permanezca intacto
* Issue another request to this URL: 'http://sergetahe.com/cursos-tutoriales-de-programación'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) con el 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/cursos-tutoriales-de-programación/
< Set-Cookie: SERVERID68971=2621236|XN/Gc|XN/Gc; path=/
< Cache-control: private
< X-IPLB-Instance: 17097
<
* Ignoring the response-body
* Connection #0 al host sergetahe.com se mantuvo intacto
* Issue another request to this URL: 'http://sergetahe.com/cursos-tutoriales-de-programación/'
* Found bundle for host sergetahe.com: 0x1fee4ebe090 [can pipeline]
* Re-using existing connection! (#0) con el 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/cursos-tutoriales-de-programación/wp-json/>; rel="https://api.w.org/"
< Link: <http://sergetahe.com/cursos-tutoriales-de-programación/>; 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 para mantener intacto sergetahe.com
* 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 al host tahe.developpez.com se mantuvo intacto
* 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/es/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 para que el servidor www.polytech-angers.fr permanezca intacto
* Issue another request to this URL: 'http://www.polytech-angers.fr/es/index.HTML'
* Found bundle for host www.polytech-angers.fr: 0x1fee4ebe390 [can pipeline]
* Re-using existing connection! (#0) con el 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 al host www.polytech-angers.fr, que se mantuvo intacto
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 al host localhost, se deja intacto
Fin de la communication avec le serveur [localhost]. Vérifiez le fichier [output/localhost.HTML]
Comentarios
- Se obtienen los mismos intercambios que con la herramienta [curl];
- en verde, los registros del script;
- en azul, los comandos enviados al servidor;
- en amarillo, los comandos recibidos como respuesta por el cliente;
16.4.7. Conclusión
En este párrafo, hemos descubierto el protocolo HTTP y hemos escrito un script [http-02.php] capaz de descargar un archivo URL de la web.
16.5. El protocolo SMTP (Protocolo simple de transferencia de correo)
16.5.1. Introducción

En este capítulo:
- [Serveur B] será un servidor SMTP local que instalaremos;
- [Client A] será un cliente SMTP de diversas formas:
- el cliente [RawTcpClient] para descubrir el protocolo SMTP;
- un script PHP que reproduce el protocolo SMTP del cliente [RawTcpClient];
- un script PHP que utiliza la biblioteca [SwiftMailServer] para enviar todo tipo de correos electrónicos;
16.5.2. Creación de una dirección [gmail]
Para realizar nuestras pruebas SMTP, necesitaremos una dirección de correo electrónico a la que escribir. Para ello, crearemos una dirección en Gmail:

- en [5], creamos el usuario [php7parlexemple] (elige otro nombre);
- en [6], la contraseña será [PHP7parlexemple] (elige otra);
- en [7], validamos esta información;

- llene los campos [9-10] y luego confirme (11);
- Acepta los términos de uso de Google (12-13) y luego confirma (14);

- en [15], la bandeja de entrada (Inbox) del usuario [PHP7] (16);
- en [17], este usuario tiene una bandeja de entrada vacía;
- en [18-19], inicie sesión en la cuenta de Google del usuario [php7parlexemple@gmail.com]. Vamos a configurar la seguridad de la cuenta;

- en [21], autoriza a otras aplicaciones, además de las de Google, a utilizar la cuenta [php7parlexemple]. Si no hacemos esto, nuestro servidor de correo local [hMailServer] no podrá comunicarse con el servidor SMTP de Gmail;

16.5.3. Instalación de un servidor SMTP
Para nuestras pruebas, instalaremos el servidor de correo [hMailServer], que es a la vez un servidor SMTP que permite enviar correos electrónicos, un servidor POP3 (Protocolo de Oficina Postal, POP) que permite leer los correos almacenados en el servidor, y un servidor IMAP (Protocolo de Acceso a Mensajes de Internet, IMAP), que también permite leer los correos almacenados en el servidor, pero va más allá. En particular, permite administrar el almacenamiento de los correos en el servidor.
El servidor de correo [hMailServer] está disponible en URL [https://www.hmailserver.com/] (mayo de 2019).

Durante la instalación, se le solicitará cierta información:

- en [1-2], seleccione tanto el servidor de correo como las herramientas para administrarlo;
- durante la instalación se le pedirá la contraseña de administrador: anótela, ya que la necesitará;
[hMailServer] se instala como un servicio de Windows que se inicia automáticamente al encender la computadora. Es preferible elegir un inicio manual:
- en [3], escribe [services] en el campo de entrada de la barra de estado;

- en [4-8], se configura el servicio en modo [manuel] (6) y se inicia (7);
Una vez iniciado, se debe configurar el servidor [hMailServer]. El servidor se instaló con un programa de administración [hMailServer Administrator]:

- en [2], en el campo de entrada de la barra de estado, escribe [hmailserver];
- en [3], inicie el administrador;
- en [4], conecte al administrador al servidor [hMailServer];
- en [5], escribe la contraseña que ingresaste al instalar [hMailServer];

Vamos a crear una cuenta de usuario:
- haga clic con el botón derecho en [Accounts] (7) y luego en (8) para agregar un nuevo usuario;
- en la pestaña [General] (9), definimos un usuario [guest] (10) con la contraseña [guest] (11). Tendrá la dirección de correo electrónico [guest@localhost] (10);
- en [12], el usuario [guest] está activado;


- en [15], se configura el protocolo SMTP del servidor de correo;
- en [16], se configura la distribución de los correos;
- en [17], la configuración de la distribución de los correos con destino a la máquina host (localhost);
- en [18], el nombre de la máquina local (localhost). El script del párrafo «enlace» te permite obtener este nombre;
- en [19], se configura un servidor de retransmisión SMTP: se trata del servidor que se encargará de la distribución de los correos que no estén dirigidos a la máquina local (localhost);
- en [20], el servidor SMTP de Gmail. Elegimos Gmail porque hemos creado una cuenta allí en el párrafo del enlace;
- en [21], el puerto SMTP de Gmail;
- En [22], el servicio SMTP de Gmail es un servicio seguro: se necesita una cuenta de Gmail para acceder a él;
- en [23], el usuario [php7parlexemple] creado en el párrafo del enlace;
- en [24], la contraseña de este usuario: [PHP7parlexemple], creado en el párrafo del enlace;
- en [25], se indica el tipo de protocolo de seguridad que utiliza Gmail;

- en [27], el puerto del servicio SMTP;
- en [28], este servicio no requiere autenticación;
- en [30], ingresa el mensaje de bienvenida que el servidor SMTP enviará a sus clientes;
16.5.4. El protocolo SMTP

Vamos a conocer el protocolo SMTP con el siguiente entorno:
- el cliente A será el cliente genérico TCP, [RawTcpClient];
- el servidor B será el servidor de correo [hMailServer];
- el cliente A le pedirá al servidor B que entregue un correo al usuario [php7parlexemple@gmail.com];
- verificaremos que este usuario haya recibido correctamente el correo enviado;
Iniciamos el cliente de la siguiente manera:
![]()
- en [1], nos conectamos al puerto 25 de la máquina local, donde opera el servicio SMTP de [hMailServer]. El argumento [--quit bye] indica que el usuario saldrá del programa al escribir el comando [bye]. Sin este argumento, el comando para salir del programa es [quit]. Sin embargo, [quit] también es un comando del protocolo SMTP. Por lo tanto, debemos evitar esta ambigüedad;
- en [2], el cliente está correctamente conectado;
- en [3], el cliente espera comandos ingresados desde el teclado;
- en [4], el servidor le envía su mensaje de bienvenida;

- en [5], el cliente envía el comando [EHLO nom-de-la-machine-client]. El servidor le responde con una serie de mensajes del tipo [250-xx] (6). El código [250] indica que el comando enviado por el cliente se ejecutó correctamente;
- en [7], el cliente indica el remitente del mensaje, en este caso [guest@localhost]. Este usuario debe existir en el servidor de correo [hMailServer]. Este es el caso aquí, ya que hemos creado este usuario previamente;
- en [8], la respuesta del servidor;
- en [9], se indica el destinatario del mensaje, en este caso el usuario de Gmail [php7parlexemple@gmail.com];
- en [10], la respuesta del servidor;
- en [11], el comando [DATA] le indica al servidor que el cliente va a enviar el contenido del mensaje;
- en [12], la respuesta del servidor;
- En [13-16], el cliente debe enviar una lista de líneas de texto que termine con una línea que contenga únicamente un punto. El mensaje puede contener líneas [Subject :, From :, To :] (13) para definir, respectivamente, el asunto del mensaje, el remitente y el destinatario;
- en [14], las cabeceras anteriores deben ir seguidas de una línea en blanco;
- en [15], el texto del mensaje;
- en [16], la línea que contiene únicamente un punto, lo cual indica el final del mensaje;
- en [17], una vez que el servidor ha recibido la línea que contiene únicamente un punto, coloca el mensaje en la fila de espera;
- en [18], el cliente le indica al servidor que ha terminado;
- en [19], la respuesta del servidor;
- en [20], se observa que el servidor ha cerrado la conexión que lo unía al cliente;
Ahora verifiquemos que el usuario [php7parlexemple@gmail.com] haya recibido correctamente el mensaje:

- en [2], vemos que el usuario [php7parlexemple@gmail.com] recibió correctamente el mensaje;



- en [7], el remitente del correo. Se observa que no es [guest@localhost]. Esto se debe a que fue el servidor de retransmisión definido en la configuración de [hmailServer] el que entregó el mensaje. Ahora bien, este servidor de retransmisión es [smtp.gmail.com], asociado a las credenciales del usuario de Gmail [php7parlexemple@gmail.com]. Cualquier correo procedente de [hMailServer] parecerá provenir del usuario [php7parlexemple@gmail.com]. Esto no es lo que queríamos en este caso, pero si no utilizamos este servidor de retransmisión, el servicio SMTP de Gmail rechaza los correos enviados por [hMailServer] porque el SMTP de Gmail exige una autenticación que [hMailServer] no envía. Seguramente hay alguna forma de solucionar este problema, pero no la he encontrado;
- en [8], se ve que el correo fue recibido desde la máquina [DESKTOP-528I5CU], que aloja el servidor de correo [hMailServer];
- en [9], el remitente del mensaje. Se ve que no es [guest@localhost];
- en [10], el remitente original del mensaje. Esta vez sí es [guest@localhost];
- en [11], el asunto;
- en [12], el destinatario;
- en [13], el mensaje;
Finalmente, nuestro cliente [RawTcpClient] logró enviar el mensaje, aunque se presentó un problema con el remitente. Tenemos la base para crear un cliente SMTP escrito en PHP.
16.5.5. Un cliente SMTP básico escrito en PHP
Vamos a aplicar en PHP lo que aprendimos anteriormente del protocolo SMTP.

El script [smtp-01.php] se configura mediante el siguiente archivo 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] es una tabla en la que cada uno de los elementos es un diccionario de tipo [nom=>infos]. El valor [infos] es, a su vez, un diccionario con las siguientes claves y valores:
- [smtp-server]: el nombre del servidor SMTP que se debe utilizar;
- [smtp-port]: el número de puerto del servicio SMTP;
- [from]: el remitente del mensaje;
- [to]: el destinatario del mensaje;
- [subject]: el asunto del mensaje;
- [message]: el mensaje que se va a enviar;
- El primer elemento utiliza el servidor SMTP [localhost] para enviar un correo electrónico a un usuario de [localhost];
- el segundo elemento utiliza los servidores SMTP y [localhost] para enviar un correo electrónico a un usuario de [Gmail];
- el tercer elemento utiliza el servidor SMTP [Gmail] para enviar un correo electrónico a un usuario de [Gmail];
El código [smtp-01.php] del cliente SMTP es el siguiente:
<?php
// cliente SMTP (protocolo de transferencia SendMail) que permite enviar un mensaje
// protocolo de comunicación cliente-servidor SMTP
// -> el cliente se conecta al puerto 25 del servidor SMTP
// <- el servidor le envía un mensaje de bienvenida
// -> el cliente envía el comando EHLO con el nombre de su máquina
// <- el servidor responde OK o no
// -> el cliente envía el comando MAIL FROM: <remitente>
// <- el servidor responde OK o no
// -> el cliente envía el comando RCPT TO: <destinatario>
// <- el servidor responde OK o no
// -> el cliente envía el comando DATA
// <- el servidor responde con OK o no
// -> el cliente envía todas las líneas de su mensaje y termina con una línea que contiene el
// único carácter.
// <- el servidor responde OK o no
// -> el cliente envía el comando QUIT
// <- el servidor responde OK o no
// las respuestas del servidor tienen el formato xxx texto, donde xxx es un número de 3 dígitos. Cualquier
// número xxx >=500 indica un error.
// La respuesta puede contener varias líneas, todas comenzando con xxx, excepto la última
// con el formato xxx (espacio)
// las líneas de texto intercambiadas deben terminar con los caracteres RC(#13) y LF(#10)
//
// cliente SMTP (Protocolo de transferencia SendMail) que permite enviar un mensaje
//
// gestión de errores
//ini_set («error_reporting», E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
//
// los parámetros para el envío de correo
const CONFIG_FILE_NAME = "config-smtp-01.json";
// se recupera la configuración
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// envío de correos
foreach ($mails as $name => $infos) {
// seguimiento
print "Envoi du mail [$name]\n";
// envío del correo
$résultat = sendmail($name, $infos, TRUE);
// visualización del resultado
print "$résultat\n";
}//para
// fin
exit;
//sendmail
//-----------------------------------------------------------------------
function sendmail(string $name, array $infos, bool $verbose = TRUE): string {
// envía el mensaje [$name,$infos]. Si $verbose=TRUE , realiza un seguimiento de los intercambios entre el cliente y el servidor
// se recupera el nombre del cliente
$client = gethostbyaddr(gethostbyname(""));
// se abre una conexión con el servidor SMTP
$connexion = fsockopen($infos["smtp-server"], (int) $infos["smtp-port"]);
// regresa en caso de error
if ($connexion === FALSE) {
return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
}
// $connexion representa un flujo de comunicación bidireccional
// entre el cliente (este programa) y el servidor SMTP contactado
// este canal se utiliza para el intercambio de comandos e información
// tras la conexión, el servidor envía un mensaje de bienvenida que se lee
$erreur = sendCommand($connexion, "", $verbose, TRUE);
if ($erreur !== "") {
// cierre de la conexión
fclose($connexion);
// regreso
return $erreur;
}
// comando EHLO
$erreur = sendCommand($connexion, "EHLO $client", $verbose, TRUE);
if ($erreur !== "") {
// cierre de la conexión
fclose($connexion);
// regreso
return $erreur;
}
// comando MAIL FROM:
$erreur = sendCommand($connexion, sprintf("MAIL FROM: <%s>", $infos["from"]), $verbose, TRUE);
if ($erreur !== "") {
// cierre de la conexión
fclose($connexion);
// regreso
return $erreur;
}
// comando RCPT TO:
$erreur = sendCommand($connexion, sprintf("RCPT TO: <%s>", $infos["to"]), $verbose, TRUE);
if ($erreur !== "") {
// cierre de la conexión
fclose($connexion);
// regreso
return $erreur;
}
// comando DATA
$erreur = sendCommand($connexion, "DATA", $verbose, TRUE);
if ($erreur !== "") {
// cierre de la conexión
fclose($connexion);
// regreso
return $erreur;
}
// preparación del mensaje para enviar
// debe contener las líneas
// De: remitente
// Para: destinatario
// Asunto:
// línea en blanco
// Mensaje
// .
$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 !== "") {
// Cierre de la conexión
fclose($connexion);
// regreso
return $erreur;
}
// comando de salida
$erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
if ($erreur !== "") {
// cierre de la conexión
fclose($connexion);
// regreso
return $erreur;
}
// fin
fclose($connexion);
return "Message envoyé";
}
// --------------------------------------------------------------------------
function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF): string {
// envía $commande al canal $connexion
// modo detallado si $verbose=1
// si $withRCLF=1, agrega la secuencia RCLF al intercambio
// datos
if ($withRCLF) {
$RCLF = "\r\n";
} else {
$RCLF = "";
}
// envío de comando si $commande no está vacío
if ($commande!=="") {
fputs($connexion, "$commande$RCLF");
// posible respuesta
if ($verbose) {
affiche($commande, 1);
}
}//si
// lectura de la respuesta
$réponse = fgets($connexion, 1000);
// eco eventual
if ($verbose) {
affiche($réponse, 2);
}
// recuperación del código de error
$codeErreur = (int) substr($réponse, 0, 3);
// ¿Última línea de la respuesta?
while (substr($réponse, 3, 1) === "-") {
// lectura de la respuesta
$réponse = fgets($connexion, 1000);
// posible eco
if ($verbose) {
affiche($réponse, 2);
}
}//mientras
// respuesta completada
// ¿Error devuelto por el servidor?
if ($codeErreur >= 500) {
return substr($réponse, 4);
}
// respuesta sin error
return "";
}
// --------------------------------------------------------------------------
function affiche($échange, $sens) {
// muestra $échange en la pantalla
// si $sens=1 muestra -->$echange
// si $sens=2 muestra <-- $échange sin los dos últimos caracteres RCLF
switch ($sens) {
case 1:
print "--> [$échange]\n";
break;
case 2:
$L = strlen($échange);
print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
break;
}//conmutador
}
Comentarios
- línea 39: se utiliza el archivo de configuración;
- línea 42: se recorre el array [mails]. Cada elemento es un diccionario [name=>infos], donde [name] es un nombre que puede ser cualquiera y [infos] es un diccionario que contiene la información necesaria para enviar un correo electrónico;
- línea 46: el envío del correo electrónico se realiza mediante la función [sendmail], que admite tres parámetros:
- $name: el nombre asignado a este envío;
- $infos: el diccionario que contiene la información necesaria para el envío;
- verbose: un valor booleano que indica si las comunicaciones entre el cliente y el servidor deben registrarse o no en la consola;
- línea 46: la función [sendmail] devuelve un mensaje de error que está vacío si no se ha producido ningún error;
- línea 56: la función [sendmail] envía los distintos comandos que debe enviar un cliente SMTP:
- líneas 77-84: el comando EHLO;
- líneas 85-92: el comando MAIL FROM: ;
- líneas 93-100: el pedido RCPT TO: ;
- líneas 101-108: el comando DATA;
- líneas 117-124: envío del mensaje (De, Para, Asunto, texto);
- líneas 125-132: el comando QUIT;
- línea 140: la función [sendCommand] se encarga de enviar los comandos del cliente al servidor SMTP. Admite cuatro parámetros:
- [$connexion]: la conexión que une al cliente con el servidor;
- [$commande]: el comando que se debe enviar;
- [$verbose]: si TRUE, entonces las comunicaciones entre el cliente y el servidor se registran en la consola;
- [$withRCLF]: si TRUE, envía el comando terminado con la secuencia \r\n. Esto es necesario para todos los comandos del protocolo SMTP, pero [sendCommand] también se utiliza para enviar el mensaje. En este caso, no se agrega la secuencia \r\n;
- líneas 150-157: se envía el comando al servidor;
- líneas 158-163: lectura de la primera línea de la respuesta. Esta puede constar de varias líneas. Cada línea tiene el formato XXX-YYY, donde XXX es un código numérico, excepto la última línea de la respuesta, que tiene el formato XXX YYY (sin el carácter -);
- líneas 167-174: lectura de todas las líneas de la respuesta;
- línea 177: si el código numérico XXX es mayor que 500, entonces el servidor devolvió un error;
Resultados
La ejecución del script arroja los siguientes resultados en la 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.
- líneas 1-26: el uso de los servidores SMTP y [hMailServer] para enviar un correo electrónico a [guest@localhost] se realiza correctamente;
- líneas 27-52: el uso de los servidores SMTP y [hMailServer] para enviar un correo electrónico a [php7parlexemple@gmail.com] se realiza correctamente;
- líneas 53-65: el uso del servidor SMTP [Gmail] para enviar un correo electrónico a [php7parlexemple@gmail.com] no funciona correctamente: en la línea 65, el servidor SMTP envía un código de error 530 con el mensaje de error. Este indica que el cliente SMTP debe autenticarse previamente a través de una conexión segura. Nuestro cliente no lo ha hecho y, por lo tanto, su solicitud es rechazada;
16.5.6. Un segundo cliente, SMTP, escribe utilizando la biblioteca [SwiftMailer]
El cliente anterior presenta al menos dos deficiencias:
- no sabe utilizar una conexión segura si el servidor la requiere;
- no sabe adjuntar archivos al mensaje;
En nuestro nuevo script utilizaremos la biblioteca [SwiftMailer] [https://swiftmailer.symfony.com/] (mayo de 2019). El procedimiento de instalación de [SwiftMailer] se describe en URL [https://swiftmailer.symfony.com/docs/introduction.HTML] (mayo de 2019).
En primer lugar, ejecute Laragon:

- en [1], abre un terminal;

- en [3], verifica que te encuentres en la carpeta [<laragon>/www], donde <laragon> es la carpeta de instalación de Laragon;
- en [3], escribe el comando indicado (mayo de 2019). Verifica en URL y [https://swiftmailer.symfony.com/docs/introduction.HTML] el comando exacto;
- en [4], se indica que no se ha realizado ninguna instalación ni actualización. Esto se debe a que la biblioteca ya estaba instalada en esta computadora;
- en [5], la carpeta de instalación de [swiftmailer] [6];
- en [7], un archivo que necesitaremos en nuestro script;
Una vez hecho esto, verifica que la carpeta [<laragon>/www/vendor] [5] se encuentre efectivamente en la rama [Include Path] de NetBeans (véase el párrafo del enlace).
Por último, la biblioteca [SwiftMailer] requiere que la extensión PHP [mbstring] esté activa. Para ello, revisa el archivo [php.ini] (véase el párrafo del enlace):

El script [smtp-02.php] utilizará el siguiente archivo de configuración jSON [config-smtp-02.json]:
Encontramos los mismos campos que en el archivo [config-smtp-01.json], con dos campos adicionales:
- [tls]: en TRUE indica que se debe utilizar una conexión segura con el servidor SMTP. En caso de que [tls] sea igual a TRUE, hay que agregar dos campos:
- [user]: el nombre de usuario que autentica la conexión;
- [password]: su contraseña;
En nuestro ejemplo, utilizamos las credenciales del usuario [php7parlexemple@gmail.com] para conectarnos al servidor de Gmail. Usa las tuyas;
- [attachments]: indica los nombres de los archivos que se adjuntarán al correo;
El código del script [smtp-02.php] es el siguiente:
<?php
// cliente SMTP (Protocolo de transferencia SendMail) que permite enviar un mensaje
//
// gestión de errores
//ini_set («error_reporting», E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencias
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
//
// los parámetros para el envío de correo
const CONFIG_FILE_NAME = "config-smtp-02.json";
// se recupera la configuración
$mails = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// envío de correos
foreach ($mails as $name => $infos) {
// seguimiento
print "Envoi du mail [$name]\n";
// envío del correo
$résultat = sendmail($name, $infos);
// visualización del resultado
print "$résultat\n";
}//para
// fin
exit;
//-----------------------------------------------------------------------
function sendmail($name, $infos) {
// envía $infos[message] al servidor SMTP $infos[smtp-server] en el puerto $infos[smt-port]
// si $infos[tls] es verdadero, se utilizará el soporte TLS
// el correo se envía en nombre de $infos[from]
// para el destinatario $infos['to']
// El documento $info[attachment] se adjunta al mensaje
// el mensaje tiene como asunto $infos[subject]
//
// mensaje en formato HTML
$messageHTML = str_replace("\n", "<br/>", $infos["message"]);
try {
// creación del mensaje
$message = (new \Swift_Message())
// asunto del mensaje
->setSubject($infos["subject"])
// remitente
->setFrom($infos["from"])
// destinatarios con un diccionario (setTo/setCc/setBcc)
->setTo($infos["to"])
// texto del mensaje
->setBody($infos["message"])
// variante HTML
->addPart("<b>$messageHTML</b>", 'text/html')
;
// archivos adjuntos
foreach ($infos["attachments"] as $attachment) {
// ruta del archivo adjunto
$fileName = __DIR__ . $attachment;
// se verifica que el archivo exista
if (file_exists($fileName)) {
// se adjunta el documento al mensaje
$message->attach(\Swift_Attachment::fromPath($fileName));
} else {
// error
print "L'attachement [$fileName] n'existe pas\n";
}
}
// ¿Protocolo 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 hay TLS
$transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"]));
}
// el administrador del envío
$mailer = new \Swift_Mailer($transport);
// envío del mensaje
$result = $mailer->send($message);
// fin
return "Message [$name] envoyé";
} catch (\Throwable $ex) {
// error
return "Erreur lors de l'envoi du message [$name] : " . $ex->getMessage();
}
}
Comentarios
- línea 10: cargamos el archivo [autoload.php] que se encuentra en la carpeta [<lagagon>/www/vendor], donde <laragon> es la carpeta de instalación de Laragon. Este archivo permitirá cargar los archivos de definición de clases de [SwiftMailer] desde la primera vez que se utilicen dichas clases. Nos evita tener que incluir tantos archivos [require] como clases e interfaces de SwiftMailer que vayamos a utilizar;
- línea 32: la nueva función [sendmail], que tiene dos parámetros:
- [$name], que sirve para diferenciar los mensajes entre sí;
- [$infos]: la información necesaria para enviar el mensaje a su destinatario;
- línea 42: tendremos dos versiones del mensaje: una en texto sin formato y otra en HTML. Aquí, cambiamos los caracteres de fin de línea por el código HTML <br/>;
- líneas 45-69: definimos el mensaje utilizando la clase [\SwiftMessage];
- línea 47: el método [SwiftMessage→setSubject] sirve para establecer el asunto del mensaje;
- línea 49: el método [SwiftMessage→setFrom] sirve para establecer el remitente del mensaje;
- línea 51: el método [SwiftMessage→setTo] sirve para establecer el destinatario del mensaje;
- línea 53: el método [SwiftMessage→setBody] sirve para establecer el cuerpo del mensaje;
- línea 55: el método [SwiftMessage→addPart] sirve para establecer diferentes versiones del mensaje; en este caso, el mensaje en formato HTML. Cuando el mensaje tiene variantes, los clientes de correo muestran la variante preferida por el usuario;
- líneas 58-69: el método [SwiftMessage→addAttachment] (64) permite adjuntar un archivo al mensaje;
- líneas 70-79: una vez definido el mensaje que se va a enviar, hay que definir cómo enviarlo. El modo de transporte del mensaje se define mediante la clase [\Swift_SmtpTransport]. Hay al menos dos datos que se deben proporcionar: el nom y el port del servidor SMTP. También hay un tercer dato: ¿el servidor SMTP exige una autenticación segura?
- líneas 73-75: la instancia [\Swift_SmtpTransport] para una conexión segura al servidor SMTP;
- línea 78: la instancia [\Swift_SmtpTransport] para una conexión no segura al servidor SMTP;
- línea 81: la clase [\SwiftMailer] es la que envía los mensajes. Se le debe pasar el modo de transporte elegido;
- línea 83: el mensaje [\SwiftMessage] se envía a través del transporte [\Swift_SmtpTransport] seleccionado. El método [SwiftMailer→send] devuelve el valor booleano FALSE si no se pudo enviar el mensaje;
- líneas 86-89: la biblioteca [SwiftMailer] lanza una excepción tan pronto como algo sale mal;
Nota: cabe señalar que el espacio de nombres de las clases de la biblioteca [SwiftMailer] es la raíz \. Se han indicado explícitamente las clases [\SwiftMessage, \Swift_SmtpTransport, \SwiftMailer] para recordarlo;
Resultados
Al ejecutar el script [smtp-02.php], se obtienen los siguientes resultados en la consola:
Si consultamos la cuenta de Gmail del usuario [php7parlexemple], vemos lo siguiente:

- en [1], el asunto;
- en [2], el remitente;
- en [3], el destinatario;
- en [4], el mensaje;
- en [5-10], los archivos adjuntos;
Si se solicita ver el mensaje original, se obtiene el siguiente 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_=_--
- línea 9: el asunto;
- línea 10: el remitente;
- línea 11: el destinatario;
- línea 13: el mensaje contiene varias partes delimitadas por las etiquetas [--_=_swift_xx];
- líneas 19-24: el mensaje en texto sin formato;
- líneas 27-30: el mensaje en formato HTML;
- líneas 34-36: el archivo adjunto [Hello from SwiftMailer.docx];
- líneas 40-42: el archivo adjunto [Hello from SwiftMailer.pdf];
- líneas 46-48: el archivo adjunto [Hello from SwiftMailer.odt];
- líneas 58-60: el archivo adjunto [Cours-Tutoriels-Serge-Tahé-1568x268.png];
- líneas 58-60: el archivo adjunto [test-localhost.eml];
- líneas 62-114: el archivo adjunto [test-localhost.eml] es en sí mismo un mensaje cuyo contenido se muestra en las líneas 62-114. Se puede observar que este mensaje contiene a su vez archivos adjuntos;
16.6. Los protocolos POP3 (Protocolo de Oficina Postal) y IMAP (Protocolo de Acceso a Mensajes de Internet)
16.6.1. Introducción
Para leer los correos almacenados en un servidor de correo, existen dos protocolos:
- el protocolo POP3 (Post Office Protocol), históricamente el primer protocolo, pero poco utilizado en la actualidad;
- el protocolo IMAP (Protocolo de Acceso a Mensajes de Internet), un protocolo más reciente que el POP3 y el más utilizado en la actualidad;
Para conocer el protocolo POP3, utilizaremos la siguiente arquitectura:

- [Serveur B] será un servidor local POP3 / IMAP, implementado por el servidor de correo [hMailServer];
- [Client A] será un cliente POP3 / IMAP de diversas formas:
- el cliente [RawTcpClient] para descubrir el protocolo POP3;
- un script PHP que reproduce el protocolo POP3 del cliente [RawTcpClient];
- un script PHP que utiliza la biblioteca IMAP de PHP, la cual permite implementar tanto clientes IMAP como POP3;
16.6.2. Introducción al protocolo POP3
En primer lugar, utilizamos el script [smtp-01.php] para enviar un correo electrónico al usuario [guest@localhost]. Si has realizado las pruebas relacionadas con el script, este usuario normalmente debería haber recibido correos electrónicos, pero no hemos podido verificarlo. Para enviarle un nuevo correo electrónico, utiliza, por ejemplo, el siguiente archivo de configuración [config-smtp-01.json]:
Ahora veamos con el cliente [RawTcpClient] cómo se puede leer el buzón del usuario [guest@localhost]:
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…
- línea 1: el servidor POP3 suele funcionar con el puerto 110. Este es el caso aquí;
- línea 5: el comando [USER] sirve para definir el usuario cuyo buzón de correo queremos leer;
- línea 7: el comando [PASS] sirve para definir su contraseña;
- línea 9: el comando [LIST] solicita la lista de mensajes presentes en el buzón del usuario;
- línea 14: el comando [RETR] solicita ver el mensaje cuyo número se indica;
- línea 29: el comando [DELE] solicita la eliminación del mensaje cuyo número se indica;
- línea 40: el comando [QUIT] le indica al servidor que hemos terminado;
La respuesta del servidor puede adoptar varias formas:
- una sola línea que comienza con [+OK] para indicar que el comando anterior del cliente se ejecutó correctamente;
- una sola línea que comienza con [-ERR] para indicar que el comando anterior del cliente falló;
- varias líneas en las que:
- la primera línea comienza con [+OK];
- la última línea consta de un solo punto;
16.6.3. Un script básico que implementa el protocolo POP3

Dado que el protocolo POP3 tiene la misma estructura que el protocolo SMTP, el script [pop3-01.php] es una adaptación del script [smtp-01.php]. Contará con el siguiente archivo de configuración [config-pop3-01.json]:
- líneas 3-4: el servidor POP3 al que se consulta es el servidor local [hMailServer];
- líneas 5-6: se desea leer el buzón del usuario [guest@localhost];
- línea 7: se leerán como máximo 5 correos;
El script [pop3-01.php] es el siguiente:
<?php
// cliente POP3 (Protocolo de Correo) que permite leer mensajes de un buzón
// protocolo de co POP3 cliente-servidor
// -> el cliente se conecta al puerto 110 del servidor SMTP
// <- el servidor le envía un mensaje de bienvenida
// -> el cliente envía el comando USER usuario
// <- el servidor responde OK o no
// -> el cliente envía el comando PASS mot_de_passe
// <- el servidor responde OK o no
// -> el cliente envía el comando LIST
// <- el servidor responde con OK o no
// -> el cliente envía el comando RETR, un número para cada uno de los correos
// <- el servidor responde OK o no. Si responde OK, envía el contenido del correo solicitado
// -> el servidor envía todas las líneas del correo y termina con una línea que contiene el
// único carácter.
// -> el cliente envía el comando DELE n.º para eliminar un correo
// <- el servidor responde OK o no
// // -> el cliente envía el comando QUIT para finalizar la comunicación con el servidor
// <- el servidor responde OK o no
// las respuestas del servidor tienen el formato +OK texto o -ERR texto
// La respuesta puede constar de varias líneas. En ese caso, la última línea está formada por un solo punto
// Las líneas de texto intercambiadas deben terminar con los caracteres RC(#13) y LF(#10)
//
// cliente POP3 (Protocolo de transferencia SendMail) que permite leer correos electrónicos
//
// gestión de errores
//ini_set («error_reporting», E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
//
// los parámetros para el envío de correo
const CONFIG_FILE_NAME = "config-pop3-01.json";
// se recupera la configuración
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// lectura de los buzones
foreach ($mailboxes as $name => $infos) {
// seguimiento
print "Lecture de la boîte à lettres [$name]\n";
// lectura del buzón
$résultat = readmail($name, $infos, TRUE);
// visualización del resultado
print "$résultat\n";
}//para
// fin
exit;
//leer correo
//-----------------------------------------------------------------------
function readmail(string $name, array $infos, bool $verbose = TRUE): string {
// lee el contenido del buzón [$name]
// importa todos los mensajes
// cada mensaje se elimina después de leerlo
// Si $verbose=1, realiza un seguimiento de los intercambios entre el cliente y el servidor
//
// abre una conexión con el servidor SMTP
$connexion = fsockopen($infos["server"], (int) $infos["port"]);
// Retorno en caso de error
if ($connexion === FALSE) {
return sprintf("Echec de la connexion au site (%s,%s) : %s", $infos["smtp-server"], $infos["smtp-port"]);
}
// $connexion representa un flujo de comunicación bidireccional
// entre el cliente (este programa) y el servidor POP3 contactado
// este canal se utiliza para el intercambio de comandos e información
// tras la conexión, el servidor envía un mensaje de bienvenida que se lee
$erreur = sendCommand($connexion, "", $verbose, TRUE);
if ($erreur !== "") {
// cierre de la conexión
fclose($connexion);
// regreso
return $erreur;
}
// comando USER
$erreur = sendCommand($connexion, "USER {$infos["user"]}", $verbose, TRUE);
if ($erreur !== "") {
// cierre de la conexión
fclose($connexion);
// regreso
return $erreur;
}
// comando PASS
$erreur = sendCommand($connexion, "PASS {$infos["password"]}", $verbose, TRUE);
if ($erreur !== "") {
// cierre de la conexión
fclose($connexion);
// regreso
return $erreur;
}
// comando LIST
$premièreLigne = "";
$erreur = sendCommand($connexion, "LIST", $verbose, TRUE, $premièreLigne);
if ($erreur !== "") {
// cierre de la conexión
fclose($connexion);
// regreso
return $erreur;
}
// análisis de la primera línea para conocer el número de mensajes
$champs = [];
preg_match("/^\+OK (\d+)/", $premièreLigne, $champs);
$nbMessages = (int) $champs[1];
// recorrido por los mensajes
$iMessage = 0;
while ($iMessage < $nbMessages && $iMessage < $infos["maxmails"]) {
// comando RETR
$erreur = sendCommand($connexion, "RETR " . ($iMessage + 1), $verbose, TRUE);
if ($erreur !== "") {
// cierre de la conexión
fclose($connexion);
// regreso
return $erreur;
}
// comando DELE
$erreur = sendCommand($connexion, "DELE " . ($iMessage + 1), $verbose, TRUE);
if ($erreur !== "") {
// cierre de la conexión
fclose($connexion);
// regreso
return $erreur;
}
// mensaje siguiente
$iMessage++;
}
// comando QUIT
$erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
if ($erreur !== "") {
// cierre de la conexión
fclose($connexion);
// regreso
return $erreur;
}
// fin
fclose($connexion);
return "Terminé";
}
// --------------------------------------------------------------------------
function sendCommand($connexion, string $commande, bool $verbose, bool $withRCLF, string &$premièreLigne = ""): string {
// envía $commande al canal $connexion
// modo detallado si $verbose=1
// si $withRCLF=1, agrega la secuencia RCLF al intercambio
// coloca la primera línea de la respuesta en [$premièreLigne
// ]
// datos
if ($withRCLF) {
$RCLF = "\r\n";
} else {
$RCLF = "";
}
// envía comando si $commande no está vacío
if ($commande !== "") {
fputs($connexion, "$commande$RCLF");
// posible respuesta
if ($verbose) {
affiche($commande, 1);
}
}//si
// lectura de la respuesta
$réponse = fgets($connexion, 1000);
// se almacena la primera línea
$premièreLigne = $réponse;
// posible eco
if ($verbose) {
affiche($réponse, 2);
}
// recuperación del código de error
$codeErreur = substr($réponse, 0, 1);
if ($codeErreur === "-") {
// se produjo un error
return substr($réponse, 5);
}
// casos especiales de los comandos RETR y LIST, que tienen respuestas de varias líneas
$commande = substr(strtolower($commande), 0, 4);
if ($commande === "list" || $commande === "retr") {
// ¿Última línea de la respuesta?
$champs = [];
$match = preg_match("/^\.\s+$/", $réponse, $champs);
while (!$match) {
// lectura de la respuesta
$réponse = fgets($connexion, 1000);
// posible eco
if ($verbose) {
affiche($réponse, 2);
}
// análisis de la respuesta
$champs = [];
$match = preg_match("/^\.\s+$/", $réponse, $champs);
}//mientras
}
// retorno sin error
return "";
}
// --------------------------------------------------------------------------
function affiche($échange, $sens) {
// muestra $échange en la pantalla
// si $sens=1 muestra -->$echange
// si $sens=2 muestra <-- $échange sin los dos últimos caracteres RCLF
switch ($sens) {
case 1:
print "--> [$échange]\n";
break;
case 2:
$L = strlen($échange);
print "<-- [" . substr($échange, 0, $L - 2) . "]\n";
break;
}//conmutador
}
Comentarios
Como ya mencionamos, [pop3-01.php] es una adaptación del script [smtp-01.php] que ya hemos comentado. Solo comentaremos las diferencias principales:
- línea 55: la función [readmail] se encarga de leer los correos del buzón. La información para conectarse a este buzón se encuentra en el diccionario [$infos];
- líneas 61-66: apertura de una conexión con el servidor POP3;
- líneas 71-77: lectura del mensaje de bienvenida enviado por el servidor;
- líneas 78-85: se envía el comando [USER] para identificar al usuario cuyos correos se desean;
- líneas 86-93: se envía el comando [PASS] para proporcionar la contraseña de ese usuario;
- líneas 94-102: se envía el comando [LIST] para saber cuántos correos hay en el buzón de este usuario.
- línea 96: se agrega el parámetro [$premièreLigne] a los parámetros de la función [readmail]. En la primera línea de su respuesta al comando LIST, el servidor indica cuántos mensajes hay en el buzón;
- líneas 104-106: se recupera el número de mensajes de la primera línea de la respuesta;
- líneas 109-128: se recorre cada uno de los mensajes. Para cada uno de ellos se emiten dos comandos:
- RETR i: para recuperar el mensaje n.º i (líneas 111-117);
- DELE i: para borrarlo una vez que se haya leído (líneas 118-125);
- líneas 129-136: se envía el comando [QUIT] para indicarle al servidor que hemos terminado;
- líneas 178-194: para los comandos [LIST] y [RETR], la respuesta del servidor tiene varias líneas, siendo la última un solo punto;
Resultados
Al ejecutar el código, se obtienen los siguientes 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.
Aquí tenemos un cliente POP3 básico al que le faltan ciertas capacidades:
- la capacidad de comunicarse con un servidor POP3 seguro;
- la capacidad de leer los archivos adjuntos a un mensaje;
Vamos a implementar la primera posibilidad con las funciones [imap] de PHP.
16.6.4. Cliente POP3 / IMAP implementado con las funciones [imap] de PHP
En primer lugar, debemos verificar que las funciones [imap] estén disponibles en la versión de PHP que estamos utilizando. Abrimos el archivo [php.ini] descrito en el párrafo del enlace y buscamos las líneas que mencionan [imap]:

Línea 895, verifique que la extensión [imap] esté activada.
El script [imap-01.php] procesará el siguiente archivo jSON [config-imap-01.json]:
El archivo [config-imap-01.json] define una tabla de servidores IMAP / POP3 con los que se debe contactar. Cada elemento es una estructura [clé:valeur], donde:
- [clé]: es el servidor al que hay que contactar. Aquí tenemos dos:
- [{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX]: se refiere al servidor [imap.gmail.com] que escucha en el puerto 993. El protocolo cliente/servidor es IMAP. El parámetro /ssl indica que la comunicación cliente-servidor es segura. El parámetro /novalidate-cert le pide al cliente que no verifique el certificado de seguridad que el servidor le enviará. Por último, un servidor IMAP administra un conjunto de buzones para un mismo usuario. Al especificar INBOX en el URL del servidor IMAP, indicamos que nos interesa el buzón denominado INBOX, que normalmente es aquel donde llegan los mensajes nuevos;
- [{localhost:110/pop3}INBOX]: se refiere al servidor [localhost] que escucha en el puerto 110. El protocolo cliente/servidor aquí es POP3;
- [valeur]: es un diccionario que especifica los siguientes puntos:
- [imap-server]: el nombre del servidor IMAP o POP3;
- [imap-port]: el puerto del servidor IMAP o POP3;
- [user]: el propietario cuyo buzón se desea leer;
- [password]: su contraseña;
- [output-dir]: la carpeta en la que se deben guardar los mensajes;
- [prefix]: el nombre de los archivos en los que se guardarán los mensajes tendrá el formato prefixN, donde N es un número de mensaje;
- [pop3]: un valor booleano en TRUE para indicar que el protocolo utilizado es POP3. En este caso, después de leer un mensaje, se eliminará. Este es el funcionamiento habitual de los servidores POP3: un mensaje leído no se conserva en el servidor;
El script [imap-01.php] es el siguiente:
<?php
// cliente IMAP (Protocolo de Acceso a Mensajes de Internet) que permite leer correos electrónicos
//
// cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
// gestión de errores
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
//
// los parámetros de lectura del correo
const CONFIG_FILE_NAME = "config-imap-01.json";
// se recupera la configuración
$mailboxes = \json_decode(\file_get_contents(CONFIG_FILE_NAME), true);
// lectura de los buzones
foreach ($mailboxes as $name => $infos) {
// seguimiento
print "------------Lecture de la boîte à lettres [$name]\n";
// lectura del buzón
readmailbox($name, $infos);
}
// fin
exit;
//-----------------------------------------------------------------------
function readmailbox(string $name, array $infos): void {
// Intento de conexión
$imapResource = imap_open($name, $infos["user"], $infos["password"]);
// Prueba del resultado de la función imap_open()
if (!$imapResource) {
// Fallo
print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
} else {
// Conexión establecida
print "Connexion établie avec le serveur [$name].\n";
// Total de mensajes en el buzón
$nbmsg = imap_num_msg($imapResource);
print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
// Mensajes no leídos en el buzón actual
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) {
// Se recupera información sobre el mensaje 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);
}
// se recupera el cuerpo del mensaje n.º $msgNumber
getMailBody($imapResource, $msgNumber, $infos);
// si el protocolo es POP3, se elimina el mensaje
$pop3 = $infos["pop3"];
if ($pop3 !== NULL) {
// se elimina el mensaje en dos pasos
imap_delete($imapResource, $msgNumber);
imap_expunge($imapResource);
}
}
}
}
}
// cierre de la conexión
$imapClose = imap_close($imapResource);
if (!$imapClose) {
// Fallo
print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
} else {
// Éxito
print "Fermeture de la connexion réussie.\n";
}
}
function getMailBody($imapResource, int $msgNumber, array $infos): void {
// se recupera el cuerpo del mensaje n.º $msgNumber
$corpsMail = imap_body($imapResource, $msgNumber);
print "Enregistrement du message dans le fichier {$infos["output-dir"]}/{$infos["prefix"]}$msgNumber\n";
// Se crea la carpeta si es necesario
if (!file_exists($infos["output-dir"])) {
mkdir($infos["output-dir"]);
}
// se guarda el mensaje
if (!file_put_contents($infos["output-dir"] . "/" . $infos["prefix"] . $msgNumber, $corpsMail)) {
print "Echec de l'enregistrement\n";
}
}
Comentarios
- líneas 19-24: se recorre el conjunto de servidores encontrados en el archivo de configuración;
- línea 32: la función [raedmailbox] lee el buzón indicado en [$name];
- línea 32: se abre una conexión IMAP;
- el primer parámetro es el URL IMAP del buzón que se va a leer;
- el segundo parámetro es el nombre de usuario del propietario de este buzón;
- el tercer parámetro es su contraseña;
La función [imap_open] se encarga de asegurar la conexión si el URL IMAP del buzón tiene el parámetro /ssl;
- línea 41: la función [imap_num_msg] permite obtener el número total de mensajes del buzón;
- línea 46: la función [imap_search] permite buscar determinados mensajes. En este caso, buscamos los mensajes que aún no se han leído (UNSEEN). El segundo parámetro es un criterio de selección. Existen más de veinte de ellos. La función [imap_search] devuelve una matriz con los números de los mensajes. Estos pueden presentarse de dos formas: número de secuencia o identificador UID del mensaje. Por defecto, la función [imap_search] devuelve una matriz con los números de secuencia. Si se agrega un tercer parámetro [SE_UID], se obtendrán los identificadores UID de los mensajes;
- línea 47: la función [imap_search] devuelve el valor booleano FALSE si no ha encontrado ningún mensaje;
- línea 50: se recorre un bucle por todos los mensajes no leídos;
- línea 52: un mensaje tiene encabezados que se pueden obtener con la función [imap_headerinfo]. Su segundo parámetro suele ser un número de secuencia del mensaje. Si se desea establecer un identificador de mensaje UID, hay que establecer el tercer parámetro en [FT_UID];
- línea 53: la función [imap_headerinfo] devuelve el valor booleano FALSE si no pudo realizar su tarea. De lo contrario, devuelve un objeto complejo que se muestra con la función [print_r], línea 57;
- línea 60: después de los encabezados, ahora se solicita el cuerpo del mensaje con la función [imap_body]. Esta función devuelve NULL si no pudo realizar su tarea;
- líneas 84-87: se guarda el cuerpo del mensaje en un archivo local;
- líneas 63-68: si el protocolo utilizado fue POP3, se elimina el mensaje que acaba de leerse:
- la función [imap_delete] marca el mensaje como «para eliminar», pero no lo elimina;
- la función [imap_expunge] elimina físicamente todos los mensajes que han sido marcados como «para eliminar»;
- línea 74: se cierra la conexión con el servidor IMAP. Para ello se utiliza la función [imap_close];
- línea 86: la función [imap_body] permite obtener el cuerpo de un mensaje identificado por su número;
Ejecutemos el script [smtp-02.json] para que el usuario [php7parlexemple] de Gmail y el usuario [guest] de [localhost] tengan mensajes nuevos. Una vez hecho esto, ejecutemos el script [imap-01.php] para leer sus buzones.
Los resultados de la consola son los siguientes:
------------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.
Si, inmediatamente después de estos resultados, volvemos a ejecutar el script [imap-01.php], los resultados son los siguientes:
------------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.
- línea 3: sigue habiendo el mismo número de mensajes en el buzón de Gmail, pero ya no hay mensajes nuevos sin leer (línea 5). Esto demuestra que la ejecución anterior cambió el estado de los mensajes leídos de «sin leer» a «leído»;
- línea 9: ya no hay mensajes en la bandeja de entrada del usuario [guest@localhost]. Esto se debe a que, en la ejecución anterior, los mensajes leídos en [localhost] se eliminaron posteriormente;
Los mensajes se han guardado localmente:

Si observamos, por ejemplo, el contenido del mensaje n.º 26 de Gmail, vemos lo siguiente:
--_=_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_=_--
- líneas 11-13: el mensaje en texto sin formato;
- línea 19: el mensaje HTML;
- línea 25: el archivo adjunto;
Intentemos mejorar este script para tener, en archivos separados, los diferentes tipos de mensajes así como los archivos adjuntos.
16.6.5. Cliente POP3 / IMAP mejorado
En el script [imap-01.php], se muestra el cuerpo del mensaje n.º i como un archivo de texto que contiene tanto los distintos tipos de mensajes como el contenido codificado de los distintos archivos adjuntos. Es posible obtener la estructura del mensaje para conocer estas distintas partes. En el script [imap-02.php], modificamos la función [getMailBody] de la siguiente manera:
function getMailBody($imapResource, int $msgNumber, array $infos): void {
// se recupera la estructura del mensaje
$structure=imap_fetchstructure($imapResource, $msgNumber);
// se muestra
print_r($structure);
}
- línea 3: solicitamos la estructura del mensaje;
- línea 5: la mostramos;
El objetivo es conocer la información contenida en la estructura de un mensaje para ver cómo se pueden obtener sus diferentes partes. En nuestro ejemplo, el mensaje es enviado por el script [smtp-02.php] con la siguiente configuración [config-smtp-02.json]:
Por lo tanto, se trata de un mensaje con cinco archivos adjuntos que se envía a [guest@localhost] (líneas 11-15). El script [imap-02.php] se ejecuta con la siguiente configuración: [config-imap-01.json]:
Por lo tanto, se explota el buzón de [guest@localhost] (línea 5). El script [imap-02.php] muestra entonces la estructura del mensaje enviado por [smtp-02.php]. Esta estructura, que se muestra en la consola, es la siguiente:
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
(
…
)
)
)
)
Comentarios
- La documentación PHP de la función [imap_fetchstructure] explica el significado de los distintos campos del objeto devuelto por la función:

Los valores numéricos del campo [type] tienen el siguiente significado:

Los valores numéricos del campo [encoding] tienen el siguiente significado:

El mensaje registrado por [imap-01.php] comenzaba con el siguiente 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_=_
- las líneas 15) y 33) delimitan el mensaje de tipo [multipart/mixed] (línea m);
- las líneas 18) y 16) delimitan la primera parte del mensaje: el mensaje en texto sin formato;
- las líneas 26) y 32) delimitan la segunda parte del mensaje: el mensaje HTML;
Encontramos la información del mensaje anterior en el objeto devuelto 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
)
)
)
)
)
- línea 3: el mensaje es de tipo MIME (Multipurpose Internet Mail Extensions) [multipart];
- línea 4: el mensaje está codificado en 7 bits;
- línea 5: [ifsubtype]=1 indica que hay un campo [subtype] en la estructura;
- línea 6: el campo [subtype] designa un subtipo MIME, en este caso el tipo [mixed]. En total, el tipo MIME del documento es [multipart/mixed];
- línea 7: [ifdescription]=0 indica que no hay ningún campo [description] en la estructura;
- línea 8: [ifid]=0 indica que no hay un campo [id] en la estructura;
- línea 10: [ifdisposition]=0 indica que no existe el campo [disposition] en la estructura;
- línea 11: [ifdparameters]=0 indica que no hay un campo [dparameters] en la estructura;
- línea 12: [ifparameters]=1 indica que hay un campo [parameters] en la estructura;
- línea 13: el campo [parameters] describe los parámetros del mensaje. Aquí solo hay uno;
- líneas 15-19: este objeto describe la siguiente línea del mensaje de texto:
Estas líneas sirven para delimitar el mensaje. En el mensaje recuperado por [imap-01.php], la parte del mensaje que acabamos de describir corresponde a la línea m). El atributo [boundary] no es el mismo porque las capturas de pantalla corresponden al mismo mensaje, pero enviado en momentos diferentes;
- línea 23: aquí comienza la estructura de las diferentes partes del mensaje;
- líneas 25-45: esta primera parte es del tipo [multipart/alternative]. Corresponde a la línea p) del texto del mensaje;
- línea 47: esta primera parte tiene a su vez subpartes;
- líneas 47-70: esta primera subparte es del tipo [text/plain] (líneas 51, 54), está codificada en el tipo [ENCQUOTEDPRINTABLE] (línea 52) y tiene un parámetro [charset=utf-8] (líneas 66-67);
- las líneas 49-72 describen las líneas s-x del mensaje de texto;
- líneas 74-99: describen la segunda subparte de la parte [multipart/alternative];
- líneas 74-99: esta segunda subparte es de tipo [text/HTML] (líneas 76, 79), está codificada en el tipo [ENCQUOTEDPRINTABLE] (línea 77) y tiene un parámetro [charset=utf-8] (líneas 89-93);
- las líneas 74-99 describen las líneas aa-ad del mensaje de texto;
La parte [multipart/alternative] ha finalizado. Comienza la parte [application/vnd.openxmlformats-officedocument.wordprocessingml.document] descrita por el siguiente texto:
Una vez más, esta información se encuentra en el objeto devuelto por la función [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
)
)
)
- línea 1: es la segunda parte del mensaje global. Recordemos que la primera parte era del tipo [multipart/alternative];
- líneas 3-6: esta segunda parte es del tipo [application/vnd.openxmlformats-officedocument.wordprocessingml.document] (líneas 3 y 6) y está codificada en Base 64 (línea 4);
- línea 11: esta segunda parte es un archivo adjunto (línea 11) y tiene dos parámetros: [filename=Hello from SwiftMailer.docx] (líneas 15-21) y [name=Hello from SwiftMailer.docx] (líneas 26-32). Cabe señalar que este último parámetro no existe en el mensaje de texto. Por lo tanto, se ha agregado en la función [imap_fetchstructure];
Las líneas 1 a 36 se repiten para cada uno de los cinco archivos adjuntos del mensaje.
La función [imap_fetch_structure] nos permite, por lo tanto, obtener la estructura de un mensaje. Esta estructura define partes que, a su vez, pueden tener subpartes. Para obtener el texto de una parte o subparte, se utiliza la función [imap_fetchbody].
Modificamos la función [getMailBody], que nos permite obtener el cuerpo de un mensaje, de la siguiente manera:
function getMailBody($imapResource, int $msgNumber, array $infos, object $infosMail): void {
// se recupera la estructura del mensaje
$structure = imap_fetchstructure($imapResource, $msgNumber);
if ($structure !== FALSE) {
// se recuperan estas diferentes partes
getParts($imapResource, $msgNumber, $infos, $infosMail, $structure);
}
}
function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
// cálculo del número de sección
if (substr($sectionNumber, 0, 2) === "0.") {
$sectionNumber = substr($sectionNumber, 2);
}
print "-----contenu de la partie n° [$sectionNumber]\n";
// tipo de contenido
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;
}
// tipo de codificación
$encodings=["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
print "Transfer-Encoding : ".$encodings[$part->encoding]."\n";
// se pasa a las posibles subpartes
if (isset($part->parts)) {
for ($i = 1; $i <= count($part->parts); $i++) {
// una nueva parte del mensaje
$subpart = $part->parts[$i - 1];
// llamada recursiva: se solicita el cuerpo de la parte [$subpart]
getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
}
}
}
Comentarios
- línea 3: recuperamos la estructura del mensaje;
- línea 6: solicitamos ver sus diferentes partes, que se encuentran en la tabla [parts] de la estructura;
- línea 10: la función [getParts] recibe los siguientes parámetros:
- [$imapResource]: la conexión al servidor IMAP;
- [$msgNumber]: el número de secuencia del mensaje del que se desean las partes;
- [$infos]: información para saber dónde almacenar las partes que se vayan a encontrar, en el sistema de archivos local;
- [$infosMail]: información general sobre el correo electrónico (remitente, destinatario(s), asunto…;
- [$part]: un objeto que representa una parte del mensaje;
- [$sectionNumber]: un número de sección (o parte) del mensaje;
- líneas 17-34: se muestra el tipo de contenido de la parte n.º [$section] del mensaje. Para ello, se utilizan los campos [$part→type] y [$part→subtype] de la parte [$part];
- líneas 36-37: se muestra el tipo de codificación de la parte [$sectionNumber];
- líneas 40-47: tal vez la parte cuya información acabamos de mostrar tenga a su vez subpartes;
- líneas 41-46: si es así, se solicita ver el tipo de contenido de las diferentes subpartes de la parte que acabamos de mostrar. Aquí se realiza una llamada recursiva a la función [getParts];
De nuevo enviamos un correo electrónico al usuario de Gmail [php7parlexemple@gmail.com] con el script [smtp-02.php] y lo leemos con el script anterior [imap-02.php]. Esto genera los siguientes resultados en la 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.
Logramos recuperar los diferentes tipos de contenido del mensaje, así como su tipo de codificación. La numeración de las partes sigue la siguiente regla:
- líneas 6-7: la parte [multipart/mixed], que representa la totalidad del mensaje, lleva el n.º 0. Las diferentes partes de este objeto llevarán entonces los n.º 1, 2…
El mensaje tiene un total de cinco partes:
- líneas 9-10: la parte [multipart/alternative], que lleva el n.º 1;
- líneas 17-18: la parte [APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT], que lleva el n.º 2. Se trata del archivo adjunto de un documento de Word;
- líneas 20-21: la parte [APPLICATION/PDF], que lleva el n.º 3. Se trata del archivo adjunto PDF;
- líneas 23-24: la parte [APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT] que lleva el n.º 4. Es el archivo adjunto OpenOffice;
- líneas 26-27: la parte [UNKNOWN/PNG], que lleva el n.º 5. Es el archivo adjunto de una imagen;
- líneas 30-31: la parte [MESSAGE/RFC822], que lleva el n.º 6. Es el archivo adjunto de un correo electrónico;
Cuando una parte tiene subpartes, estas se numeran como x.1, x.2…, donde x es el número de la parte que las engloba. Así:
- líneas 11-12: la primera parte de la parte [multipart/alternative] lleva el n.º 1.1. Se trata de un contenido de tipo [text/plain]: el mensaje del correo electrónico;
- líneas 14-15: la segunda parte de la parte [multipart/alternative] lleva el n.º 1.2. Se trata de un contenido de tipo [text/HTML]: el mensaje del correo electrónico en HTML;
- líneas 32-33: la primera parte del archivo adjunto [MESSAGE/RFC822] lleva el n.º 6.1. Se trata de un contenido de tipo [text/plain]. De hecho, según el estándar MIME, la numeración de las partes de un archivo adjunto de correo electrónico [MESSAGE/RFC822] difiere de la regla descrita anteriormente. Así, la primera parte del archivo adjunto [MESSAGE/RFC822] no lleva el n.º 6.1, sino otro número;
Ahora que sabemos cómo identificar las diferentes partes y subpartes de un correo electrónico, nos queda recuperar su contenido.
El código del script evoluciona de la siguiente manera:
function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
// cálculo del número de sección
if (substr($sectionNumber, 0, 2) === "0.") {
$sectionNumber = substr($sectionNumber, 2);
}
print "-----contenu de la partie n° [$sectionNumber]\n";
// tipo de contenido
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;
}
// tipo de codificación
$encodings = ["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
print "Transfer-Encoding : " . $encodings[$part->encoding] . "\n";
// ¿Es un mensaje?
if ($part->type === TYPEMESSAGE) {
// no se gestionarán las subpartes de este mensaje (correo adjunto)
// se muestra el cuerpo del correo adjunto
print imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
} else {
// pasamos a las posibles subpartes
if (isset($part->parts)) {
for ($i = 1; $i <= count($part->parts); $i++) {
// una nueva parte del mensaje
$subpart = $part->parts[$i - 1];
// llamada recursiva: se solicita el cuerpo de la parte [$subpart]
getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
}
} else {
// no hay subpartes; por lo tanto, se muestra el cuerpo del mensaje
print imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
}
}
}
Comentarios
- línea 46: la función [imap_fetchbody] recupera el cuerpo de la parte n.º [$sectionNumber] del mensaje. La numeración de las partes de un mensaje sigue la regla explicada anteriormente;
- línea 1: se comienza con la sección «0»;
- línea 41: las subpartes de esta sección se numerarán entonces como «0.1», «0.2», cuando deberían numerarse como «1», «2»…
- líneas 3-5: se corrige esta anomalía;
- líneas 37-43: si la parte actual tiene subpartes, se recorre cada una de ellas (líneas 38-43). Su número de sección es [$sectionNumber.$i];
- líneas 44-47: cuando ya no hay subpartes, se muestra el cuerpo de la parte actual con la función [imap_fetchbody]. En nuestro ejemplo, se trata de las partes [text/plain], [text/HTML] y los anexos;
La ejecución de este script arroja los siguientes 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.
Comentarios
- líneas 14-16: el contenido del mensaje de texto codificado en [quoted-printable] (línea 13);
- línea 20: el contenido del mensaje HTML codificado en [quoted-printable] (línea 19);
- líneas 24-28: el contenido del archivo de Word codificado en [base64] (línea 23);
- líneas 32-37: el contenido del archivo PDF codificado como [base64] (línea 31);
- líneas 41-45: el contenido del archivo OpenOffice codificado como [base64] (línea 40);
- líneas 50-55: el contenido del archivo de imagen codificado como [base64] (línea 49);
- líneas 59-63: el contenido del correo adjunto codificado como [base64] (línea 58);
Ahora que:
- sabemos cómo recuperar los textos de las diferentes partes de un correo electrónico;
- conocemos la codificación de estos textos;
podemos guardar estos textos en archivos.
El código evoluciona de la siguiente manera:
function getParts($imapResource, int $msgNumber, array $infos, object $infosMail, stdclass $part, string $sectionNumber = "0"): void {
// cálculo del número de sección
if (substr($sectionNumber, 0, 2) === "0.") {
$sectionNumber = substr($sectionNumber, 2);
}
print "-----contenu de la partie n° [$sectionNumber]\n";
// tipo de contenido
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;
}
// tipo de codificación
$encodings = ["7 bits", "8 bits", "binaire", "base 64", "quoted-printable", "autre"];
print "Transfer-Encoding : " . $encodings[$part->encoding] . "\n";
// ¿Es un mensaje?
if ($part->type === TYPEMESSAGE) {
// no se procesarán las subpartes de este mensaje
savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
} else {
// pasamos a las posibles subpartes
if (isset($part->parts)) {
for ($i = 1; $i <= count($part->parts); $i++) {
// una nueva parte del mensaje
$subpart = $part->parts[$i - 1];
// llamada recursiva: se solicita el cuerpo de la parte [$subpart]
getParts($imapResource, $msgNumber, $infos, $infosMail, $subpart, "$sectionNumber.$i");
}
} else {
// no hay subpartes; por lo tanto, se guarda el cuerpo del mensaje
savePart($imapResource, $msgNumber, $sectionNumber, $infos, $infosMail);
}
}
}
- líneas 33 y 45: la visualización del texto de una parte [$imapResource, $msgNumber, $sectionNumber] del correo electrónico ahora se reemplaza por su guardado en un archivo;
La función [savePart] es la siguiente:
// guardado de una parte del mensaje
function savePart($imapResource, int $msgNumber, string $sectionNumber, array $infos, object $infosMail): void {
// carpeta de almacenamiento
$outputDir = $infos["output-dir"] . "/message-$msgNumber";
// si la carpeta no existe, se crea
if (!file_exists($outputDir)) {
mkdir($outputDir);
}
// estructura de la parte que se va a guardar
$struct = imap_bodystruct($imapResource, $msgNumber, $sectionNumber);
// tipo de documento
$type = $struct->type;
// subtipo de documento
$subtype = "";
if (isset($struct->subtype)) {
$subtype = strtolower($struct->subtype);
}
// se analiza el tipo de la parte
switch ($type) {
case TYPETEXT:
// caso del mensaje de texto: 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:
// otros casos: solo nos interesan los archivos adjuntos
if (isset($struct->disposition)) {
$disposition = strtolower($struct->disposition);
if ($disposition === "attachment") {
// se trata de un archivo adjunto: se guarda
saveAttachment($imapResource, $msgNumber, $sectionNumber, $outputDir, $struct);
}
} else {
// no se procesará esta parte
print "Partie [$sectionNumber] ignorée\n";
}
break;
}
}
- líneas 3-8: creación del archivo de respaldo. Este lleva el número del mensaje cuyas partes se analizan;
- línea 10: la parte del mensaje que se va a guardar se define de manera única mediante los tres parámetros [$imapResource, $msgNumber, $sectionNumber]. Se solicita la estructura de esta parte con la función [imap_bodystruct];
- línea 12: se recupera el tipo principal de la parte del mensaje;
- líneas 13-17: se recupera su subtipo;
- líneas 20-30: se procesan los dos tipos de contenido: [text/plain] (líneas 23-25) y [text/HTML] (líneas 26-28). Los demás tipos [text/xx] se ignoran;
- línea 24: el texto de la parte [text/plain] se guardado en un archivo [message.txt];
- línea 27: el texto de la parte [text/HTML] se guardado en un archivo [message.HTML];
- líneas 31-43: se procesan las partes cuyo tipo principal no es [text];
- línea 35: solo se toman en cuenta los archivos adjuntos del mensaje;
- línea 37: estos se guardan en un archivo mediante la función [saveAttachment];
Si resumimos el código anterior:
- guarda las partes [text/plain] y [text/HTML] mediante la función [saveText]. Estas partes representan el contenido del correo;
- guarda los distintos archivos adjuntos mediante la función [saveAttachment];
La función [saveText] es la siguiente:
// guardar el texto [$text] del mensaje
function saveText(string $fileName, int $type, string $text, object $infosMail, object $struct) {
// preparación del texto para guardarlo
// $text está codificado: lo decodificamos
switch ($struct->encoding) {
case ENCBASE64:
$text = base64_decode($text);
break;
case ENCQUOTEDPRINTABLE:
$text = quoted_printable_decode($text);
break;
}
// encabezados del mensaje
// de
$from = "From: ";
foreach ($infosMail->from as $expéditeur) {
$from .= $expéditeur->mailbox . "@" . $expéditeur->host . ";";
}
// Para
$to = "To: ";
foreach ($infosMail->to as $destinataire) {
$to .= $destinataire->mailbox . "@" . $destinataire->host . ";";
}
// asunto
$subject = "Subject: " . $infosMail->subject;
// creación del texto a guardar
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;
}
// creación del archivo
print "sauvegarde d'un message dans [$fileName]\n";
// creación del archivo
if (! file_put_contents($fileName, $contents)) {
// error al crear el archivo
print "Impossible de créer le fichier [$fileName]\n";
}
}
Comentarios
- línea 1:
- [$fileName] es el nombre del archivo en el que se guardará el texto [$text];
- [$type]: toma el valor 0 para un archivo de texto y 1 para un archivo HTML;
- [$text]: es el texto que se va a guardar. Pero primero hay que decodificarlo, ya que está codificado;
- [$infosMail]: contiene información general sobre el correo electrónico. Vamos a utilizar los campos [from, to, subject];
- [$struct]: es la estructura que describe la parte del correo que estamos guardando. Esto nos permitirá conocer el tipo de codificación del texto que se va a guardar;
- líneas 4-12: decodificamos el texto que se va a guardar;
- líneas 13-25: se recupera la información [from, to, subject] del correo electrónico;
- líneas 27-36: según el tipo, 0 o 1, del texto que se va a guardar, se genera un texto sin formato (línea 30) o un texto HTML (línea 34);
- línea 40: el texto completo se guarda en el archivo [$fileName];
Los archivos adjuntos se guardan con la siguiente función [saveAttachment]:
// guardado de un archivo adjunto
function saveAttachment($imapResource, int $msgNumber, string $sectionNumber, string $outputDir, object $struct) {
// se analiza la estructura del archivo adjunto
// se busca recuperar el nombre del archivo en el que guardar el archivo adjunto
// ese nombre se encuentra en los [dparameters] de la estructura
if (isset($struct->dparameters)) {
// se recuperan los [dparameters]
$dparameters = $struct->dparameters;
$fileName = "";
// se recorre la tabla de los [dparameters]
foreach ($dparameters as $dparameter) {
// cada [dparameter] es un objeto con dos atributos [attribute, value]
$attribute = strtolower($dparameter->attribute);
// el atributo [filename] corresponde al nombre del archivo que se va a crear
// en este caso, el nombre del archivo se encuentra en [$dparameter->value]
if ($attribute === "filename") {
$fileName = $dparameter->value;
break;
}
}
// si no se ha encontrado ningún nombre de archivo, se busca en el atributo [parameters] de la estructura
if ($fileName === "" && isset($struct->parameters)) {
// se recuperan los [parameters]
$parameters = $struct->parameters;
foreach ($parameters as $parameter) {
// cada parámetro es un diccionario con dos claves [attribute, value]
$attribute = strtolower($parameter->attribute);
// si el atributo es [name], entonces [value] es el nombre del archivo
if ($attribute === "name") {
$fileName = $parameter->value;
// el nombre del archivo puede estar codificado
// por ejemplo =?utf-8?Q?Cursos-Tutoriales-Serge-Tah=C3=A9-1568x268=2Ep
// se recupera la codificación con una expresión regular
$champs = [];
$match = preg_match("/=\?(.+?)\?/", $fileName, $champs);
// si hay coincidencia, entonces se decodifica el nombre del archivo
if ($match) {
$fileName = iconv_mime_decode($fileName, 0, $champs[1]);
}
break;
}
}
}
}
// si se ha encontrado un nombre de archivo, se guarda el archivo adjunto
if ($fileName !== "") {
// guardar el archivo adjunto
$fileName = "$outputDir/$fileName";
print "sauvegarde de l'attachement dans [$fileName]\n";
// creación de archivo
if ($file = fopen($fileName, "w")) {
// se recupera el texto codificado del archivo adjunto
$text = imap_fetchbody($imapResource, $msgNumber, $sectionNumber);
// el archivo adjunto está codificado; lo decodificamos
switch ($struct->encoding) {
// base 64
case ENCBASE64:
$text = base64_decode($text);
break;
// quoted printable
case ENCQUOTEDPRINTABLE:
$text = quoted_printable_decode($text);
break;
default:
// se ignoran los demás casos
break;
}
// escritura del texto en el archivo
fputs($file, $text);
// cierre del archivo
fclose($file);
} else {
// fallo al crear el archivo
print "L'attachement n'a pu être sauvegardé dans [$fileName]\n";
}
}
}
Comentarios
- línea 2: la función [saveAttachment] admite los siguientes parámetros:
- [$imapResource, int $msgNumber, string $sectionNumber] define de manera única la parte IMAP que se va a guardar;
- [string $outputDir] es la carpeta de almacenamiento;
- [object $struct] describe la estructura de la parte del mensaje que se va a guardar;
- líneas 6-44: se busca el nombre del archivo asociado al archivo adjunto. Se utilizará ese mismo nombre de archivo para guardarlo. El nombre del archivo adjunto se encuentra en la tabla [$struct→dparameters] o en la tabla [$struct→parameters], o incluso en ambas;
- líneas 30-40: si el nombre del archivo contiene caracteres no codificados en 7 bits, entonces se ha codificado en [quoted-printable]. En este caso, en [$struct→dparameters], el atributo se llama [fileName*] en lugar de [fileName]. Esto significa que no cumplió con la condición de la línea 16. Entonces, se busca el nombre del archivo en la tabla [$struct→parameters];
- línea 32: un ejemplo de nombre de archivo codificado. Tiene la siguiente forma: =?codage_original?codage_actuel?nom_encodé. Así, el nombre [=?utf-8?Q?Cours-Tutoriels-Serge-Tah=C3=A9-1568x268=2Ep] significa que el nombre del archivo era UTF-8 y que actualmente es [quoted-printable] (Q);
- línea 38: el nombre del archivo se decodifica con la función [iconv_mime_decode], que en este caso admite tres parámetros:
- la cadena a decodificar;
- dejar en 0 por defecto;
- el juego de caracteres que se utilizará para representar la cadena decodificada. Este parámetro está presente en la cadena a decodificar. Se obtiene mediante una expresión regular en las líneas 34-35;
- líneas 45-75: se guarda el archivo adjunto en un archivo con el nombre que se encontró;
Para probar el script [imap-02.php], primero enviamos un correo electrónico a [guest@localhost] con la siguiente configuración:
Por lo tanto, hay cinco archivos adjuntos.
Leemos el correo enviado con [imap-02.php] y la siguiente configuración:
Los resultados de la consola son los siguientes:
------------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.
Los archivos guardados se encuentran en la carpeta [output/localhost-pop3/message-N]:

16.6.6. Cliente POP3 / IMAP con la biblioteca [php-mime-mail-parser]
En el script anterior [imap-02.php], pudimos guardar:
- los contenidos [text/plain] y [text/HTML] del correo electrónico;
- los archivos adjuntos del correo electrónico;
En el caso de un archivo adjunto de tipo [message/rfc822], también guardamos el contenido del archivo adjunto. Sin embargo, este tipo de archivo adjunto es en sí mismo un correo electrónico que, a su vez, contiene los contenidos [text/plain] y [text/HTML], así como archivos adjuntos. Por lo tanto, podríamos encontrarnos en la siguiente situación:
- un archivo [mail 1] cuya estructura es similar a la de un archivo adjunto de tipo [message/rfc822];
- un archivo [mail 2] adjunto al correo 1;
- un [mail 3] adjunto al correo 2;
- etc…
El script [imap-02.php] guarda el contenido de [mail 1] (textos y archivos adjuntos). Guarda [mail 2] como documento adjunto, pero se detiene ahí. No intenta analizar [mail 2] para extraer los textos y los archivos adjuntos. Se podría pensar que basta con aplicar a [mail 2] lo que se hizo con [mail 1]. Una llamada recursiva al método que procesaba [mail 1] podría entonces ser suficiente para obtener el contenido de todos los correos anidados unos dentro de otros. Desafortunadamente, las partes de [mail 2] están numeradas con una lógica diferente a la utilizada para [mail 1], lo que impide utilizar el mismo algoritmo en ambos casos, a menos que se emplee una lógica bastante compleja para calcular los números de las partes de un correo electrónico, independientemente de la posición de este dentro del conjunto de correos anidados.
El script [imap-02.php] ya era complejo. Para evitar complicarlo aún más al manejar los contenidos de los correos anidados, utilizaremos la biblioteca [php-mime-mail-parser] disponible en GitHub (mayo de 2019) en URL [https://github.com/php-mime-mail-parser/php-mime-mail-parser] y escrita por Vincent Dauce.
16.6.6.1. Instalación de la biblioteca [php-mime-mail-parser]
La página de presentación de la biblioteca indica cómo instalarla en Windows:

Hay dos pasos para OS en Windows:
télécharger une DLL ;
modifier le fichier [php.ini] qui configure PHP ;
LA DLL de la biblioteca [mailparse] está disponible en URL [http://pecl.php.net/package/mailparse] (mayo de 2019);

- en [2], elija la versión más reciente y estable de la biblioteca;

- en [3], elija la versión de PHP que esté utilizando (en este documento es PHP 7.2);
- en [4], elige la versión de tu OS para Windows (en este caso es un Windows de 64 bits). Tomamos la versión [Thread Safe];
Para conocer la versión de PHP descargada con Laragon, abre un [Terminal] desde la ventana de Laragon y escribe el siguiente 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
La versión de PHP 7.2.11 aparece en la línea 3. En la misma línea se indica la versión de Windows utilizada para la compilación (32 o 74 bits).
Una vez obtenido el archivo DLL, hay que copiarlo en la carpeta [<laragon>/bin/php/<version-php>/ext] [5]:

Una vez hecho esto, hay que habilitar esta extensión en el archivo [php.ini], que configura PHP (véase el párrafo del enlace):

Es probable que la línea [7] no exista y que tengas que agregarla tú mismo.
Una vez activada la extensión, se puede verificar su validez ingresando el siguiente comando en un terminal de 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)
El comando [php –-ini] carga el archivo de configuración de la línea 4. A continuación, cargará los DLL de todas las extensiones activadas en [php.ini]. Si alguna de ellas es incorrecta, se indicará. De esta manera, se verificará la validez del archivo DLL agregado a [php_mailparse.dll]. Puede declararse incorrecto por diversas razones, entre las que las más frecuentes son las siguientes:
- has descargado un archivo DLL que no corresponde a la versión de PHP utilizada;
- has descargado un archivo DLL de 32 bits cuando tienes un PHP de 64 bits, o viceversa;
Una vez que la extensión esté activada y verificada, puede proceder a la instalación de la biblioteca [php-mime-mail-parser]:

El comando [8] debe introducirse en un terminal de Laragon (véase el párrafo del enlace):

- en [1], asegúrate de que te encuentres en la carpeta [<laragon>/www];
- en [2], el comando de instalación de la biblioteca [php-mime-mail-parser];
- en [3], no se ha instalado nada aquí porque la biblioteca [php-mime-mail-parser] ya estaba instalada;
La instalación de la biblioteca [php-mime-mail-parser] se realiza en la carpeta [<laragon>/www/vendor]:


- en [2-3], los archivos fuente de la biblioteca [php-mime-mail-parser];
Ahora que se ha instalado el entorno de trabajo, podemos pasar a escribir el script [imap-03.php].
16.6.6.2. El script [imap-03.php]
El script [imap-03.php] utiliza el mismo archivo de configuración [config-imap-01.json] que los scripts anteriores:
El script [imap-03.php] es el siguiente:
<?php
// cliente IMAP (Protocolo de acceso a mensajes de Internet) que permite leer correos electrónicos
// escrito con la biblioteca [php-mime-mail-parser]
// disponible enURL [https://github.com/php-mime-mail-parser/php-mime-mail-parser] (mayo de 2019)
//
// cumplimiento estricto de los tipos declarados de los parámetros de las funciones
declare (strict_types=1);
// manejo de errores
error_reporting(E_ALL & ~ E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
//ini_set("display_errors", "off");
//
// dependencias
require_once 'C:/myprograms/laragon-lite/www/vendor/autoload.php';
// parámetros de lectura de correo
const CONFIG_FILE_NAME = "config-imap-01.json";
// se recupera la configuración
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);
// lectura de los buzones
foreach ($mailboxes as $name => $infos) {
// seguimiento
print "------------Lecture de la boîte à lettres [$name]\n";
// lectura del buzón
readmailbox($name, $infos);
}
// fin
exit;
Comentarios
- líneas 18-23: el contenido del archivo de configuración se coloca en el diccionario [$mailboxes];
- líneas 26-31: cada buzón es leído por la función [readmailbox] (línea 30). Esta función lee, de hecho, los mensajes no leídos del buzón. Un buzón corresponde a la dirección de correo electrónico de un usuario específico;
La función [readmailbox] es la siguiente:
function readmailbox(string $name, array $infos): void {
// se inicia sesión
$imapResource = imap_open($name, $infos["user"], $infos["password"]);
if (!$imapResource) {
// fallo
print "La connexion au serveur [$name] a échoué : " . imap_last_error() . "\n";
exit;
}
// Conexión establecida
print "Connexion établie avec le serveur [$name].\n";
// total de mensajes en el buzón
$nbmsg = imap_num_msg($imapResource);
print "Il y a [$nbmsg] messages dans la boîte à lettres [$name]\n";
// mensajes no leídos en el buzón actual
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 {
// Se recorre la lista de mensajes no leídos
foreach ($msgNumbers as $msgNumber) {
print "---message n° [$msgNumber]\n";
// se recupera el cuerpo del mensaje n.º $msgNumber
getMailBody($imapResource, $msgNumber, $infos);
// si el protocolo es POP3, se elimina el mensaje después de recuperarlo
$pop3 = $infos["pop3"];
if ($pop3 !== NULL) {
// se marca el mensaje como «para eliminar»
imap_delete($imapResource, $msgNumber);
}
}
// fin de la lectura de los mensajes no leídos
if ($pop3 !== NULL) {
// se eliminan los mensajes marcados como «para eliminar»
imap_expunge($imapResource);
}
}
}
// cierre de la conexión
$imapClose = imap_close($imapResource);
if (!$imapClose) {
// fallo
print "La fermeture de la connexion a échoué : " . imap_last_error() . "\n";
} else {
// éxito
print "Fermeture de la connexion réussie.\n";
}
}
Comentarios
El código de la función [readmailbox] es el mismo que en los scripts anteriores.
La función [getMailBody] (línea 25), que analiza el cuerpo de un mensaje (contenido + archivos adjuntos), es la siguiente:
// análisis del cuerpo del mensaje
function getMailBody($imapResource, int $msgNumber, array $infos): void {
// se recupera el texto completo del mensaje
$text = imap_fetchbody($imapResource, $msgNumber, "");
if ($text === FALSE) {
print "Le corps du message [$msgNumber] n'a pu être récupéré";
return;
}
// se crea un analizador que analizará el texto del mensaje
$parser = (new PhpMimeMailParser\Parser())->setText($text);
// se recuperan las diferentes partes del mensaje
$outputDir = $infos["output-dir"] . "/message-$msgNumber";
getParts($parser, $msgNumber, $outputDir);
}
Comentarios
- línea 2: la función [getMailBody] acepta tres parámetros:
- [$imapResource]: el recurso IMAP al que se está conectado;
- [$msgNumber]: el número del mensaje (en el buzón) que se va a procesar;
- [$infos]: información diversa sobre el buzón procesado;
- línea 4: se recupera la totalidad del mensaje n.º [$msgNumber];
- líneas 5-8: caso en el que no se pudo recuperar el contenido del mensaje;
- línea 10: se comienza a utilizar la biblioteca [php-mime-mail-parser]. El objeto [$parser] se encargará de analizar el texto del mensaje;
- línea 12: [$outputDir] será la carpeta en la que se guardarán los contenidos de texto y los archivos adjuntos del mensaje n.º [$msgNumber];
- línea 13: se le pide a la función [getParts] que identifique las diferentes partes (contenido de texto y archivos adjuntos) del mensaje n.º [$msgNumber] y que las guarde en la carpeta [$outputDir];
La función [getParts] es la siguiente:
// recuperación de las diferentes partes de un mensaje
function getParts(PhpMimeMailParser\Parser $parser, int $msgNumber, string $outputDir): void {
// se crea la carpeta de copia de seguridad del mensaje si es necesario
if (!file_exists($outputDir)) {
if (!mkdir($outputDir)) {
print "Le dossier [$outputDir] n'a pu être créé\n";
return;
}
}
// se recuperan los encabezados del mensaje
$arrayHeaders = $parser->getHeaders();
// se guardan los mensajes de texto
$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");
}
// se guardan los mensajes 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");
}
// se recuperan los archivos adjuntos del mensaje
$attachments = $parser->getAttachments();
// número del archivo adjunto
$iAttachment = 0;
// se recorre la lista de archivos adjuntos
foreach ($attachments as $attachment) {
// tipo de archivo adjunto
$fileType = $attachment->getContentType();
print "-- Sauvegarde d'un attachement de type [$fileType] dans le fichier [$outputDir/{$attachment->getFilename()}]\n";
// se guarda el archivo adjunto
try {
$attachment->save($outputDir, PhpMimeMailParser\Parser::ATTACHMENT_DUPLICATE_SUFFIX);
} catch (Exception $e) {
print "L'attachement n'a pu être sauvegardé : " . $e->getMessage() . "\n";
}
// caso particular del tipo message/rfc822
if ($fileType === "message/rfc822") {
// el archivo adjunto es en sí mismo un mensaje; también lo analizaremos
// se cambia el directorio de almacenamiento
$iAttachment++;
$outputDir = $outputDir . "/rfc822-$iAttachment";
// se cambia el contenido a analizar
$parser->setText($attachment->getContent());
// se analiza el mensaje de manera recursiva
getParts($parser, $msgNumber, $outputDir);
}
}
}
Comentarios
- línea 2: la función [getParts] admite tres parámetros:
- un analizador [$parser] al que se le ha enviado el texto completo del mensaje a analizar;
- [$msgNumber] es el número del mensaje que se está analizando;
- [$outputDir] es la carpeta en la que se deben guardar los contenidos y los archivos adjuntos del mensaje;
- líneas 4-9: creación de la carpeta [$outputDir];
- línea 11: se recuperan los encabezados del mensaje que se está analizando (de, para, asunto…);
- línea 13: se recuperan las partes del correo electrónico con el tipo [text/plain]. Se obtiene una tabla;
- líneas 14-17: se guardan todos los elementos de la tabla recuperada, asignando a cada uno un nombre de archivo diferente;
- línea 19: se recuperan las partes del correo electrónico con el tipo [text/html]. Se obtiene una matriz;
- líneas 20-23: se guardan todos los elementos del array recuperado, asignando a cada uno un nombre de archivo diferente;
- línea 25: se recupera la lista de archivos adjuntos del mensaje analizado;
- línea 29: se recorre esta lista;
- línea 24: se recupera el tipo del archivo adjunto (atributo Content-Type);
- líneas 34-38: se guarda el archivo adjunto en la carpeta [$outputDir]. El segundo parámetro, [PhpMimeMailParser\Parser::ATTACHMENT_DUPLICATE_SUFFIX], es una estrategia de nomenclatura para los archivos adjuntos. Si [$attachment→getFilename()] es igual a X y el archivo X ya existe, entonces la biblioteca [php-mime-mail-parser] prueba los nombres [X_1], [X_2], etc., hasta encontrar un nombre de archivo que no exista;
- línea 40: se verifica si el archivo adjunto es un correo electrónico;
- líneas 41-48: si es así, entonces se analiza a su vez ese correo para extraer su contenido y sus archivos adjuntos;
- línea 44: si [$outputDir] es igual a X y entre los archivos adjuntos del mensaje analizado hay dos correos electrónicos, entonces el primero se guardará en la carpeta [$outputDir/rfc822-1] y el segundo en la carpeta [$outputDir/rfc822-2];
- línea 46: el contenido del correo adjunto se convierte en el nuevo texto que se va a analizar;
- línea 48: se llama a la función [getParts] de manera recursiva para analizar el nuevo texto;
La función [saveMessage] guarda los contenidos de texto del mensaje que se va a analizar:
// guardar un mensaje de texto
function saveMessage(string $text, int $type, array $arrayHeaders, string $filename): void {
// contenido a guardar
$contents = "";
// se agregan los encabezados
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";
}
// agregar el texto del mensaje
$contents .= $text;
// guardar todo
if (!file_put_contents($filename, $contents)) {
// fallo
print "Le message n'a pu être sauvegardé dans le fichier [$filename]\n";
} else {
// éxito
print "Le message a été sauvegardé dans le fichier [$filename]\n";
}
}
Comentarios
- La función [saveMessage] admite los siguientes parámetros:
- [$text]: el texto que se va a guardar;
- [$type]: el tipo de texto (0: text/plain, 1: text/HTML);
- [$arrayHeaders]: las cabeceras del mensaje analizado;
- [$filename]: el nombre del archivo en el que se debe guardar [$text];
- línea 4: [$contents] representará todo el texto que se va a guardar;
- líneas 6-20: primero se guardarán todos los encabezados del mensaje (from, to, subject…);
- líneas 16-19: en el caso de un texto HTML, cada línea termina con la etiqueta <br/> para que cada encabezado aparezca solo en su línea en un navegador;
- línea 22: se agrega a los encabezados el texto del mensaje que se va a guardar;
- líneas 24-30: todo se guarda en el archivo [$filename];
El uso de la biblioteca [php-mime-mail-parser] facilita considerablemente la escritura del script para leer correos electrónicos.
El script [smtp-02.php] se utiliza para enviar un correo electrónico al usuario [guest@localhost] con la siguiente configuración:
- líneas 11-15: hay cinco archivos adjuntos;
- línea 15: [test-localhost-2.eml] es un correo electrónico estructurado de la siguiente manera:
- [test-localhost-2.eml] contiene 4 archivos adjuntos (los mismos que en las líneas 11-14) y un correo adjunto;
- el correo adjunto a [test-localhost-2.eml] contiene 4 archivos adjuntos (los mismos que en las líneas 11-14);
El script [imap-03.php] se utiliza para leer el buzón del usuario [guest@localhost] con la siguiente configuración:
Tras su ejecución, la estructura de carpetas de [output/localhost-pop3] quedó de la siguiente manera:

- en [1], los 5 archivos adjuntos del correo recibido por [guest@localhost];
- en [2], los 5 archivos adjuntos del correo [test-localhost-2.eml] de [1];
- en [3], los 4 archivos adjuntos del correo electrónico [test-localhost.eml] de [2];
Los mensajes de la consola son los siguientes:
------------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.
Si visualizamos [message_1.HTML] de [3] en un navegador, obtenemos lo siguiente:
