Un Mail User Agent con PHP cuando mail() no está disponible

Hacer un mailer con PHP

Mailer con PHP Debido a que la función mail() de PHP puede ser objeto de SPAM en los servidores compartidos, a veces el administrador del servidor la desactiva. Recordemos que esa función hace las veces de un MUA. Es el componente que se comunica con un servidor SMTP entregándole el correo para que ese servidor lo gestione, bien transfiriéndolo a otro servidor o, si es el servidor final, depositándolo en los buzones finales mediante el protocolo POP3. Esta explicación está muy resumida, pues hay más cosas relacionadas con el transporte de correo. Pero lo que nos interesa es que hacemos cuando mail() no está disponible.

En ese caso tenemos que usar algún script que haga las veces de un MUA, es decir, que haga lo mismo que mail(). El código más conocido es el de PHPMAILER originalmente en el sitio sourceforge.net. Aunque la versión actual en este momento es la 5.2.0 y se encuentra en el nuevo sitio phpmailer.worxware.com.

Hace tiempo hice unas pruebas con la versión 5.1 y observé que se compone de dos módulos. Uno es class.smtp.php que viene a ser el agente de transferencia, digamos el que se encarga de comunicarse con el servidor de forma equivalente a como lo hace la función mail(). El otro módulo es class.phpmailer.php y es la parte que se encarga de la edición del mensaje, preparando las cabeceras y conectando con el agente de transferencia que puede ser seleccionado entre los siguientes:

  • El propio módulo class.smtp.php
  • La función mail() de PHP
  • La aplicación Sendmail o QMail instaladas en servidores UNIX. Estas aplicaciones además de recibir el correo de un MUA son capaces de transferirlo al servidor final de correo. Digamos que funcionan como un MTA, que viene a ser una parte de las funciones que realiza un servidor completo de correo SMTP. De hecho la función mail() de PHP usa Sendmail para transferir el correo bajo UNIX, mientras que en Windows hemos de darle la dirección de un servidor SMTP para que haga la función MTA.

Si tenemos nuestro Apache+PHP en Windows no encontraremos Sendmail, por lo que hemos de usar mail() o un módulo que haga esa función. En este tema no voy a explicar cómo se usa PHPMAILER pues en las referencias anteriores puede encontrar mejores explicaciones. Lo que sí voy a hacer es construirme un módulo muy simple que haga las funciones de esos dos módulos: preparar el mensaje y enviarlo a un servidor SMTP.

Una clase en PHP para crear un Agente de Usuario SMTP

Este ejercicio trata de construir una clase denominada objetoSmtp con la que crear instancias para funcionar a modo de MUA. Servirá para enviar email desde un formulario de contacto. Son ejemplos exclusivamente para ejecutar en un localhost, pues si busca algo con propósito de producción es mejor usar la función mail() o bien algo como PHPMAILER que indiqué en el apartado anterior. Este ejercicio, por su sencillez, nos ayudará a entender como funciona un poco esto del protocolo SMTP. Hay dos archivos en formato de texto, uno de ellos contiene el código de smtp.php que es el de la clase objetoStmp. El otro es el código de index.php que contiene un documento PHP-HTML con un formulario de contacto que lo envía a una dirección de email.

De forma abreviada, indicando las declaraciones de variables y métodos, el código de la clase objetoSmtp es la siguiente:

class objetoSmtp {
    const SALTO = "\r\n";
    
    public $dominio = "";
    public $puerto = 25;
    public $timeout = 30;
    
    private $conectado = false;
    private $manejador = 0;
    private $conversacion = "";
    
    //Constructor
    public function __construct(){} 
    
    //Conectar con un servidor de correo
    public function conectar($dominio, $puerto, $timeout){}     

    //Enviar un correo
    public function enviar($de, $para, $asunto, $cuerpo){} 
    
    //Ejecutar comandos SMTP
    private function comandar($comando, $parametro){} 

    //Leer líneas del socket
    private function extraer_linea(){} 
    
    //Generar fecha para campo Date
    public static function Fecha(){} 
    
    //Presentar la conversación con el servidor
    public function extraer_conversacion(){}
}

La clase anterior contiene los métodos mínimos para hacer la función de enviar un email a un servidor de correo. Aparte del constructor de la clase tenemos el método para conectar con el servidor, el que envía el correo y otros métodos privados para el funcionamiento de la clase. Hay un método para presentar el detalle de la "conversación" mantenida entre el servidor y el MUA, es decir, este objeto. Este clase se usaría desde una página PHP que incluye un script PHP al inicio y luego un HTML con el formulario de contacto. El script PHP es el siguiente:

<?php
require "smtp.php";
$servidor_smtp = "";
$de = "";
$para = "";
$asunto = "";
$mensaje = "";
$conversacion = "";
$enviado = false;
if (isset($_GET) && isset($_GET["envio"]) &&
    ($_GET["envio"]=="Enviar")){
    foreach($_GET as $campo=>$valor){
        switch ($campo) {
            case "servidor-smtp": $servidor_smtp = $valor; break;
            case "de": $de = $valor; break;
            case "para": $para = $valor; break;
            case "asunto": $asunto = $valor; break;
            case "mensaje": $mensaje = $valor; break;            
        }
    }
    $correo = new objetoSmtp;
    $enviado = false;
    if ($correo->conectar($servidor_smtp, 25, 30)) {
        $enviado = $correo->enviar($de, $para, $asunto, $mensaje); 
    }
    $conversacion = $correo->extraer_conversacion();   
}
?>

El script revisa si hay GET y recoge los campos del formulario. Entre los típicos de un envío de email está el del servidor SMTP. Para estas pruebas he usado Mercury instalado en modo local, tal como comenté en el primer capítulo de estos temas. Vemos que creamos una nueva instancia del objeto con new objetoSmtp. Luego hacemos la conexión con el método conectar() y si es válida enviamos el correo. Extraemos la conversación para luego presentarla en el HTML que vemos parcialmente a continuación:

<!DOCTYPE html>
<html lang="es">
<head>
    ...
</head>
<body>
    <h3>Pruebas SMTP</h3>
    <form  action="index.php" method="get">
        ...
    </form>
    <?php 
    if ($enviado) {
        echo "<p style=\"color: blue\">Mensaje enviado<p>";
    } else {
        echo "<p style=\"color: red\">Mensaje no enviado</p>";
    }?>    
    <pre style="border: maroon solid 1px;"><?php echo $conversacion; ?></pre>
</body>
</html>

Omito el formulario pues no tiene mayor interés (puede verlo en el código). El resto es simple, nos dirá si fue o no enviado y muestra la conversación. Una captura de una prueba la vemos aquí:

prueba smtp

El servidor, como dije antes, es el Mercury que monté en local y que llamé smtp.localemail. Los usuarios admin y user1 tienen buzones en ese servidor. El mensaje se envía y nos devuelve otra vez ese formulario con un texto incluyendo la conversación producida entre el servidor y el MUA (esto lo veremos con más detalle después). El servidor se conecta con POP3 a un agente de correo, en este caso Outlook Express, recibiéndose ese correo y cuyas cabeceras podemos ver aquí:

cabecera email de la prueba smtp

Conversación SMTP

El protocolo SMTP y lo necesario para hacer un Agente de Usuario de email se basa en las siguientes especificaciones :

Decimos que es una "conversación" entre el servidor SMTP y el agente de usuario, que es en este caso nuestro script objetoSmtp. Lo que sucedió en la conexión del ejemplo anterior se ve aquí en su totalidad. En marrón están los comandos que envío el objetoStmp y en verde las respuestas del servidor:

Nuevo objetoSmtp construido el Thu, 15 Dec 2011 13:28:03 +0000
Conectando a smtp.localemail......
220 smtp.localemail ESMTP server ready.
EHLO smtp.localemail
250-smtp.localemail Hello smtp.localemail; ESMTPs are:
250-TIME
250-SIZE 0
250-8BITMIME
250 HELP
MAIL FROM:<user1@smtp.localemail>
250 Sender OK - send RCPTs.
RCPT TO:<admin@smtp.localemail>
250 Recipient OK - send RCPT or DATA.
DATA
354 OK, send data, end with CRLF.CRLF
From: user1@smtp.localemail
To: admin@smtp.localemail
X-Mailer: Probando SMTP
Subject: prueba
Date: Thu, 15 Dec 2011 13:28:03 +0000

Mensaje
.
250 Data received OK.
QUIT
221 smtp.localemail Service closing channel.

Con el método conectar($dominio, $puerto, $timeout) intentamos conectar con el servidor de correo en smtp.localemail. En el código de objetoSmtp esto se hace con la función fsockopen() que abre un socket para comunicarnos con un servidor. Éste nos responde con un código 220 smtp.localemail ESMTP server ready. Viene a decir que nos reconoce la conexión y está listo. El servidor responde con códigos numéricos especificados en el protocolo, mientras que nosotros (el MUA) le envíamos peticiones mediante comandos. Son palabras claves especificadas también en el protocolo. Así EHLO es un comando de saludo inicial, algo así como que el MUA requiere al servidor para iniciar una petición. El servidor le responde con el código 250 que dice que entiende la petición. El MUA va enviando las cabeceras del sobre como MAIL FROM o RCPT TO y el servidor las recoge devolviendo un estado correcto. Tras el último RCPT TO (en este caso sólo uno) se envían los datos del mensaje, pero antes se lo hacemos saber al servidor enviando el comando DATA. El código 354 le dice al MUA que el servidor está listo para recibir el mensaje y que debe finalizarlo con la secuencia CRLF.CRLF. El mensaje se compone de las cabeceras From, To, X-Mailer, Subject, Date tras lo cual viene un salto de línea y el texto del mensaje. Cuando el servidor recibe CRLF.CRLF devuelve un código OK 250 y luego el MUA cierra la conexión con el comando QUIT.