Skip to content

16. Funciones de red

Ahora abordamos las funciones de red de PHP que nos permiten realizar la programación TCP / IP (Protocolo de control de transferencia / Protocolo de Internet).

Image

16.1. Fundamentos de la programación web

16.1.1. Generalidades

Consideremos la comunicación entre dos máquinas remotas A y B:

Image

Cuando una aplicación AppA de una máquina A quiere comunicarse con una aplicación AppB de una 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 trabaja 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 destinada dicha información. Las aplicaciones de la máquina B acceden a la red a través de ventanas, también denominadas 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 diálogo aceptado por la aplicación AppB. De hecho, las máquinas A y B van a «comunicarse». Lo que van a decir quedará encapsulado en los protocolos TCP-IP. No obstante, 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 análogo 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. La voz se codificará en forma de señales en el teléfono A, se transmitirá por las líneas telefónicas y llegará al teléfono B para ser descodificada. La persona B oye entonces las 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 forma útil;

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 estudiaremos las comunicaciones de red que utilizan el protocolo de transporte TCP, cuyas principales características son las siguientes:

  • el proceso que desea transmitir establece primero una conexión con el proceso destinatario de la información que va a transmitir. Esta conexión se establece 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 emitidos por el proceso de origen siguen esta ruta virtual y llegan en el orden en que fueron emitidos;
  • la información enviada tiene un carácter continuo. El proceso emisor envía información a su propio ritmo. Esta no se envía necesariamente de inmediato: el protocolo TCP espera a tener suficiente para enviarla. Se almacena en una estructura denominada segmento TCP. Una vez lleno, este segmento 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 destinatario comprueba que recibe los segmentos en secuencia. Por cada segmento recibido correctamente, envía un acuse de recibo al remitente;
  • cuando este último lo recibe, lo indica al proceso emisor. De este modo, este puede saber que un segmento ha llegado a su destino;
  • si, tras un cierto tiempo, el protocolo TCP que ha emitido 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. Así, el proceso de destino puede enviar acuses de recibo incluso mientras el proceso de origen sigue enviando información. Esto permite, por ejemplo, que el protocolo TCP de origen envíe varios segmentos sin esperar el acuse de recibo. Si, tras un cierto tiempo, se da cuenta de que no ha recibido el acuse de recibo de un determinado segmento 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 acepta o rechaza la solicitud. Si acepta, la máquina A puede enviar sus solicitudes al servicio SB1. Estas deben ajustarse al protocolo de diálogo que entiende el servicio SB1. De este modo, se establece un diálogo de solicitud-respuesta entre la máquina A, denominada máquina cliente, y la máquina B, denominada máquina servidor. Uno de los dos interlocutores 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 de 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 trata de forma diferente la solicitud de conexión inicial de un cliente y sus solicitudes posteriores destinadas a obtener un servicio. El programa no presta el servicio por sí mismo. Si lo hiciera, durante la duración del servicio ya no estaría a la escucha de las solicitudes de conexión y las clients no serían atendidas. Por lo tanto, procede de otra manera: tan pronto como se recibe una solicitud de conexión en el puerto de escucha y se acepta, el servidor crea una tarea encargada de prestar el servicio solicitado por el cliente. Este servicio se presta en otro puerto del servidor denominado puerto de servicio. De este modo, se pueden atender varias clients 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. Descubrir 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 denomina protocolo de comunicación del servidor. Entre los protocolos más comunes de Internet se encuentran los siguientes:

  • HTTP: Protocolo de Transferencia HTTP (HyperText) —el protocolo de comunicación con un servidor web (servidor HTTP);
  • SMTP: Simple Mail Transfer Protocol —el protocolo de comunicación con un servidor de envío de correo electrónico (servidor SMTP);
  • POP: Protocolo de oficina Post: 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 (Internet Message Access Protocol): el protocolo de comunicación con un servidor de almacenamiento de correo electrónico (servidor IMAP). Este protocolo ha sustituido progresivamente al protocolo POP, 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 tenemos 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 teclee;

entonces podemos comunicarnos con un servidor TCP que utilice un protocolo de líneas de texto, siempre que conozcamos las reglas de dicho protocolo.

16.2.2. Utilidades TCP

Image

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 clients en un puerto P;

El servidor TCP [RawTcpServer]llama con la sintaxis [RawTcpServeur port] para crear un servicio TCP en el puerto [port] de la máquina local (el ordenador en el que estás trabajando):

  • el servidor puede atender varios clients simultáneamente;
  • el servidor ejecuta los comandos que el usuario escribe en el teclado. Estos son los siguientes:
    • list: muestra los clients actualmente conectados al servidor. Estos se muestran en formato [id=x-nom=y]. El campo [id] sirve para identificar los clients;
    • 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 del equipo en el 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 situamos en cada una de ellas en la carpeta de utilidades. En una de las ventanas iniciamos el servidor [RawTcpServer] en el puerto 100:

Image

  • en [1], nos encontramos 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 introducido por el usuario mediante el teclado;

En la otra ventana de comandos, se inicia el cliente TCP:

Image

  • 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á trabajando);
  • En [7], el cliente ha logrado 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 introducido por el usuario mediante el teclado;

Volvamos a la ventana del servidor. Su contenido ha cambiado:

Image

  • en [9], se ha detectado un cliente. El servidor le ha asignado el n.º 1. El servidor ha identificado correctamente al cliente remoto (máquina y puerto);
  • en [10], el servidor vuelve a esperar a un nuevo cliente;

Volvamos a la ventana del cliente y enviemos un comando al servidor:

Image

  • en [11], el comando enviado al servidor;

Volvamos a la ventana del servidor. Su contenido ha cambiado:

Image

  • en [12], entre corchetes, el mensaje recibido por el servidor;

Enviemos una respuesta al cliente:

Image

  • en [13], la respuesta enviada al cliente 1. Solo se envía el texto entre corchetes, no los corchetes en sí;

Volvamos a la ventana del cliente:

Image

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

Image

  • en [15], solicitamos la lista de clients;
  • en [16], la respuesta;
  • en [17], cerramos la conexión con el cliente n.º 1;
  • en [18], la confirmación del servidor;
  • en [19], detenemos el servidor;
  • en [20], la confirmación del servidor;

Volvamos a la ventana del cliente:

Image

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

Image

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

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

Los registros del cliente son los siguientes:

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

16.3. Obtener el nombre o la dirección IP de un equipo de Internet

Image

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. Pero, en definitiva, solo se utiliza la dirección IP. Por lo tanto, a veces es necesario conocer la dirección IP de un equipo identificado 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);
//
// gestión 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 solicita que PHP señale todos los errores (E_ALL y E_STRICT) y que estos se muestren. Este modo solo se recomienda en modo desarrollo para mejorar el código con las advertencias de PHP. En modo producción, en la línea 8, se pondría «off». Desde PHP 5.4, 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» de la máquina denominada $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 de la dirección $ip con el formato «ip3.ip2.ip1.ip0». Si el equipo $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

Image

Cuando un navegador muestra un URL, actúa como cliente de un servidor web o, dicho de otro modo, 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:

Image

A continuación, con un navegador, solicitamos el URL [localhost:100], es decir, indicamos que el servidor HTTP consultado opera en el puerto 100 de la máquina local:

Image

Volvamos a la ventana del servidor:

Image

  • en [3], el cliente que se ha conectado;
  • en [4-7], la serie de líneas de texto que ha enviado:
    • en [4]: esta línea tiene el formato [GET URL HTTP/1.1]. Solicita el URL / y 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. Recordemos 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 si existen 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:

Image

16.4.2. Ejemplo 2

Ahora que conocemos los comandos enviados por 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, a continuación, el servidor web Apache:

Image

Image

Ahora, con un navegador, solicitemos el 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:

Image

  • en [1], en lugar del 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] es implícito cuando no se menciona ningún protocolo y el puerto [80] es 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:

Image

  • Hacemos clic con el botón derecho en la página recibida y seleccionamos option [2]. Obtenemos el siguiente código fuente:

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

Ahora solicitemos el URL [http://localhost:80] con nuestro cliente TCP:

Image

  • En [1], nos conectamos al puerto 80 del servidor localhost. Ahí es donde opera el servidor web de Laragon;

Ahora escribimos los comandos que hemos visto en el párrafo anterior:

Image

  • en [1], el comando [GET]. Solicitamos la raíz / del servidor web;
  • en [2], el comando [Host];
  • estas son las dos únicas órdenes imprescindibles. Para el resto de órdenes, el servidor web tomará los valores por defecto;
  • en [3], la línea en blanco que debe terminar los comandos del cliente;
  • debajo de la línea 3, viene la respuesta del servidor web;
  • en [4] hasta la línea vacía [5] aparecen los encabezados HTTP de la respuesta del servidor;
  • después de la línea [5] viene el documento HTML solicitado [6];

Escribimos [quit] para cerrar el cliente y cargamos el archivo de registros [localhost-80.txt]:

--> [GET / HTTP/1.1]
--> [Host: localhost:80]
--> []
<-- [HTTP/1.1 200 OK]
<-- [Date: Thu, 16 May 2019 14:24:39 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]
<-- []
<-- [<!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>]
  • líneas 11-79: el documento HTML recibido. En el ejemplo anterior, Firefox había recibido el mismo;

Ahora tenemos las bases para programar un cliente TCP que solicitaría un URL.

16.4.3. Ejemplo 3

Image

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:

{
    "localhost": {
        "port": 80,
        "GET": "/",
        "Host": "localhost:80",
        "User-Agent": "client PHP",
        "Accept": "text/HTML",
        "Accept-Language": "fr",
        "endOfLine":"\r\n"
    }
}
  • línea 2: el nombre del equipo que aloja el servidor web al que se quiere 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);
//
// gestión 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 índice del sitio $ite
  $résultat = getURL($site, $protocole);
  // visualización del resultado
  print "$résultat\n";
}//para
// fin
exit;

//-----------------------------------------------------------------------
function getURL(string $site, array $protocole, $suivi = TRUE): string {
  // lee el $siteURL y lo almacena en el archivo $site.HTML
  // el diálogo cliente/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 diálogo 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 en \n o \r\n
  $endOfLine = $protocole["endOfLine"];
  // por simplificación, no se comprueban los casos de error en la comunicación cliente/servidor
  // el cliente envía el comando GET para solicitar el URL $protocolo["GET"]
  // sintaxis GET URL HTTP/1.1
  $commande = "GET " . $protocole["GET"] . " HTTP/1.1$endOfLine";
  // ¿seguimiento?
  if ($suivi) {
    print "--> $commande";
  }
  // se envía el comando 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";
      // ¿Seguimiento?
      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. El cliente lee, por tanto, 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; los encabezados 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 ha cerrado la conexión; a su vez, el cliente la cierra
  fclose($connexion);
  // cierre del archivo $HTML
  fclose($HTML);
  // retorno
  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 va a consultar;
    • los valores establecen el protocolo HTTP que se debe respetar;
  • 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.Por defecto, las comunicaciones cliente/servidor se registran en la consola ($suivi=TRUE);
  • línea 33: la función fsockopen($site,$port,$errNumber,$erreur) permite crear una conexión con un servicio TCP / IP que opera en el puerto $port del equipo $site. Si la conexión falla, [$errNumber] es un número de error y [$erreur] el mensaje de error asociado. Una vez establecida la conexión cliente/servidor, numerosos servicios TCP / IP intercambian líneas de texto. Este es el caso del protocolo HTTP (HyperText Transfer Protocol). El flujo del servidor que llega al cliente puede entonces tratarse como un archivo de texto leído con [fgets]. Lo mismo ocurre con el flujo que va del cliente al servidor, que 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 version 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á en primer lugar una serie de encabezados HTTP que proporcionarán diversa información sobre el documento solicitado. Estos encabezados terminan con una línea vacía;
  • 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, marca de fin de línea) del final de la línea;
  • línea 97: se comprueba 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 totalidad de la página que se le ha solicitado, 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: cliente PHP
--> Accept: text/HTML
--> Idioma aceptado: fr
Réponse du serveur [localhost] ----------------------------
<-- HTTP/1.1 200 OK
<-- Fecha: Jue, 16 de mayo de 2019 15:43:18 GMT
<-- Servidor: 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>

Hemos obtenido el mismo documento que con el navegador Firefox.

16.4.4. Ejemplo 4

En este ejemplo, vamos a demostrar que el cliente HTTP que hemos escrito es insuficiente. Modifiquemos el archivo de configuración [config-http-01.json] de la siguiente manera:

{
    "tahe.developpez.com": {
        "port": 443,
        "GET": "/",
        "Host": "sergetahe.com:443",
        "User-Agent": "script PHP 7",
        "Accept": "text/HTML",
        "Accept-Language": "fr",
        "endOfLine":"\n"
    }
}

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 seguro http denominado https. En este protocolo, el diálogo cliente/servidor comienza con un intercambio de información que va a asegurar la conexión. El cliente debe entonces utilizar el protocolo [HTTPS] y no el protocolo [HTTP], lo que nuestro cliente no hace.

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
<-- Fecha: viernes, 17 de mayo de 2019 13:02:26 GMT
<-- Servidor: Apache/2.4.25 (Debian)
<-- Longitud del contenido: 454
<-- Conexión: cerrar
<-- Tipo de contenido: 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] ha respondido 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.

Utilicemos ahora el siguiente archivo de configuración:

{
    "sergetahe.com": {
        "port": 80,
        "GET": "/cours-tutoriels-de-programmation/",
        "Host": "sergetahe.com:80",
        "User-Agent": "script PHP 7",
        "Accept": "text/HTML",
        "Accept-Language": "fr",
        "endOfLine": "\n"
    }
}

Los resultados de la consola son entonces los siguientes:


Client : début de la communication avec le serveur [sergetahe.com] ----------------------------
--> GET /cursos-tutoriales-de-programación/ 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
<-- Fecha: viernes, 17 de mayo de 2019 13:36:06 GMT
<-- Content-Type: text/HTML; charset=UTF-8
<-- Transfer-Encoding: chunked
<-- Servidor: 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 indica al cliente el número de caracteres del siguiente fragmento enviado por el servidor. Esto es lo que se ve en el archivo [output/sergetahe.com.HTML]:

Image

  • en [1] y [2], el tamaño en hexadecimal de los fragmentos 1 y 2 del documento;

Un cliente HTTP correcto no debería dejar estos números en el documento HTML final.

He aquí otro ejemplo:

{
    "sergetahe.com": {
        "port": 80,
        "GET": "/cours-tutoriels-de-programmation",
        "Host": "sergetahe.com:80",
        "User-Agent": "script PHP 7",
        "Accept": "text/HTML",
        "Accept-Language": "fr",
        "endOfLine": "\n"
    }
}

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. La ejecución del cliente HTTP da entonces los siguientes resultados en la consola:


Client : début de la communication avec le serveur [sergetahe.com] ----------------------------
--> GET /cursos-tutoriales-de-programación 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 Movido permanentemente
<-- Fecha: viernes, 17 de mayo de 2019, 13:47:00 GMT
<-- Content-Type: text/HTML; charset=iso-8859-1
<-- Content-Length: 262
<-- Servidor: Apache
<-- Ubicación: 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. Obsérvese esta vez el carácter / que termina el nuevo URL;

El archivo [output/serge.tahe.com.HTML] es entonces el siguiente:


<!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 mostrado que nuestro cliente HTTP era insuficiente. Ahora vamos a presentar una herramienta llamada [curl] que permite recuperar documentos web gestionando las dificultades mencionadas: protocolo https, documento enviado por partes, redireccionamientos… La herramienta [curl] se ha instalado con Laragon:

Image

Abramos un terminal Laragon [1]:

Image

En el terminal, escribimos el siguiente comando:

Image

  • en [1], el tipo de consola;
  • en [2], la carpeta actual. Esta carpeta es especial: es donde el servidor Apache de Laragon busca los documentos que se le solicitan. Por lo tanto, evitaremos contaminar esta carpeta;
  • en [3], el comando introducido;

Es posible que el comando [curl --help] genere un error. La causa más probable es que no tenga el tipo de terminal adecuado. En ese caso, abra otro terminal con los comandos [4-6];

El comando [curl --help] muestra todas las opciones de configuración de [curl]. Hay varias decenas. Utilizaremos muy pocas. 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:

Image

  • en [1], nos desplazamos a la carpeta [c:\temp]. Si esta carpeta no existe, puede crearla o elegir otra;
  • en [2], creamos una carpeta llamada [curl];
  • en [3], nos situamos sobre ella;
  • en [4], se muestra su contenido. Está vacío;

Asegúrese de que el servidor Apache de Laragon está en marcha y, con [curl], solicite 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*   Intentando ::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 al host localhost sin cambios                                                   
  • líneas 8-12: líneas enviadas por [curl] al servidor [localhost]. Se reconoce el protocolo HTTP;
  • líneas 13-19: líneas enviadas en respuesta por el servidor;
  • línea 13: indica que se ha recibido correctamente el documento solicitado;

El archivo [localhost.HTML] contiene el documento solicitado. Puede comprobarlo abriendo el archivo en un editor de texto.

Ahora solicitemos el URL y el [https://tahe.developpez.com:443/]. Para obtener este URL, el cliente HTTP debe saber hablar HTTPS. Este es el caso del cliente [curl].

Los resultados de la consola son los siguientes:


c:\Temp\curl
λ curl --verboso --salida 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*   Intentando 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 ha dejado intacto
  • líneas 10-40: los intercambios entre el cliente y el servidor para asegurar la conexión: esta se cifrará;
  • 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] gestiona 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].

Solicitemos ahora 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 / al final).

Los resultados de la consola son los siguientes:


c:\Temp\curl
λ curl --verboso --salida 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*   Intentando 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-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 al host sergetahe.com se ha dejado 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 sin modificaciones
  • línea 2: se utiliza option [--location] para indicar que se quieren 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 tiene 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 apartado enlace:

Image

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:

{
    "sergetahe.com": {
        "timeout": 5,
        "url": "http://sergetahe.com"
    },
    "tahe.developpez.com": {
        "timeout": 5,
        "url": "https://tahe.developpez.com"
    },  
    "www.polytech-angers.fr": {
        "timeout": 5,
        "url": "http://www.polytech-angers.fr"
    },  
    "localhost": {
        "timeout": 5,
        "url": "http://localhost"
    }
}

Cada elemento del diccionario [clé, valeur] tiene la siguiente estructura:

  • clave: el nombre de un servidor web;
  • valor: un diccionario con las siguientes claves:
    • timeout: tiempo máximo de espera de 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);
//
// gestión 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 del resultado
  print "$résultat\n";
}//para
// fin
exit;

//-----------------------------------------------------------------------
function getUrl(string $site, string $url, int $timeout, $suivi = TRUE): string {
  // lee el URL $url y lo almacena 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 ha producido 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,
    // recuperación del documento solicitado en forma de 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);

  // procesamiento del resultado
  if ($page_content !== FALSE) {
    // Almacenamiento del resultado en $site.HTML
    $result = file_put_contents("output/$site.HTML", $page_content);
    if ($result === FALSE) {
      // retorno de error
      return "Erreur lors de la création du fichier [output/$site.HTML]";
    }
    // respuesta correcta
    return "Fin de la communication avec le serveur [$site]. Vérifiez le fichier [output/$site.HTML]";
  } else {
    // se ha producido 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 será un 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 option [CURLOPT_RETURNTRANSFER => true], la función [curl_exec] devuelve como resultado el documento enviado por el servidor en forma de cadena de caracteres. La función [curl_exec] devuelve el valor booleano FALSE en caso de fallo de 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 alojar sergetahe.com sin modificar
* 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 sin cambios
* 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 alojar sergetahe.com sin modificaciones
* 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 deja 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/fr/index.HTML
< Cache-Control: max-age=1
< Expires: Sat, 18 May 2019 08:46:46 GMT
< Content-Length: 339
< Content-Type: text/HTML; charset=iso-8859-1
<
* Ignoring the response-body
* Connection #0 para alojar www.polytech-angers.fr sin modificaciones
* Issue another request to this URL: 'http://www.polytech-angers.fr/fr/index.HTML'
* Found bundle for host www.polytech-angers.fr: 0x1fee4ebe390 [can pipeline]
* Re-using existing connection! (#0) 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 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 apartado, hemos descubierto el protocolo HTTP y hemos escrito un script [http-02.php] capaz de descargar un URL de la web.

16.5. El protocolo SMTP (Protocolo simple de transferencia de correo)

16.5.1. Introducción

Image

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], lo que permite 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, vamos a crear una dirección en Gmail:

Image

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

Image

  • rellene los campos [9-10] y, a continuación, valide (11);
  • acepte las condiciones de uso de Google (12-13) y, a continuación, valide (14);

Image

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

Image

  • en [21], autorice a otras aplicaciones distintas de las de Google a utilizar la cuenta [php7parlexemple]. Si no lo hacemos, nuestro servidor de correo local [hMailServer] no podrá comunicarse con el servidor SMTP de Gmail;

Image

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, un servidor POP3 (Post Office Protocol) que permite leer los correos almacenados en el servidor, y un servidor IMAP (Internet Message Access Protocol) que también permite leer los correos almacenados en el servidor, pero va más allá. En particular, permite gestionar el almacenamiento de los correos en el servidor.

El servidor de correo [hMailServer] está disponible en URL [https://www.hmailserver.com/] (mayo de 2019).

Image

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

Image

  • 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 arrancar el equipo. Es preferible elegir un inicio manual:

  • en [3], escriba [services] en el campo de entrada de la barra de estado;

Image

  • en [4-8], se pone el servicio en modo [manuel] (6) y se inicia (7);

Una vez iniciado, hay que configurar el servidor [hMailServer]. El servidor se ha instalado con un programa de administración [hMailServer Administrator]:

Image

  • en [2], en el campo de entrada de la barra de estado, escriba [hmailserver];
  • en [3], inicie el administrador;
  • en [4], conecte el administrador al servidor [hMailServer];
  • en [5], escriba la contraseña introducida durante la instalación de [hMailServer];

Image

Vamos a crear una cuenta de usuario:

  • haga clic con el botón derecho en [Accounts] (7) y luego en (8) para añadir 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;

Image

Image

  • 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 electrónicos destinados al servidor local (localhost);
  • en [18], el nombre de la máquina local (localhost). El script del párrafo enlace le 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 destinados a la máquina local (localhost);
  • en [20], el servidor SMTP de Gmail. Utilizamos Gmail porque hemos creado una cuenta allí en el apartado 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 enlace;
  • en [24], la contraseña de este usuario: [PHP7parlexemple], creada en el párrafo enlace;
  • en [25], se indica el tipo de protocolo de seguridad utilizado por Gmail;

Image

  • en [27], el puerto del servicio SMTP;
  • en [28], este servicio no requiere autenticación;
  • en [30], introduzca el mensaje de bienvenida que el servidor SMTP enviará a sus clients;

16.5.4. El protocolo SMTP

Image

Vamos a descubrir 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 solicitará al servidor B que distribuya un correo al usuario [php7parlexemple@gmail.com];
  • comprobaremos que dicho usuario ha recibido correctamente el correo enviado;

Iniciamos el cliente de la siguiente manera:

Image

  • 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 finalizar el programa es [quit]. Sin embargo, [quit] es también 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 introducidos mediante el teclado;
  • en [4], el servidor le envía su mensaje de bienvenida;

Image

  • 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 ha ejecutado 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] 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 terminada por 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], los encabezados anteriores deben ir seguidos de una línea en blanco;
  • en [15], el texto del mensaje;
  • en [16], la línea que contiene un único punto que 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 cola;
  • en [18], el cliente 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 comprobemos que el usuario [php7parlexemple@gmail.com] ha recibido correctamente el mensaje:

Image

  • en [2], vemos que el usuario [php7parlexemple@gmail.com] ha recibido correctamente el mensaje;

Image

Image

Image

  • en [7], el remitente del correo. Se ve 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á proceder del usuario [php7parlexemple@gmail.com]. Esto no es lo que queríamos aquí, 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 se ha 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] ha conseguido enviar el mensaje aunque se haya producido 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 reproducir en PHP lo que hemos aprendido anteriormente del protocolo SMTP.

Image

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 matriz 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 va a 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 el servidor SMTP [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 SMTP cliente-servidor
// -> 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 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 que comienzan todas por 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 del 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";
}//for
// fin
exit;

//sendmail
//-----------------------------------------------------------------------

function sendmail(string $name, array $infos, bool $verbose = TRUE): string {
  // envía 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"]);
  // 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 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);
    // retorno
    return $erreur;
  }
  // comando EHLO
  $erreur = sendCommand($connexion, "EHLO $client", $verbose, TRUE);
  if ($erreur !== "") {
    // cierre de la conexión
    fclose($connexion);
    // retorno
    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);
    // volver
    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);
    // retorno
    return $erreur;
  }
  // comando DATA  
  $erreur = sendCommand($connexion, "DATA", $verbose, TRUE);
  if ($erreur !== "") {
    // cierre de la conexión
    fclose($connexion);
    // retorno
    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);
    // retorno
    return $erreur;
  }
  // comando de salida
  $erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
  if ($erreur !== "") {
    // cierre de la conexión
    fclose($connexion);
    // retorno
    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 verboso si $verbose=1
  // si $withRCLF=1, añade 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 eco
    if ($verbose) {
      affiche($commande, 1);
    }
  }//si
  // lectura de respuesta
  $réponse = fgets($connexion, 1000);
  // posible eco
  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);
    }
  }//while
  // respuesta completada
  // ¿error devuelto por el servidor?
  if ($codeErreur >= 500) {
    return substr($réponse, 4);
  }
// retorno sin error
  return "";
}

// --------------------------------------------------------------------------

function affiche($échange, $sens) {
  // muestra $échange en 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;
  }//switch
}

Comentarios

  • línea 39: se utiliza el archivo de configuración;
  • línea 42: se recorre los elementos de la matriz [mails]. Cada elemento es un diccionario [name=>infos], donde [name] es un nombre que puede ser cualquiera y [infos] 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 dado 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 cliente/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 diferentes 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 comando 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 va a 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 sirve para enviar el mensaje. En este caso no se añade 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 superior a 500, el servidor ha devuelto un error;

Resultados

La ejecución del script da 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.]
--> [De: 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.]
--> [De: 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 del servidor SMTP [hMailServer] para enviar un correo electrónico a [guest@localhost] se realiza correctamente;
  • líneas 27-52: el uso del servidor SMTP [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, es rechazado;

16.5.6. Un segundo cliente SMTP escrito con 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 modo de instalación de [SwiftMailer] se describe en URL [https://swiftmailer.symfony.com/docs/introduction.HTML] (mayo de 2019).

En primer lugar, ejecute Laragon:

Image

  • en [1], abra un terminal;

Image

  • en [3], compruebe que se encuentra en la carpeta [<laragon>/www], donde <laragon> es la carpeta de instalación de Laragon;
  • en [3], escriba el comando indicado (mayo de 2019). Compruebe en URL [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 este equipo;
  • en [5], la carpeta de instalación de [swiftmailer] [6];
  • en [7], un archivo que necesitaremos en nuestro script;

Una vez hecho esto, compruebe que la carpeta [<laragon>/www/vendor] [5] se encuentra 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, compruebe el archivo [php.ini] (véase el apartado del enlace):

Image

El script [smtp-02.php] utilizará el siguiente archivo de configuración jSON [config-smtp-02.json]:

{
    "mail to localhost via localhost": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "test-localhost",
        "message": "ligne 1\nligne 2\nligne 3",
        "tls": "FALSE",
        "attachments": ["/attachments/Hello from SwiftMailer.docx",
            "/attachments/Hello from SwiftMailer.pdf",
            "/attachments/Hello from SwiftMailer.odt",
            "/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
            "/attachments/test-localhost.eml"
        ]
    },
    "mail to gmail via gmail": {
        "smtp-server": "smtp.gmail.com",
        "smtp-port": "587",
        "from": "php7parlexemple@gmail.com",
        "to": "php7parlexemple@gmail.com",
        "subject": "test-gmail-via-gmail",
        "message": "ligne 1\nligne 2\nligne 3",
        "tls": "TRUE",
        "user": "php7parlexemple@gmail.com",
        "password": "PHP7parlexemple",
        "attachments": ["/attachments/Hello from SwiftMailer.docx",
            "/attachments/Hello from SwiftMailer.pdf",
            "/attachments/Hello from SwiftMailer.odt",
            "/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
            "/attachments/test-localhost.eml"
        ]
    },
    "mail to gmail via localhost": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "php7parlexemple@gmail.com",
        "subject": "test-gmail-via-localhost",
        "message": "ligne 1\nligne 2\nligne 3",
        "tls": "FALSE",
        "attachments": ["/attachments/Hello from SwiftMailer.docx",
            "/attachments/Hello from SwiftMailer.pdf",
            "/attachments/Hello from SwiftMailer.odt",
            "/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
            "/attachments/test-localhost.eml"
        ]
    }
}

Encontramos los mismos campos que en el archivo [config-smtp-01.json], con dos campos adicionales:

  • [tls]: en TRUE indica que hay que utilizar una conexión segura con el servidor SMTP. En caso de que [tls] sea igual a TRUE, hay que añadir dos campos:
    • [user]: el nombre de usuario que autentica la conexión;
    • [password]: su contraseña;

En nuestro ejemplo, hemos utilizado las credenciales del usuario [php7parlexemple@gmail.com] para conectarnos al servidor de Gmail. Utilice las suyas;

  • [attachments]: proporciona los nombres de los archivos que se deben adjuntar al correo electrónico;

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 de envío del 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";
}//for
// 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 comprueba que el archivo existe
      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 {
      // sin TLS
      $transport = (new \Swift_SmtpTransport($infos["smtp-server"], $infos["smtp-port"]));
    }
    // el gestor 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 el primer uso de dichas clases. Nos evita tener que incluir tantos [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 lectores 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 viene definido por la clase [\Swift_SmtpTransport]. Hay que proporcionar al menos dos datos: el nombre y el puerto del servidor SMTP. También hay una tercera: ¿impone el servidor SMTP 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: es la clase [\SwiftMailer] 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] elegido. El método [SwiftMailer→send] devuelve el valor booleano FALSE si no se ha podido enviar el mensaje;
  • líneas 86-89: la biblioteca [SwiftMailer] lanza una excepción en cuanto algo no va bien;

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:

1
2
3
4
5
6
Envoi du mail [mail to localhost via localhost]
Message [mail to localhost via localhost] envoyé
Envoi du mail [mail to gmail via gmail]
Message [mail to gmail via gmail] envoyé
Envoi du mail [mail to gmail via localhost]
Message [mail to gmail via localhost] envoyé

Si consultamos la cuenta de Gmail del usuario [php7parlexemple], vemos lo siguiente:

Image

  • 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 etiquetas [--_=_swift_xx];
  • líneas 19-24: el mensaje en texto sin formato;
  • líneas 27-30: el mensaje en 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 (Post Office Protocol) y IMAP (Internet Message Access Protocol)

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), más reciente que el POP3 y el más utilizado en la actualidad;

Para descubrir el protocolo POP3, utilizaremos la siguiente arquitectura:

Image

  • [Serveur B] será un servidor POP3 / IMAP local, 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, lo que permiteimplementar tanto clients como IMAP y POP3;

16.6.2. Descubrimiento del protocolo POP3

En primer lugar, utilizamos el script [smtp-01.php] para enviar un correo electrónico al usuario [guest@localhost]. Si ha realizado las pruebas asociadas al script, este usuario debería haber recibido los correos, pero no hemos podido verificarlo. Para enviarle un nuevo correo, utilice, por ejemplo, el siguiente archivo de configuración [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"
    }
}

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 --salir adiós 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 trabajar 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] indica al servidor que se ha terminado;

La respuesta del servidor puede adoptar varias formas:

  • una sola línea que comience por [+OK] para indicar que el pedido anterior del cliente se ha completado con éxito;
  • una línea única que comienza por [-ERR] para indicar que el pedido anterior del cliente ha fallado;
  • varias líneas en las que:
    • la primera línea comienza por [+OK];
    • la última línea está formada por un único punto;

16.6.3. Un script básico que implementa el protocolo POP3

Image

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]. Tendrá el siguiente archivo de configuración [config-pop3-01.json]:

1
2
3
4
5
6
7
8
9
{
    "localhost:110": {
        "server": "localhost",
        "port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "maxmails":5
    }
}
  • 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 Post Office) que permite leer mensajes de un buzón
// protocolo de    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 OK o no
// -> el cliente envía el comando RETR n.º para cada uno de los correos
// <- el servidor responde OK o no. Si es 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 electrónico
// <- 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 está formada por un único 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 del 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";
}//for
// 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 tras su lectura
  // Si $verbose=1, realiza un seguimiento de los intercambios entre el cliente y el servidor
  //
  // apertura de 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);
    // retorno
    return $erreur;
  }
  // comando USER
  $erreur = sendCommand($connexion, "USER {$infos["user"]}", $verbose, TRUE);
  if ($erreur !== "") {
    // cierre de la conexión
    fclose($connexion);
    // retorno
    return $erreur;
  }
  // comando PASS
  $erreur = sendCommand($connexion, "PASS {$infos["password"]}", $verbose, TRUE);
  if ($erreur !== "") {
    // cierre de la conexión
    fclose($connexion);
    // retorno
    return $erreur;
  }
  // comando LIST
  $premièreLigne = "";
  $erreur = sendCommand($connexion, "LIST", $verbose, TRUE, $premièreLigne);
  if ($erreur !== "") {
    // cierre de la conexión
    fclose($connexion);
    // retorno
    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];
  // se recorre el bucle de 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);
      // retorno
      return $erreur;
    }
    // comando DELE
    $erreur = sendCommand($connexion, "DELE " . ($iMessage + 1), $verbose, TRUE);
    if ($erreur !== "") {
      // cierre de la conexión
      fclose($connexion);
      // volver
      return $erreur;
    }
    // mensaje siguiente
    $iMessage++;
  }
  // comando QUIT
  $erreur = sendCommand($connexion, "QUIT", $verbose, TRUE);
  if ($erreur !== "") {
    // cierre de la conexión
    fclose($connexion);
    // volver
    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 verboso si $verbose=1
  // si $withRCLF=1, añade 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 eco
    if ($verbose) {
      affiche($commande, 1);
    }
  }//si
  // lectura de 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 ha producido 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);
    }//while
  }
  // retorno sin error
  return "";
}

// --------------------------------------------------------------------------

function affiche($échange, $sens) {
  // muestra $échange en 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;
  }//switch
}

Comentarios

Como hemos dicho, [pop3-01.php] es una adaptación del script [smtp-01.php] que ya hemos comentado. Solo comentaremos las principales diferencias:

  • 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 introducir la contraseña de dicho 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 añade 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 eliminarlo una vez leído (líneas 118-125);
  • líneas 129-136: se envía el comando [QUIT] para indicar al servidor que se ha terminado;
  • líneas 178-194: para los comandos [LIST] y [RETR], la respuesta del servidor tiene varias líneas, siendo la última un único punto;

Resultados

Al ejecutarlo, 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:

  1. la posibilidad de comunicarse con un servidor POP3 seguro;
  2. la posibilidad 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 comprobar que las funciones [imap] estén disponibles en version de PHP, que es el que utilizamos. Abrimos el archivo [php.ini] descrito en el apartado «enlace» y buscamos las líneas que hacen referencia a [imap]:

Image

Línea 895, compruebe que la extensión [imap] esté activada.

El script [imap-01.php] utilizará el siguiente archivo jSON [config-imap-01.json]:

{

    "{imap.gmail.com:993/imap/ssl/novalidate-cert}INBOX": {
        "imap-server": "imap.gmail.com",
        "imap-port": "993",
        "user": "php7parlexemple@gmail.com",
        "password": "PHP7parlexemple",
        "output-dir": "output/gmail-imap",
        "prefix": "message-"
    },
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3",
        "prefix": "message-"
    }
}

El archivo [config-imap-01.json] define una tabla de servidores IMAP / POP3 con los que 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]: designa el 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 solicita al cliente que no verifique el certificado de seguridad que el servidor le va a enviar. Por último, un servidor IMAP gestiona 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 es normalmente aquel al que llegan los nuevos mensajes;
    • [{localhost:110/pop3}INBOX]: designa el servidor [localhost] que escucha en el puerto 110. El protocolo cliente/servidor es aquí 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, tras 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");
//
//
// 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 de la respuesta de la función imap_open()
  if (!$imapResource) {
    // Error
    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) {
    // Error
    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: apertura de 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 propietario de este buzón;
    • el tercer parámetro es su contraseña;

La función [imap_open] se encarga de la seguridad de 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 unos veinte criterios. La función [imap_search] devuelve una matriz de números de mensajes. Estos pueden tener dos formas: número de secuencia o identificador UID del mensaje. Por defecto, la función [imap_search] devuelve una matriz de números de secuencia. Si se añade 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 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 es normalmente un número de secuencia de 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 ha podido 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: tras los encabezados, ahora se solicita el cuerpo del mensaje con la función [imap_body]. Esta función devuelve NULL si no ha podido 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 era POP3, se elimina el mensaje que se acaba de leer:
    • 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:


------------Lectura del buzón [{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.
------------Lectura del buzón [{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:


------------Lectura del buzón [{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.
------------Lectura del buzón [{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 ha cambiado el estado de los mensajes leídos de «sin leer» a «leído»;
  • línea 9: ya no hay mensajes en el buzón 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:

Image

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 y 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 diferentes tipos de mensajes como el contenido codificado de los distintos archivos adjuntos. Es posible obtener la estructura del mensaje para conocer estas diferentes 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]:

{
    "mail to localhost via localhost": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "test-localhost",
        "message": "ligne 1\nligne 2\nligne 3",
        "tls": "FALSE",
        "attachments": [
            "/attachments/Hello from SwiftMailer.docx",
            "/attachments/Hello from SwiftMailer.pdf",
            "/attachments/Hello from SwiftMailer.odt",
            "/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
            "/attachments/test-localhost.eml"
        ]
    }
}

Se trata, por tanto, 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]:

{
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3"
    }
}

Por lo tanto, se aprovecha el buzón de [guest@localhost] (línea 5). A continuación, el script [imap-02.php] muestra 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:

Image

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

Image

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

Image

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 ningún campo [id] en la estructura;
  • línea 10: [ifdisposition]=0 indica que no hay ningún campo [disposition] en la estructura;
  • línea 11: [ifdparameters]=0 indica que no hay ningún 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:
boundary="_=_swift_1558872295_5bc8ee2ca8b3723c0b39ca8bbfbebdeb_=_"

Estas líneas sirven para delimitar el mensaje. En el mensaje recuperado por [imap-01.php], la parte del mensaje que se acaba 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 enviadas en momentos diferentes;

  • línea 23: aquí comienza la estructura de las diferentes partes del mensaje;
  • líneas 25-45: esta primera parte es de 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:

1
2
3
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"

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 de tipo [multipart/alternative];
  • líneas 3-6: esta segunda parte es de 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 añadido en la función [imap_fetchstructure];

Las líneas 1-36 se repiten para cada uno de los cinco archivos adjuntos del mensaje.

La función [imap_fetch_structure] nos permite, por tanto, obtener la estructura de un mensaje. Esta 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.º 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 queremos 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: es posible que 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 da los siguientes resultados en la consola:


------------Lectura del buzón [{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}]
-----contenido de la parte n.º [0]
Content-Type: MULTIPART/MIXED
Transfer-Encoding : 7 bits
-----contenido de la parte n.º [1]
Content-Type: MULTIPART/ALTERNATIVE
Transfer-Encoding : 7 bits
-----contenido de la parte n.º [1.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : quoted-printable
-----contenido de la parte n.º [1.2]
Content-Type: TEXT/HTML
Transfer-Encoding : quoted-printable
-----contenido de la parte n.º [2]
Content-Type: APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
Transfer-Encoding : base 64
-----contenido de la parte n.º [3]
Content-Type: APPLICATION/PDF
Transfer-Encoding : base 64
-----contenido de la parte n.º [4]
Content-Type: APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT
Transfer-Encoding : base 64
-----contenido de la parte n.º [5]
Content-Type: UNKNOWN/PNG
Transfer-Encoding : base 64
-----contenido de la parte n.º [6]
Content-Type: MESSAGE/RFC822
Transfer-Encoding : base 64
-----contenido de la parte n.º [6.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : 7 bits
Fermeture de la connexion réussie.

Conseguimos 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. Se trata del 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 x.1, x.2… donde x es el n.º 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 [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.º 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: se muestra entonces 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 empieza 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.º 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 archivos adjuntos;

La ejecución de este script da los siguientes resultados:


------------Lectura del buzón [{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}]
-----contenido de la parte n.º [0]
Content-Type: MULTIPART/MIXED
Transfer-Encoding : 7 bits
-----contenido de la parte n.º [1]
Content-Type: MULTIPART/ALTERNATIVE
Transfer-Encoding : 7 bits
-----contenido de la parte n.º [1.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : quoted-printable
ligne 1
ligne 2
ligne 3
-----contenido de la parte n.º [1.2]
Content-Type: TEXT/HTML
Transfer-Encoding : quoted-printable
<b>ligne 1<br/>ligne 2<br/>ligne 3</b>
-----contenido de la parte n.º [2]
Content-Type: APPLICATION/VND.OPENXMLFORMATS-OFFICEDOCUMENT.WORDPROCESSINGML.DOCUMENT
Transfer-Encoding : base 64
UEsDBBQABgAIAAAAIQDfpNJsWgEAACAFAAATAAgCW0NvbnRlbnRfVHlwZXNdLnhtbCCiBAIooAAC
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

AAAAAAAAAF0mAABkb2NQcm9wcy9jb3JlLnhtbFBLAQItABQABgAIAAAAIQCdxkmwcgEAAMcCAAAQ
AAAAAAAAAAAAAAAAAAgpAABkb2NQcm9wcy9hcHAueG1sUEsFBgAAAAALAAsAwQIAALArAAAAAA==
-----contenido de la parte n.º [3]
Content-Type: APPLICATION/PDF
Transfer-Encoding : base 64
JVBERi0xLjUKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURl
Y29kZT4+CnN0cmVhbQp4nHWNvQoCMRCE+zzF1sLF2WSTSyAEPD0Lu4OAhdj5AxaC1/j6Rk4s5GSa

PDcxQUJGQ0JGQURGODYxM0NBNUJDODNFMDNDNjI1QkQwPgo8NzFBQkZDQkZBREY4NjEzQ0E1QkM4
M0UwM0M2MjVCRDA+IF0KL0RvY0NoZWNrc3VtIC9DMTRCN0Q5N0YwNUU1OTYxQzhDODg0NEI3NkNF
OEIwRQo+PgpzdGFydHhyZWYKMTIzMTQKJSVFT0YK
-----contenido de la parte n.º [4]
Content-Type: APPLICATION/VND.OASIS.OPENDOCUMENT.TEXT
Transfer-Encoding : base 64
UEsDBBQAAAgAAAs9uU5exjIMJwAAACcAAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlvbi92bmQub2Fz
aXMub3BlbmRvY3VtZW50LnRleHRQSwMEFAAACAAACz25TgAAAAAAAAAAAAAAABwAAABDb25maWd1

AQIUABQACAgIAAs9uU42l0SORAQAABIRAAALAAAAAAAAAAAAAAAAAI8bAABjb250ZW50LnhtbFBL
AQIUABQACAgIAAs9uU4Uf52+LgEAACUEAAAVAAAAAAAAAAAAAAAAAAwgAABNRVRBLUlORi9tYW5p
ZmVzdC54bWxQSwUGAAAAABEAEQBlBAAAfSEAAAAA
-----contenido de la parte n.º [5]
Content-Type: UNKNOWN/PNG
Transfer-Encoding : base 64
iVBORw0KGgoAAAANSUhEUgAABiAAAAEMCAYAAABN1n5OAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAg
AElEQVR4nOy9e5TdV3Xn+Zm7aqprlBq1Rq1Wq7XU6opGrXaMMI6jAcfj9ihu4hAehkAghBASICF0

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAA2Mb8f9Q5r2ohJn6/AAAAAElFTkSuQmCC
-----contenido de la parte 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 Word codificado en [base64] (línea 23);
  • líneas 32-37: el contenido del archivo PDF codificado en [base64] (línea 31);
  • líneas 41-45: el contenido del archivo OpenOffice codificado en [base64] (línea 40);
  • líneas 50-55: el contenido del archivo de imagen codificado en [base64] (línea 49);
  • líneas 59-63: el contenido del correo adjunto codificado en [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.º 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
    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: se guarda entonces 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 se sustituye ahora 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 guardado
  $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 de la carpeta de guardado. Esta lleva el n.º del mensaje cuyas partes se analizan;
  • línea 10: la parte del mensaje que se va a guardar se define de forma ú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 trata el caso de las partes cuyo tipo principal no es [text];
  • línea 35: solo se tienen 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 electrónico;
  • guarda los diferentes archivos adjuntos mediante la función [saveAttachment];

La función [saveText] es la siguiente:


// guardado del 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 descodificamos
  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:
      // texto sin formato
      $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 descodificarlo, 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 hay que 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 construye 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
  // este 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 de 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 descodificamos
      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 {
      // error 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 forma única la parte IMAP que se va a guardar;
    • [string $outputDir] es la carpeta de guardado;
    • [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á este 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 ha cumplido la condición de la línea 16. A continuación, 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 admite aquí 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 ha encontrado;

Para probar el script [imap-02.php], primero enviamos un correo electrónico a [guest@localhost] con la siguiente configuración:

{
    "mail to localhost via localhost": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "test-localhost",
        "message": "ligne 1\nligne 2\nligne 3",
        "tls": "FALSE",
        "attachments": [
            "/attachments/Hello from SwiftMailer.docx",
            "/attachments/Hello from SwiftMailer.pdf",
            "/attachments/Hello from SwiftMailer.odt",
            "/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
            "/attachments/test-localhost.eml"
        ]
    }
}

Por lo tanto, hay cinco archivos adjuntos.

Leemos el correo enviado con [imap-02.php] y la siguiente configuración:

{
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3"
    }
}

Los resultados de la consola son los siguientes:


------------Lectura del buzón [{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}]
-----contenido de la parte n.º [0]
Content-Type: MULTIPART/MIXED
Transfer-Encoding : 7 bits
-----contenido de la parte n.º [1]
Content-Type: MULTIPART/ALTERNATIVE
Transfer-Encoding : 7 bits
-----contenido de la parte n.º [1.1]
Content-Type: TEXT/PLAIN
Transfer-Encoding : quoted-printable
sauvegarde d'un message dans [output/localhost-pop3/message-1/message.txt]
-----contenido de la parte n.º [1.2]
Content-Type: TEXT/HTML
Transfer-Encoding : quoted-printable
sauvegarde d'un message dans [output/localhost-pop3/message-1/message.HTML]
-----contenido de la parte 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]
-----contenido de la parte n.º [3]
Content-Type: APPLICATION/PDF
Transfer-Encoding : base 64
sauvegarde de l'attachement dans [output/localhost-pop3/message-1/Hello from SwiftMailer.pdf]
-----contenido de la parte 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]
-----contenido de la parte 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]
-----contenido de la parte 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]:

Image

16.6.6. Cliente POP3 / IMAP con la biblioteca [php-mime-mail-parser]

En el script anterior [imap-02.php], hemos podido 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 hemos guardado el contenido del archivo adjunto. Ahora bien, este tipo de archivo adjunto es en sí mismo un correo electrónico que, a su vez, tiene contenidos [text/plain] y [text/HTML], así como archivos adjuntos. Por lo tanto, podemos encontrarnos en la siguiente situación:

  • un [mail 1] cuya estructura es análoga a la de un archivo adjunto de tipo [message/rfc822];
  • un [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 ha hecho con [mail 1]. Una llamada recursiva al método que procesaba [mail 1] podría entonces bastar para obtener el contenido de todos los correos anidados unos dentro de otros. Por desgracia, 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 utilice una lógica bastante compleja para calcular los números de las partes de un correo, 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 a la hora de gestionar los contenidos de los correos anidados, vamos a utilizar 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:

Image

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

Image

  • en [2], elija la version más reciente y estable de la biblioteca;

Image

  • en [3], elija el version del PHP que esté utilizando (en este documento es el PHP 7.2);
  • en [4], elija version de su OS de Windows (aquí es un Windows de 64 bits). Tomamos el version [Thread Safe];

Para conocer el version del PHP descargado con Laragon, abra un [Terminal] desde la ventana de Laragon y escriba 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                      

El version de PHP 7.2.11 aparece en la línea 3. La misma línea indica el version de Windows utilizado para la compilación (32 o 74 bits).

Una vez obtenido el DLL, hay que copiarlo en la carpeta [<laragon>/bin/php/<version-php>/ext] [5]:

Image

Una vez hecho esto, hay que activar esta extensión en el archivo [php.ini], que configura PHP (véase el apartado «enlace»):

Image

Es probable que la línea [7] no exista y que tengas que añadirla tú mismo.

Una vez activada la extensión, se puede comprobar su validez escribiendo 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 errónea, se señalará. De este modo, se verificará la validez del DLL añadido al [php_mailparse.dll]. Puede declararse incorrecto por diversas razones, entre las que destacan las siguientes:

  • ha descargado un DLL que no se corresponde con el version de PHP utilizado;
  • ha descargado un archivo DLL de 32 bits cuando tiene un PHP de 64 bits, o viceversa;

Una vez activada y verificada la extensión, se puede pasar a la instalación de la biblioteca [php-mime-mail-parser]:

Image

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

Image

  • en [1], compruebe que se encuentra en la carpeta [<laragon>/www];
  • en [2], el comando de instalación de la biblioteca [php-mime-mail-parser];
  • en [3], aquí no se ha instalado nada 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]:

Image

Image

  • en [2-3], las fuentes 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:

{
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3"
    }
}

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);
// gestión 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 del 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 determinado;

La función [readmailbox] es la siguiente:


function readmailbox(string $name, array $infos): void {
  // se establece la conexió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 tras 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) {
    // error
    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] admite 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 ha podido recuperar el contenido del mensaje;
  • línea 10: se empieza 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 solicita a la función [getParts] que localice 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.º 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 forma recursiva
      getParts($parser, $msgNumber, $outputDir);
    }
  }
}

Comentarios

  • línea 2: la función [getParts] admite tres parámetros:
    • un analizador [$parser] al que se ha transmitido el texto completo del mensaje que se va a analizar;
    • [$msgNumber] es el número del mensaje que se está analizando;
    • [$outputDir] es la carpeta en la que deben guardarse 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 (remitente, destinatario, asunto…);
  • línea 13: se recuperan las partes del correo electrónico con el tipo [text/plain]. Se recupera una tabla;
  • líneas 14-17: se guardan todos los elementos de la matriz 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 de la matriz recuperada, 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 de 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 comprueba si el archivo adjunto es un correo electrónico;
  • líneas 41-48: si es así, se analiza a su vez ese correo electrónico 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 forma recursiva para analizar el nuevo texto;

La función [saveMessage] guarda el contenido de texto del mensaje que se va a analizar:


// guardado de un mensaje de texto
function saveMessage(string $text, int $type, array $arrayHeaders, string $filename): void {
  // contenido que se va a guardar
  $contents = "";
  // se añaden 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";
  }
  // adición del texto del mensaje
  $contents .= $text;
  // guardar todo
  if (!file_put_contents($filename, $contents)) {
    // error
    print "Le message n'a pu être sauvegardé dans le fichier [$filename]\n";
  } else {
    // correcto
    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]: los encabezados del mensaje analizado;
    • [$filename]: el nombre del archivo en el que se debe guardar [$text];
  • línea 4: [$contents] representará la totalidad del 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, se termina cada línea con la etiqueta <br/> para que cada encabezado aparezca solo en su línea en un navegador;
  • línea 22: se añade 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 de lectura de 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:

{
    "mail to localhost via localhost": {
        "smtp-server": "localhost",
        "smtp-port": "25",
        "from": "guest@localhost",
        "to": "guest@localhost",
        "subject": "test-localhost",
        "message": "ligne 1\nligne 2\nligne 3",
        "tls": "FALSE",
        "attachments": [
            "/attachments/Hello from SwiftMailer.docx",
            "/attachments/Hello from SwiftMailer.pdf",
            "/attachments/Hello from SwiftMailer.odt",
            "/attachments/Cours-Tutoriels-Serge-Tahé-1568x268.png",
            "/attachments/test-localhost-2.eml"
        ]
    }
}
  • 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 electrónico 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:

{
    "{localhost:110/pop3}": {
        "imap-server": "localhost",
        "imap-port": "110",
        "user": "guest@localhost",
        "password": "guest",
        "pop3": "TRUE",
        "output-dir": "output/localhost-pop3"
    }
}

Tras la ejecución, el árbol de carpetas de [output/localhost-pop3] quedó de la siguiente manera:

Image

  • en [1], los 5 archivos adjuntos del correo electrónico 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 [test-localhost.eml] de [2];

Los mensajes de la consola son los siguientes:


------------Lectura del buzón [{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}]
---mensaje n.º [1]
-- Guardar un mensaje de tipo [text/plain]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/message_1.txt]
-- Guardar un mensaje de tipo [text/html]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/message_1.html]
-- Guardarun archivo adjunto de tipo [application/vnd.openxmlformats-officedocument.wordprocessingml.document] en el archivo [output/localhost-pop3/message-1/Hello from SwiftMailer.docx]
-- Guardarun archivo adjunto de tipo [application/pdf] en el archivo [output/localhost-pop3/message-1/Hello from SwiftMailer.pdf]
-- Guardarun archivo adjunto de tipo [application/vnd.oasis.opendocument.text] en el archivo [output/localhost-pop3/message-1/Hello from SwiftMailer.odt]
-- Guardarun archivo adjunto de tipo [image/png] en el archivo [output/localhost-pop3/message-1/Cours-Tutoriels-Serge-Tahé-1568x268.png]
-- Guardarun archivo adjunto de tipo [message/rfc822] en el archivo [output/localhost-pop3/message-1/test-localhost-2.eml]
-- Guardar un mensaje de tipo [text/plain]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/message_1.txt]
-- Guardar un mensaje de tipo [text/html]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/message_1.html]
-- Guardarun archivo adjunto de tipo [application/vnd.openxmlformats-officedocument.wordprocessingml.document] en el archivo [output/localhost-pop3/message-1/rfc822-1/Hello from SwiftMailer.docx]
-- Guardarun archivo adjunto de tipo [application/pdf] en el archivo [output/localhost-pop3/message-1/rfc822-1/Hello from SwiftMailer.pdf]
-- Guardarun archivo adjunto de tipo [application/vnd.oasis.opendocument.text] en el archivo [output/localhost-pop3/message-1/rfc822-1/Hello from SwiftMailer.odt]
-- Guardarun archivo adjunto de tipo [image/png] en el archivo [output/localhost-pop3/message-1/rfc822-1/Cours-Tutoriels-Serge-Tahé-1568x268.png]
-- Guardarun archivo adjunto de tipo [message/rfc822] en el archivo [output/localhost-pop3/message-1/rfc822-1/test-localhost.eml]
-- Guardar un mensaje de tipo [text/plain]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/message_1.txt]
-- Guardar un mensaje de tipo [text/html]
Le message a été sauvegardé dans le fichier [output/localhost-pop3/message-1/rfc822-1/rfc822-1/message_1.html]
-- Guardarun archivo adjunto de tipo [application/vnd.openxmlformats-officedocument.wordprocessingml.document] en el archivo [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Hello from SwiftMailer.docx]
-- Guardarun archivo adjunto de tipo [application/pdf] en el archivo [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Hello from SwiftMailer.pdf]
-- Guardarun archivo adjunto de tipo [application/vnd.oasis.opendocument.text] en el archivo [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Hello from SwiftMailer.odt]
-- Guardarun archivo adjunto de tipo [image/png] en el archivo [output/localhost-pop3/message-1/rfc822-1/rfc822-1/Cours-Tutoriels-Serge-Tahé-1568x268.png]
Fermeture de la connexion réussie.

Si se visualiza [message_1.HTML] de [3] en un navegador, se obtiene lo siguiente:

Image