El formulario de contacto

El formulario de contacto El formulario de contacto con envío de mensajes a una cuenta de email es una utilidad de uso de un sitio web. Cuando la gente ya lleva tiempo usando otras forma alternativas de comunicarse, como las redes sociales, dedicar un rato a estudiar cómo funciona el correo electrónico puede parecer una pérdida de tiempo. Realmente no lo sé, pero creo que es bueno tener al menos una idea general acerca de este tema. Hay varias cosas que quisiera saber, por ejemplo, cómo funciona básicamente un servidor de correo electrónico o qué son los protocolos SMTP o POP3 entre otros. Además sería interesante instalar un servidor de correo a modo de localhost para hacer pruebas.

Normalmente se usa la función mail() de PHP para enviar el mensaje a una cuenta de correo del administrador del sitio. Pero ¿Y sí el propietario del servidor no me permite usar esa función por tenerla desactivada para evitar el spam? ¿Puedo usar algún módulo alternativo a mail() de PHP?. Y sobre todo qué riesgos hay que controlar para que no usen nuestro formulario de contacto con otros propósitos que el previsto, especialmente usando la técnica del CAPTCHA. En definitiva, varias cosas por saber que espero aprenderlas con estas pruebas. El objetivo final es incorporar todo esto en un nuevo formulario de contacto para este sitio.

La función mail() de PHP puede conectar con cualquier servidor de correo, pero a veces éstos bloquean mensajes que provienen de servidores localhost. El asunto tiene que ver con el uso de Apache+PHP para enviar spam. No me interesa saber como conseguir enviar correos con servidores externos, sino más bien cómo funciona el correo y los servidores de correo. Es mejor instalarse uno su propio servidor de correo en modo local y hacer con comodidad todas las pruebas que necesitemos. De otra forma, usando un servidor externo, no sabremos muy bien si el correo no llegó porque hicimos algo mal en el script o que el servidor lo bloqueó.

Hay servidores de correo que podemos montar en local para hacer pruebas. Algunos sólo como una demo, otros tienen licencias no comerciales con lo que podemos usarlos sólo en un entorno localhost. El sitio del autor David Harris Pegasus Mail - Mercury contiene la aplicación de correo Pegasus y el servidor Mercury, ambos para Windows. Este servidor tiene una licencia libre para uso individual sin fines comerciales. Por lo tanto nos servirá para hacer estas pruebas, pero he de decir que no pretendo convertirme en un experto de servidores de correo, pues la finalidad de instalarlo es sólo y exclusivamente para hacer pruebas con la función mail() y aprender algo sobre el protocolo SMTP. A continuación hay unas capturas de pantalla que tomé cuando hice la instalación y la configuración. Son requirimientos mínimos para que la cosa funcione, pues hay otras configuraciones que no me he detenido a estudiar:

Instalando y configurando Mercury
Instalando GD de PHPImagen no disponible
Estos son todos los pasos que seguí para instalarlo. En cada pantalla el botón que pulsé es el que aparece con el foco.
NOTA: Mercury y Pegasus Mail son marcas del software del autor David Harris. Para más detalles ver su sitio web Pegasus Mail - Mercury.

Configurando la función mail() de PHP bajo Windows

La función mail() de PHP nos permite enviar correos electrónicos. Sin embargo hay una diferencia importante si nuestro servidor Apache+PHP está en Windows o Unix. El archivo de configuracion php.ini que viene por defecto con la instalación de PHP contiene una parte para la configuración de la función mail():

[mail function]
; For Win32 only.
; http://php.net/smtp
SMTP = localhost
; http://php.net/smtp-port
smtp_port = 25

; For Win32 only.
; http://php.net/sendmail-from
;sendmail_from = me@example.com

; For Unix only.  You may supply arguments as well (default: "sendmail -t -i").
; http://php.net/sendmail-path
;sendmail_path =

; Force the addition of the specified parameters to be passed as extra parameters
; to the sendmail binary. These parameters will always replace the value of
; the 5th parameter to mail(), even in safe mode.
;mail.force_extra_parameters =

; Add X-PHP-Originating-Script: that will include uid of the script followed by the filename
mail.add_x_header = On

; The path to a log file that will log all mail() calls. Log entries include
; the full path of the script, line number, To address and headers.
;mail.log =

En el sistema operativo Unix existe un ejecutable local que funciona a modo de MTA. Se trata del denominado Mail Transfer Agent o Message Transfer Agent o a veces también llamado mail relay, términos que se traducen como Agente de Transferencia de Correo. El MTA es el encargado de transferir correo de un ordenador a otro, usando la arquitectura cliente-servidor basada en el protocolo SMTP, implementando un smtp server y un stmp client.

En cambio bajo Windows hemos de montar un MTA para lograr transferir el correo, pues este sistema operativo no trae ese ejecutable. Yo tengo mi Apache+PHP en localhost bajo Windows. Para tener un MTA he instalado un servidor de correo que incorpora esa función aparte de otras como servidor de POP3.

Por lo tanto usaré un servidor de correo que he denominado localemail montado a modo localhost, cuyo servidor SMTP lo he llamado smtp.localemail. He creado 3 cuentas de prueba admin@smtp.localemail, user1@smtp.localemail y user2@smtp.localemail. Podría hacer pruebas usando servidores externos como Gmail, Hotmail o Yahoo. O incluso el propio servidor de correo de nuestro sitio en producción. Pero estos servidores suelen estár protegidos para no gestionar correos procedentes de un dominio localhost, pues son una posible fuente de spam y otros riesgos. Como dije antes, no trato aquí de buscar la forma de saltarnos esas protecciones para usar servidores externos, pues no es ese mi interés. Creo que es más productivo saber instalar y configurar un servidor de correo en modo local para hacer estas pruebas.

Una vez instalado el servidor de correo, hemos de configurar el php.ini a algo como esto:

[mail function]
; For Win32 only.
; http://php.net/smtp
SMTP = smtp.localemail
; http://php.net/smtp-port
smtp_port = 25

; For Win32 only.
; http://php.net/sendmail-from
sendmail_from = php@localhost

Estos valores que vamos a dar en el php.ini son configurados por el administrador de nuestro servidor en producción, por lo que no tendremos porque cambiarlos cuando usemos mail() en nuestro sitio real. Pero para estas pruebas locales si hemos de hacerlo. Ponemos el SMTP al de nuestro servidor local de correo smtp.localemail. Dejamos el puerto 25 que viene por defecto. También hemos de poner una dirección para la cabecera from. En este caso ponemos php@localhost, una dirección que ni siquiera existe pero es necesario poner algo. Esta dirección en principio no tiene mayor interés ahora y, como dije antes, estos datos son puestos por el administrador del servidor. Luego veremos un poco más de esto.

Un formulario para usar con la función mail() de PHP

Vamos ahora con el formulario de contacto que podría ser un PHP como este primer ejemplo llamado mail-01.php (ver el código). Este ejemplo sirve única y exclusivamente para hacer pruebas con la funcion mail(), observando los riesgos de seguridad relacionados con el email pero no tiene en cuenta otras medidas (como por ejemplos las que expongo en formularios seguros). Por lo tanto ni puede ejecutarse desde este sitio ni mucho menos usarlo para un propósito real.

<?php
/* mail-01.php
 * Ejemplo de formulario de contacto para enviar email. Esto es un ejemplo
 * muy básico sin protecciones de seguridad, sólo para entender cómo es la
 * función mail() de PHP.
 * Andrés de la Paz © 2011
 * http://www.wextensible.com
 * 
 */
$nombre = "";
$email = "";
$mensaje = "";
$form_iniciado = false;
$enviado = false;
$mensaje_error = "";
if (isset($_GET) && isset($_GET["envio"]) 
    && ($_GET["envio"]=="Enviar")){
    foreach($_GET as $campo=>$valor){
        switch ($campo) {
            case "nombre": $nombre = $valor; break;
            case "email": $email = $valor; break;
            case "mensaje": $mensaje = $valor; break;
        }
    }
    if (($nombre != "")&&($email != "")&&($mensaje != "")){
        $form_iniciado = true;
        $destino = "admin@smtp.localemail";
        $cabeceras = "From: ".$email."\n";
        $asunto = "Mensaje de contacto";
        $cuerpo = "MENSAJE DEL FORMULARIO DE CONTACTO\n".
                  "NOMBRE: ".$nombre."\n".
                  "MENSAJE: \n".$mensaje;
        $enviado = @mail($destino, $asunto, $cuerpo, $cabeceras);
        $arr_error = error_get_last();
        if (!is_null($arr_error)){
            $mensaje_error = "No se pudo enviar el mensaje: ".$arr_error["message"];   
        }                
    } else {
        $mensaje_error = "Todos los campos son requeridos";
    }   
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge; chrome=1" />
    <title>Ejemplo función mail() de PHP</title>
</head>
<body>
    <h3>Formulario de contacto</h3>
    <?php if(!$form_iniciado){ ?>
        <form action="mail-01.php" method="get">
            Nombre <input type="text" name="nombre" size="60" 
                value="<?php echo $nombre; ?>" /><br />
            Email <textarea name="email" rows="2" cols="60">
                <?php echo $email; ?></textarea><br />
            Mensaje <textarea name="mensaje" rows="10" cols="50">
                <?php echo $mensaje; ?></textarea>
            <input type="submit" name="envio" value="Enviar" />
        </form>
    <?php } else {
        if ($enviado) {?>
            <p>Hemos recibido su mensaje.</p>
        <?php } else { ?>
            <p>Hubo un error en el envío.</p>
        <?php }?>
        <p>Datos del mensaje:</p>         
        <ul>
            <li>Nombre: <?php echo $nombre; ?></li>
            <li>Email: <?php echo $email; ?></li>
            <li>Mensaje: <?php echo $mensaje; ?></li>
        </ul>
    <?php }?>
</body>       
</html>

Si queremos recibir un mensaje, suponemos que lo mínimo será conocer el $nombre de la persona y el $mensaje que quiere comunicarnos. Esto en principio no tiene mayores problemas. El campo de su dirección de $email es el más importante, pues nos permitirá responderle. Hemos puesto un <textarea> en este campo para probar los riesgos de seguridad, pero lo usual es que sea un elemento <input type="text"> (en el ejemplo que vamos a ejecutar veremos el motivo de esto). Estos datos los recibimos en el servidor y los insertamos en la función mail()

$enviado = @mail($destino, $asunto, $cuerpo, $cabeceras)

El $destino es la dirección de correo del administrador del sitio donde queremos recibirlo. En este caso el ejemplo pone $destino = "admin@smtp.localemail" y sería una cuenta de nuestro servidor de correo donde veríamos los mensajes del formulario de contacto. El último argumento son las $cabeceras del email. Entonces lanzamos el formulario de contacto y vamos a enviar nuestro primer mensaje:

prueba formulario contacto

El servidor de correo SMTP

Antes de seguir con el ejemplo anterior, debemos saber que la especificación RFC5321 Simple Mail Transfer Protocol (Protocolo simple de transferencia de correo) expone la semántica y sintaxis de los comandos a usar en una comunicación SMTP. Ese documento actualiza la versión anterior RFC2821, que a su vez actualizó la RFC821. Al final de esa especificación hay algunas muestras de ejemplos como A Typical SMTP Transaction Scenario que expone una simple comunicación.

Veámos ahora que pasó con el correo de ejemplo que envié. Recuerde que debe ir al destino admin@smtp.localemail y debe decir que proviene de user1@smtp.localemail. Observando el panel de control del Mercury, la parte que gestiona los mensajes del SMTP-Server:

smpt server

La conexión se establece a través de la IP 127.0.0.4, que es sobre la que configuré el SMTP-Server del Mercury. Lo que vemos son las peticiones según el protocolo SMTP que recibe el MTA (el SMTP-Server) de la función mail() de PHP, que hace las veces de un MUA (Mail User Agent o Agente de Usuario de Correo). La función mail() le ha enviado las siguientes peticiones en azul y el SMTP-Server le va respondiendo en color marrón:

HELO HP92155003154
250-smtp.localemail Hello smtp.localemail
MAIL FROM:<php@localhost>
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
Subject: Mensaje de contacto
Date: Sat, 3 Dec 2011 20:33:16 +0000

MENSAJE DEL FORMULARIO DE CONTACTO
NOMBRE: Andrés
MENSAJE: 
Prueba1
.
250 Data received OK.
QUIT
221 smtp.localemail Service closing channel.

El MUA se identifica con la palabra clave HELO y su nombre de dominio. En este caso pone el nombre de la máquina que es suministrado por la función mail(). El SMTP-Server le responde con un código 250 de estado correcto. Luego el MUA (recuerde que es la función mail() de PHP) le dice la dirección de procedencia MAIL FROM:<php@localhost>. Esta proviene de la configuración sendmail_from=php@localhost que pusimos en el php.ini. El SMTP-Server responde correcto y le pide un destinatario (RCPT). El MUA le envía el destinatario con RCPT TO: <admin@smtp.localemail>. Esta es la dirección que pusimos en el primer argumento de la función mail():

$destino = "admin@smtp.localemail";
$cabeceras = "From: ".$email."\n";
$asunto = "Mensaje de contacto";
$cuerpo = "MENSAJE DEL FORMULARIO DE CONTACTO\n".
          "NOMBRE: ".$nombre."\n".
          "MENSAJE: \n".$mensaje;
$enviado = @mail($destino, $asunto, $cuerpo, $cabeceras);

A continuación el SMTP-Server le pide otro RCPT o los datos, es decir, el cuerpo del mensaje (DATA). En este caso el MUA envía la palabra DATA para hacerle saber eso al SMTP-Server, quién le comunica un código 354 diciendo que está preparado, puede enviar los datos y que los finalice con dos saltos de linea CLRF.CRLF con un punto en medio. Los datos ocupan 9 líneas que podemos desglosar en estos grupos:

  • From: user1@smtp.localemail, este es el "supuesto" origen del mensaje, pero que realmente hemos insertado en el script PHP dentro del argumento cabeceras de la función mail() mediante $cabeceras = "From: ".$email."\n";
  • To: admin@smtp.localemail. Este es el destinatario, el mismo del argumento $destino = "admin@smtp.localemail" para mail().
  • Subject: Mensaje de contacto, el argumento $asunto = "Mensaje de contacto" para mail().
  • Date: Sat, 3 Dec 2011 20:33:16 +0000. La fecha es obligatoria para algunos servidores y la inserta mail() automáticamente.
  • El cuerpo del mensaje, es decir, el contenido de texto del mensaje, que hemos compuesto en el script PHP en la variable $cuerpo. La palabra Andrés es la correspondiente a Andrés pero dado que no especificamos una cabecera de UTF-8 en el correo, el servidor no puede descifrar esos caracteres. Es algo que tendré que estudiar como se resuelve.
  • La función mail() finalizará con CLRF.CRLF

Finalmente el SMTP-Server le responde un 250 de datos recibidos y correctos. El MUA finaliza la conexión enviando la palabra QUIT y el servidor le responde con un código 221 cerrando el canal de conexión. Lo más importante hasta aquí es que hay dos grupos de origenes y destinatarios:

  1. Los del sobre (o envolope en inglés), correspondientes a los establecidos en los primeros pasos de la conexión con
    • El origen MAIL FROM:<php@localhost>
    • El destinatario RCPT TO:<admin@smtp.localemail>
  2. Y los del propio mensaje establecidos en el DATA:
    • El origen From: user1@smtp.localemail
    • El destinatario To: admin@smtp.localemail

Los primeros son los que sirven para trasladar un email desde un servidor de correo a otro. Los segundos son los que se usan para trasladar el mensaje desde el servidor final de correo a un buzón de ese servidor que será el destinatario final. Admito que es bastante díficil de entender, pero se hace más fácil si vemos el siguiente paso.

El servidor de correo POP3

El sobre de correo que se recibe en un servidor puede ir destinado a otro servidor de otro dominio. E incluso puede saltar entre varios servidores. En todo caso tiene que alcanzar al destinatario del mensaje indicado en To: admin@smtp.localemail. En este ejemplo todo ocurre dentro del mismo dominio y servidor. El SMTP-Server tiene que mirar el dominio de destino del sobre RCPT TO:<admin@smtp.localemail> para enviarlo a ese servidor, que en este caso es el mismo. Entonces busca los destinatarios finales en To: admin@smtp.localemail y encuentra que la dirección admin@smtp.localemail está en su lista de mailbox directory, directorio de buzones de correo:

mailbox

El protocolo POP3 se encarga de la entrega final de los mensajes a MUA's como el Outlook express de Windows, por ejemplo. Ahora tenemos un servidor POP3 y un cliente POP3. El servidor POP3 recibe los mensajes de un servidor SMTP y los transfiere a un cliente POP3 que se encarga de la entrega final.

servidor pop3

En el Mercury configuré el servidor pop3.localemail apuntando a la IP 127.0.0.5. El mensaje es para el usuario admin y ocupa 482 bytes. Ahora es el cliente POP3 quién se encarga de la entrega:

cliente pop3

En este caso el cliente POP3 estableció contacto con todos los MUA's conectados. En mi caso hice que el Outlook Express de Windows se conectara al servidor de correo. Esta es la configuración de la cuenta admin@smtp.localemail:

configura cuenta outlook express
configura servidores outlook express

Se observa como el correo entrante debe buscarlo en el servidor pop3.localemail. Y este es el correo recibido en la bandeja de entrada del MUA Outlook Express:

outlook express

Vemos que aparece el origen De señalado como user1@stmp.localemail, pero que realmente no vino desde esa dirección sino de la función mail(). En las propiedades del mensaje podemos ver el mensaje completo, cuyo código fuente copiamos y pegamos aquí para observarlo mejor:

Received: from spooler by smtp.localemail (Mercury/32 v4.72); 3 Dec 2011 20:34:50 -0000
X-Envelope-To: admin@smtp.localemail
Received: from POP3D by smtp.localemail with MercuryD (v4.72); 3 Dec 2011 20:34:41 -0000
Received: from spooler by smtp.localemail (Mercury/32 v4.72); 3 Dec 2011 20:34:17 -0000
X-Envelope-To: admin@smtp.localemail
Received: from POP3D by smtp.localemail with MercuryD (v4.72); 3 Dec 2011 20:34:06 -0000
Received: from spooler by smtp.localemail (Mercury/32 v4.72); 3 Dec 2011 20:33:32 -0000
X-Envelope-To: admin@smtp.localemail
Received: from POP3D by smtp.localemail with MercuryD (v4.72); 3 Dec 2011 20:33:32 -0000
Received: from spooler by smtp.localemail (Mercury/32 v4.72); 3 Dec 2011 20:33:21 -0000
X-Envelope-To: <admin@smtp.localemail>
Return-path: <php@localhost>
Received: from HP92155003154 (127.0.0.4) by smtp.localemail (Mercury/32 v4.72) ID MG000001;
   3 Dec 2011 20:33:16 -0000
Date: Sat, 03 Dec 2011 20:33:16 +0000
Subject: Mensaje de contacto
To: admin@smtp.localemail
From: user1@smtp.localemail

MENSAJE DEL FORMULARIO DE CONTACTO
NOMBRE: Andrés
MENSAJE: 
Prueba1

Los distintos agentes van agregando cabeceras al mensaje antes de la fecha, es decir, desde la línea de Date y hacia arriba. Así la primera cabecera es la que agregró el SMTP-Server al recibirlo de la función mail(), que en este caso pone como referencia la máquina from HP921... (mi ordenador). Luego el SMTP-Server lo envia al POP3 para que lo distribuya al destinatario final admin@smtp.localemail. No es ahora mi intención entender completamente las cabeceras de un email, pues es bastante complicado. Sólo quiero tener una visión general del tema para cuando haga uso de la función mail() saber más sobre cuestiones de seguridad.