11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 1/16
27
DIC
2011
El formulario de contacto con función mail() de PHP
Inicio» Cómo se hace» I. El formulario de contact...» II. Incorporando seguridad al f...» III. Evitar el uso automático
de...» IV. Un Mail User Agent con PHP ...
1. El formulario de contacto
2. Configurando la función mail() de PHP bajo Windows
3. Un formulario para usar con la función mail() de PHP
4. El servidor de correo SMTP
5. El servidor de correo POP3
1. 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
wextensible
11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 2/16
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
11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 3/16
1/20
Estos son todos los pasos que seguí para instalarlo. En cada pantalla el botón que pulsé esel 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.
2. 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/smtpSMTP = localhost; http://php.net/smtp-portsmtp_port = 25
11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 4/16
; For Win32 only.; http://php.net/sendmail-from;sendmail_from = [email protected]
; 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 filenamemail.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 [email protected],
[email protected] y [email protected]. 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
11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 5/16
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/smtpSMTP = smtp.localemail; http://php.net/smtp-portsmtp_port = 25
; For Win32 only.; http://php.net/sendmail-fromsendmail_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.
3. 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
11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 6/16
* 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 = "[email protected]"; $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"; } }?>
11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 7/16
<!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">
11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 8/16
(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 = "[email protected]" 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:
4. 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
[email protected] y debe decir que proviene de [email protected]. Observando el panel de
control del Mercury, la parte que gestiona los mensajes del SMTP-Server:
11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 9/16
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 HP92155003154250-smtp.localemail Hello smtp.localemailMAIL FROM:<php@localhost>250 Sender OK - send RCPTs.RCPT TO:<[email protected]>250 Recipient OK - send RCPT or DATA.DATA354 OK, send data, end with CRLF.CRLFFrom: [email protected]: [email protected]: Mensaje de contacto
11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 10/16
Date: Sat, 3 Dec 2011 20:33:16 +0000
MENSAJE DEL FORMULARIO DE CONTACTONOMBRE: AndrésMENSAJE: Prueba1.250 Data received OK.QUIT221 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: <[email protected]>. Esta es la
dirección que pusimos en el primer argumento de la función mail():
$destino = "[email protected]";$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: [email protected], 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";
11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 11/16
To: [email protected]. Este es el destinatario, el mismo del argumento $destino =
"[email protected]" 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:<[email protected]>
2. Y los del propio mensaje establecidos en el DATA:
El origen From: [email protected]
El destinatario To: [email protected]
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.
5. 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
11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 12/16
en To: [email protected]. 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:<[email protected]> para
enviarlo a ese servidor, que en este caso es el mismo. Entonces busca los destinatarios finales en To:
[email protected] y encuentra que la dirección [email protected] está en su lista de mailbox
directory, directorio de buzones de correo:
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.
11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 13/16
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:
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
11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 14/16
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:
11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 15/16
Vemos que aparece el origen De señalado como [email protected], 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 -0000X-Envelope-To: [email protected]: from POP3D by smtp.localemail with MercuryD (v4.72); 3 Dec 2011 20:34:41 -0000Received: from spooler by smtp.localemail (Mercury/32 v4.72); 3 Dec 2011 20:34:17 -0000X-Envelope-To: [email protected]: from POP3D by smtp.localemail with MercuryD (v4.72); 3 Dec 2011 20:34:06 -0000Received: from spooler by smtp.localemail (Mercury/32 v4.72); 3 Dec 2011 20:33:32 -0000X-Envelope-To: [email protected]: from POP3D by smtp.localemail with MercuryD (v4.72); 3 Dec 2011 20:33:32 -0000Received: from spooler by smtp.localemail (Mercury/32 v4.72); 3 Dec 2011 20:33:21 -0000X-Envelope-To: <[email protected]>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 -0000Date: Sat, 03 Dec 2011 20:33:16 +0000Subject: Mensaje de contactoTo: [email protected]: [email protected]
MENSAJE DEL FORMULARIO DE CONTACTONOMBRE: AndrésMENSAJE: Prueba1
Los distintos agentes van agregando cabeceras al mensaje antes de la fecha, es decir, desde la línea de
11/7/2014 Cómo se hace un formulario de contacto: Función mail() de PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/ 16/16
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 [email protected]. 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.
11/7/2014 Cómo se hace un formulario de contacto: Incorporando seguridad
http://www.wextensible.com/como-se-hace/formulario-contacto/email-seguro.html 1/12
27
DIC
2011
Incorporando seguridad al formulario de contacto
Inicio» Cómo se hace» I. El formulario de contacto c...» II. Incorporando seguridad ...» III. Evitar el uso automático
de...» IV. Un Mail User Agent con PHP ...
1. Uso indebido de la función mail() de PHP
2. Evitar la inserción de cabeceras en mail() de PHP
3. Medidas de seguridad para usar mail() en el formulario de contacto
4. Codificación UTF-8 en el formulario
5. La longitud de un texto en UTF-8
1. Uso indebido de la función mail() de PHP
Con el formulario de contacto de ejemplo del tema anterior me propongo
realizar algunas pruebas de usos indebidos de la función mail() de PHP.
Usaré el servidor de correo Mercury montado a modo de localemail y dos
aplicaciones MUA (Mail User Agent) Outlook Express y Pegasus Mail para
que se conecten mediante POP3 a ese servidor. Los usuarios de prueba
serán [email protected] que consultaré desde Outlook Express y
[email protected] desde Pegasus Mail.
La prueba consistirá en enviar un mensaje desde el formulario de contacto
insertando indebidamente una cabecera CC en el campo del email, como se
observa en la imagen. Recuerde que configuramos el script de la función mail() de PHP para que nos
remitiera un correo a nuestra dirección [email protected]. También comenté que el campo para el
email es un <textarea>, por lo que tras la primera dirección [email protected] podemos poner un
salto de línea y Cc: [email protected]. De esta forma estamos enviando una copia del correo a ese
wextensible
11/7/2014 Cómo se hace un formulario de contacto: Incorporando seguridad
http://www.wextensible.com/como-se-hace/formulario-contacto/email-seguro.html 2/12
otro usuario . Esto es lo que vemos en los dos MUA's, en el Outlook Express (donde muestra la hora 11:31
del envío desde el POP3):
Y en el Pegasus Mail (la hora 11:30 que muestra es la de la cabecera Date, momento en el que se recibe el
mensaje en el SMTP-Server. No es la del envío de POP3 que fue a las 11:31):
La duplicación de mensajes se entiende si vemos el estado del cliente POP3 en Mercury. A las 11:31:08 se
conecta el usuario [email protected] (que está en el Outlook) y el servidor le entrega el mensaje y a
su vez remite una copia a [email protected]. A las 11:31:09 se conecta el usuario que está en
Pegasus user2@localemail, el servidor le entrega el mensaje que va su nombre y envía a su vez una copia
11/7/2014 Cómo se hace un formulario de contacto: Incorporando seguridad
http://www.wextensible.com/como-se-hace/formulario-contacto/email-seguro.html 3/12
Puede ver una copia de las cabeceras de estos mensajes. En todo caso aquí lo importante no es esta
duplicación, sino el hecho de que podrían permitirse enviar múltiples correos a modo de spam. Vea como en
la primera de las cabeceras encontramos dos X-Envelope-To, es decir, los dos destinatarios
[email protected] y [email protected]:
Received: from spooler by smtp.localemail (Mercury/32 v4.72); 6 Dec 2011 11:31:11 -0000X-Envelope-To: [email protected]: from POP3D by smtp.localemail with MercuryD (v4.72); 6 Dec 2011 11:31:10 -0000Received: from spooler by smtp.localemail (Mercury/32 v4.72); 6 Dec 2011 11:31:00 -0000X-Envelope-To: <[email protected]>Return-path: <php@localhost>Received: from HP92155003154 (127.0.0.4) by smtp.localemail (Mercury/32 v4.72) ID MG00000A; 6 Dec 2011 11:30:54 -0000
11/7/2014 Cómo se hace un formulario de contacto: Incorporando seguridad
http://www.wextensible.com/como-se-hace/formulario-contacto/email-seguro.html 4/12
Date: Tue, 06 Dec 2011 11:30:54 +0000Subject: Mensaje de contactoTo: [email protected]: [email protected]: [email protected]
El problema es que el campo donde va la dirección email no debe permitir saltos de línea, pues alguién
podría insertar una lista de cabeceras Cc para hacer salir múltiples copias del mensaje. En esto y otros
agujeros de seguridad se basa el spam. No basta con cambiar el <textarea> por un <input
type="text">, el cual suprime los saltos de línea. Por un lado podrían ponernos una lista de direcciones de
correo separadas por comas. Por otro lado este ejemplo envía los datos por GET, por lo que podrían
también hacer una petición directa con cabeceras Cc, Bcc o con una lista de direcciones. Por ejemplo:
http://.../mail-01.php?nombre=A&email=user1%40smtp.localemail%0D%0ACc%3A+user2%40smtp.localemail&mensaje=XXXXXXX&envio=Enviar
Esta cadena la he separado en dos líneas pero realmente es una única línea. Los puntos suspensivos sería
la ruta donde se encontraría el script mail-01.php que vimos en el tema anterior. Veáse resaltado el salto
de línea codificado para enviar por URL %0D%0A antes de la cabecera de copia Cc. Algo que podemos hacer
es cambiar el método de envío a POST, pero aún así se pueden insertar cabeceras. Con algo como Telnet
podemos enviar una petición como esta:
POST /.../mail-01.php HTTP/1.1Host: localhostContent-Type: application/x-www-form-urlencodedContent-Length: 102
nombre=A&email=user1%40smtp.localemail%0D%0A...&envio=Enviar
Esta petición incluye el tipo de contenido application/x-www-form-urlencoded que es para poner los
campos y valores codificados tal como lo hacemos en las peticiones GET. La longitud de 102 caracteres es la
que se corresponde con los caracteres de la cadena completa, la cual hemos cortado aquí para simplificar.
Antes de esa cadena hay que poner un salto de línea.
2. Evitar la inserción de cabeceras en mail() de PHP
11/7/2014 Cómo se hace un formulario de contacto: Incorporando seguridad
http://www.wextensible.com/como-se-hace/formulario-contacto/email-seguro.html 5/12
Por lo tanto sea con GET o POST hemos de impedir que el usuario pueda modificar las cabeceras que
incluiremos en la función mail(). Recordemos como era nuestro script en ese punto:
$destino = "[email protected]";$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);
Para evitar exponer el argumento de $cabeceras (headers) con el email que nos ponga el usuario del
formulario de contacto, podemos incluir esa dirección dentro del cuerpo de texto del mensaje:
$destino = "[email protected]";$cabeceras = "From: [email protected]\n";$asunto = "Mensaje de contacto";$cuerpo = "MENSAJE DEL FORMULARIO DE CONTACTO\n". "NOMBRE: ".$nombre."\n". "EMAIL: ".$email."\n". "MENSAJE: \n".$mensaje;$enviado = @mail($destino, $asunto, $cuerpo, $cabeceras);
De esta forma cualquier cosa que introduzcan en nuestro formulario de contacto irá a parar al cuerpo del
mensaje, no tocándo en ningún caso las cabeceras ni por supuesto la línea del $asunto (subject) que lo
dejamos con una cadena fija, pues ahí también podrían insertarse cabeceras con saltos de línea. Hacemos
ese cambio en el script anterior y lo llamamos ahora mail-02.php (ver código). Por ejemplo, enviando un
mensaje con el formulario de prueba con 4 direcciones en el campo de email y habiendo hecho esos cambios
en el script tenemos el siguiente mensaje recibido en el correo del administrador del sitio (la cuenta
11/7/2014 Cómo se hace un formulario de contacto: Incorporando seguridad
http://www.wextensible.com/como-se-hace/formulario-contacto/email-seguro.html 6/12
Todas las direcciones introducidas en el campo $email recibido del formulario van a parar dentro del texto
del cuerpo del mensaje. El remitente y el destinatario son ambos [email protected]. Por supuesto
que en una versión final del formulario de contacto podemos impedir que se ponga más de una dirección en
el campo email, pero eso lo veremos a continuación con una versión mejorada del formulario.
3. Medidas de seguridad para usar mail() en el formulario de contacto
Ahora tenemos una nueva versión del script que controla el formulario. Se trata de la página email-03.php
(ver código) que tiene este PHP al inicio
$nombre = "";$max_longitud_nombre = 50;$email = "";$max_longitud_email = 50;$mensaje = "";$max_longitud_mensaje = 500;$form_iniciado = false;$enviado = false;$mensaje_error = "";if (isset($_POST) && isset($_POST["envio"]) && ($_POST["envio"]=="Enviar")){ foreach($_POST as $campo=>$valor){
11/7/2014 Cómo se hace un formulario de contacto: Incorporando seguridad
http://www.wextensible.com/como-se-hace/formulario-contacto/email-seguro.html 7/12
$valor = htmlspecialchars($valor, ENT_QUOTES); $longitud = strlen(utf8_decode($valor)); switch ($campo) { case "nombre": $nombre = $valor; if ($nombre == ""){ $mensaje_error .= "El nombre es requerido.<br />"; } else if ($longitud > $max_longitud_nombre) { $mensaje_error .= "Nombre sobrepasa ". $max_longitud_nombre." letras.<br />"; } break; case "email": $email = $valor; $patron = "/̂\w+(?:[\-\.]?\w+)*@\w+(?:[\-\.]?\w+)". "*(?:\.[a-zA-Z]{2,4})+$/"; if ($email == ""){ $mensaje_error .= "El email es requerido.<br />"; } else if ($longitud > $max_longitud_email) { $mensaje_error .= "Email sobrepasa ". $max_longitud_email." letras.<br />"; } else if (!preg_match($patron, $email)) { $mensaje_error .= "Email no válido.<br />"; } break; case "mensaje": $mensaje = $valor; if ($mensaje == ""){ $mensaje_error .= "El mensaje es requerido.<br />"; } else if ($longitud > $max_longitud_mensaje) { $mensaje_error .= "Mensaje sobrepasa ". $max_longitud_mensaje." letras.<br />"; } $mensaje = wordwrap($mensaje, 70, PHP_EOL, true); break; } } if ($mensaje_error == ""){ $form_iniciado = true; $destino = "[email protected]";
11/7/2014 Cómo se hace un formulario de contacto: Incorporando seguridad
http://www.wextensible.com/como-se-hace/formulario-contacto/email-seguro.html 8/12
$cabeceras = "From: [email protected]".PHP_EOL. "X-Mailer: PHP-mail".PHP_EOL. "MIME-Version: 1.0".PHP_EOL. "Content-type: text/plain; charset=UTF-8".PHP_EOL. "Content-transfer-encoding: 8BIT".PHP_EOL; $asunto = "Mensaje de contacto"; $cuerpo = "MENSAJE DEL FORMULARIO DE CONTACTO".PHP_EOL. "NOMBRE: ".$nombre.PHP_EOL. "EMAIL: ".$email.PHP_EOL. "MENSAJE: ".PHP_EOL.$mensaje; $enviado = @mail($destino, $asunto, $cuerpo, $cabeceras); if (!$enviado){ $mensaje_error = "No se pudo enviar el mensaje. "; //En producción no debería mostrarse el error $arr_error = error_get_last(); $mensaje_error .= $arr_error["message"]; } }}
En este ejemplo escapamos en los valores recibidos todas las referencias a caracteres reservados de HTML
con la función htmlspecialchars(). Es una medida extra de seguridad más bien orientada a que esos
valores se van a devolver en la misma página en caso de error o cuando se finalice el envío correcto del
formulario. Puede ver más sobre esto en este mismo sitio en filtrar entradas con htmlspecialchars().
También controlamos las longitudes máximas de los campos contando los caracteres con
$longitud=strlen(utf8_decode($valor)). Sobre esto comentaré algo más en un apartado posterior.
También forzamos a que se nos envíe una dirección de email correcta. Aunque luego la vamos a insertar en
el cuerpo y por tanto no importaría que no lo fuera, es obvio que esperamos que ahí haya una dirección de
email y no otra cosa. El patrón usado es:
/̂\w+(?:[\-\.]?\w+)*@\w+(?:[\-\.]?\w+)*(?:\.[a-zA-Z]{2,4})+$/
Puede ver más sobre esto en el tema de validar formularios, en la sección de expresiones regulares. En este
punto he de decir que estoy presentando este ejemplo sin hacer uso del script para validar formularios
expuesto en esos temas. La razón es que quiero ir viendo los pormenores antes de pasar a usar ese
sistema de validación con el formulario de contacto ya en producción en este sitio.
11/7/2014 Cómo se hace un formulario de contacto: Incorporando seguridad
http://www.wextensible.com/como-se-hace/formulario-contacto/email-seguro.html 9/12
Siguiendo con este script pasamos a validar el campo $mensaje. Por un lado controlamos el tamaño
máximo. Luego recortamos a líneas de 70 caracteres. En principio la especificación RFC5322 (la que
sustituye a la 2822) dice que una línea no puede tener más de 998 caracteres (1000 con el salto CRLF),
aunque en todo caso no debería tener más de 78 (80 con CRLF). Al mismo tiempo en la página del manual
de PHP de la función mail() pone un ejemplo haciendo un recorte de línea a 70 caracteres. El caso es que
es posible que los MUA que reciben y muestran el correo podrían gestionar líneas incluso más largas de los
1000 caracteres. Pero sinceramente no sé si podría afectar en algo, por lo que para empezar es mejor hacer
ese recorte.
La función wordwrap($mensaje, 70, PHP_EOL, true) recorta en líneas de 70 caracteres insertando un
salto de línea que viene definido por la constante PHP_EOL propia de PHP. Es tal que el salto de línea queda
definido según donde actúe PHP (p.e., CRLF en Windows y LF en Unix). El último argumento ordena que
corte la palabra aún el caso de que tenga más de 70 caracteres.
Si no hay ningún mensaje de error pasamos a enviar el mensaje. Las cabeceras se separan por un salto de
línea, usándose la constante PHP_EOL. Aparte de la cabecera From: [email protected] donde
ponemos la misma dirección que en $destino, hemos puesto también X-Mailer que nos servirá para
identificar (con ese o cualquier otro término) este correo como proveniente de nuestro formulario. Luego
vienen tres cabeceras para configurar la codificación de caracteres. Se trata de MIME-Version, Content-
type y Content-transfer-encoding de 8 bits para UTF-8. La cabecera MIME es necesaria para activar las
características de codificación distintas a ASCII. El tipo de contenido en este caso es text/plain pues no
deseamos que nos envíen código HTML que sería con text/html. El charset=UTF-8 coincide con el de la
página y por tanto el del formulario.
Por último señalar que con @mail() desactivamos que se muestre cualquer posible error. Aunque mientras
lo probamos en localhost lo recuperamos con error_get_last() para luego mostrarlo.
4. Codificación UTF-8 en el formulario
En cuanto al HTML que le sigue al script anterior es igual que el del ejemplo del tema anterior, pero sólo
cambia lo que aparece resaltado
11/7/2014 Cómo se hace un formulario de contacto: Incorporando seguridad
http://www.wextensible.com/como-se-hace/formulario-contacto/email-seguro.html 10/12
...<body> <h3>Formulario de contacto</h3> <?php if(!$form_iniciado){ ?> <form action="mail-03.php" method="post" accept-charset="utf-8"> ... ...</body> </html>
Aquí cambiamos el GET por el POST. Aunque éste método no evita que alguién use algo como Telnet para
hacer peticiones, tiene la ventaja de que los campos no aparecen en la URL. Los buscadores a veces
indexan estas URL y no siempre conviene exponer datos privados de esta forma en índices públicos.
El atributo accept-charset fuerza al navegador a que use sólo la codificación dada UTF-8 para los datos
del formulario. Si este atributo no está presente el navegador usa la codificación de la página. Podría
pensarse que no es necesario hacer más nada si ya la página está en UTF-8. Pero también es posible que el
usuario cambie manualmente la codificación en el menu de herramientas del navegador. Por ejemplo, en
Chrome cambiando a una codificación no occidental como Árabe (Windows-1256), remitimos los siguientes
valores:
NOMBRE: andrésEMAIL: [email protected]:CañónBarça
Este mensaje se recibe en Outlook Express con las siguientes cabeceras relacionadas con la codificación,
realmente las que pusimos en el script PHP:
...MIME-Version: 1.0Content-type: text/plain; charset=UTF-8Content-transfer-encoding: 8BIT...
11/7/2014 Cómo se hace un formulario de contacto: Incorporando seguridad
http://www.wextensible.com/como-se-hace/formulario-contacto/email-seguro.html 11/12
Pero lo que vemos en el correo del Outlook son caracteres que han desaparecido o están sustituidos por su
referencia Unicode en los campos de nombre y mensaje:
La razón es que el contenido del formulario se envía con la codificación seleccionada por el usuario Árabe
(Windows-1256), de tal forma que la codificación UTF-8 resulta mal formada. Para evitar esto ponemos ese
atributo accept-charset en el formulario. Así aunque el usuario cambie la codificación, esto no afecta a los
valores del mismo que siguen estando codificados en UTF-8.
5. La longitud de un texto en UTF-8
Al preparar el script anterior me he dado cuenta de que la medición de longitud de caracteres la estaba
haciendo erróneamente con la función de PHP strlen($cadena). Sin embargo para una cadena como
"cañón" que contiene la "ñ" y la "ó" que no pertenecen a ASCII códigos 1-127, UTF-8 las codifica con 2
bytes, con lo que la función strlen("cañón") nos da una longitud de 7 caracteres.
Hace ya tiempo que hice unas pruebas con algoritmos de transformación UTF-8 para
entender un poco todo eso. Veámos esto otra vez. La imagen de la izquierda
presenta cuatro caracteres UTF-8. Corresponden a los códigos UNICODE con
números decimales 65, 937, 35486 y 66436. Su representación de texto es AΩ語 . Quizás los dos últimos
caracteres no se presenten en su navegador y se verán como un recuadro o un signo de interrogación. Para
verlos es necesario ajustar su navegador para que los represente, pero en todo caso esos caracteres están
ahí. ¿Cuál es la longitud de esta cadena de texto?. La respuesta es obvia, tiene 4 caracteres. Pero si
11/7/2014 Cómo se hace un formulario de contacto: Incorporando seguridad
http://www.wextensible.com/como-se-hace/formulario-contacto/email-seguro.html 12/12
usamos la función de PHP strlen($cadena) resulta que nos dará 10 caracteres. Realmente esa función
está contando bytes, pues son exactamente 10 los que contiene: 41,CE,A9,E8,AA,9E,F0,90,8E,84
(expresados en hexadecimal).
Para contar los caracteres de una cadena UTF-8 podemos decodificarla primero a ISO-8859-1, que son
caracteres codificados en 1 byte (8 bits), en el rango 0-255 (es el equivalente al ASCII extendido con 8
bits). Esa conversión la podemos hacer con la función de PHP utf8_decode() que tomará caracter a caracter
UTF-8 para intentar hacerlos corresponder con los 256 de ISO-8859-1. Luego le pasaríamos el strlen()
para contarlos. Si hacemos utf8_decode("AΩ語 ") obtenemos A???. Los signos de interrogación son los
caracteres UTF-8 que PHP no pudo traducir pues no están en la tabla 0-255. Pero a efectos de contar
caracteres no nos importa en que se traduzcan, pues haciendo strlen(utf8_decode("AΩ語 ")) obtenemos
la longitud de 4 caracteres que estamos buscando.
11/7/2014 Cómo se hace un formulario de contacto: Uso de CAPTCHA
http://www.wextensible.com/como-se-hace/formulario-contacto/captcha.html 1/12
27
DIC
2011
Evitar el uso automático del formulario de contacto con Captcha
Inicio» Cómo se hace» I. El formulario de contacto c...» II. Incorporando seguridad al f...» III. Evitar el uso
automáti...» IV. Un Mail User Agent con PHP ...
1. Cómo funciona un CAPTCHA
2. Instalando GD2 de PHP
3. Un CAPTCHA con GD2 de PHP
4. Mejorando la usabilidad del CAPTCHA
5. Mejorando la robustez del CAPTCHA
6. Estructura de la página de prueba del CAPTCHA
1. Cómo funciona un CAPTCHA
CAPTCHA es el acrónimo de Completely Automated Public Turing test to tell Computers and Humans Apart,
traducido como prueba de Turing completamente pública y automática para diferenciar los ordenadores y
humanos. En las pruebas de formulario de contacto del tema anterior intentaba evitar que el usuario
insertara cabeceras no deseadas en el email. Con eso quería que no se utilizara el formulario para enviar
múltiples correos, a modo de SPAM. Pero aún así es posible usar un programa que haga un montón de
peticiones a nuestro sitio enviando formularios con datos y por tanto nos llenen el buzón de basura. Con la
técnica del CAPTCHA intentaré impedir que esto suceda.
wextensible
11/7/2014 Cómo se hace un formulario de contacto: Uso de CAPTCHA
http://www.wextensible.com/como-se-hace/formulario-contacto/captcha.html 2/12
El sitio captcha.net ofrece servicio gratuito para incorporar este sistema en un formulario de nuestro sitio.
La imagen anterior es como se vería un control de este tipo. El usuario tendrá que introducir las dos
palabras que aparecen como una imagen y que verificará el servidor en www.captcha.net. En caso de que
le resulte ilegible puede actualizarlas dándole otro conjunto de palabras para intentar descifrarlas.
La base de esta técnica es muy simple. Se trata de que a un ordenador, es decir, a un programa de
ordenador le resulta bastante trabajaso extraer las palabras en forma de texto a partir de una imagen de
ese texto. Se trata en parte de usar la técnica de reconocimiento de caracteres, OCR Optical character
recognition. Aunque explotando aquellos aspectos que suponen más esfuerzo en OCR. Un uso de OCR es
para extraer el texto de un documento de papel escaneado, observándose que el reconocimiento de
caracteres puede verse alterado por cosas como:
Píxeles del fondo que no forman parte de los caracteres pueden alterarlos.
El documento contiene muchas fuentes de texto y al sistema le resulta díficil encontrar patrones para
algunas fuentes poco comunes.
Los caracteres pueden aparecer deformados o le faltan partes.
La distancia entre caracteres puede verse alterada produciendo errores.
Los caracteres que aparecen juntos o incluso solapados son díficiles de separar.
Actualmente el reconocimiento de caracteres por parte de un programa informático es un problema no
totalmente resuelto. Sin embargo a los humanos nos resulta más fácil alcanzar la solución a ese problema.
Pero a medida que la informática avanza se va reduciendo esa distancia. Prueba de ello es que los primeros
sistemas de CAPTCHA fueron rotos en su día y a medida que esos sistemas van incorporando aspectos más
complejos también se van buscando nuevas técnicas para romperlos. Esto es importante de resaltar porque
un CAPTCHA sólo tiene una utilidad: "Intentar saber que el usuario es un humano", pero las máquinas cada
vez se parecen más a los humanos.
Como dice la definición, un CAPTCHA debe ser automático y sobre todo debe ser público. Así el
hecho de romper un CAPTCHA debe basarse en técnicas de inteligencia artificial más que en el
conocimiento del algoritmo que genera el CAPTCHA. Pero no podemos perder de vista que un
CAPTCHA no es invulnerable. Esto quiere decir que de un conjunto de CAPTCHAs podría ser resuelto
alguna proporción de casos por una máquina, al menos en un tiempo comparable al que necesitaría un
11/7/2014 Cómo se hace un formulario de contacto: Uso de CAPTCHA
http://www.wextensible.com/como-se-hace/formulario-contacto/captcha.html 3/12
humano. Será más robusto si esa proporción es baja. Pero al mismo tiempo también debe tener la
característica de usabilidad. Por ejemplo, la imagen de la izquierda contiene los caracteres aQ3mK y podría
ser un buen candidato para cumplir la cualidad de robustez, pero es dificultoso para que un humano lo
resuelva. Si cada vez más las máquinas son capaces de resolver los CAPTCHAs, habrá un momento en que
ya no podrán ser más robustos pues dejarán de ser usables. Entonces habrá que usar otra cosa como
CAPTCHAs basado en imágenes en lugar de texto.
2. Instalando GD2 de PHP
En el siguiente apartado presento como hice unas pruebas para hacer un sistema de CAPTCHA. Usaré la
extensión de PHP GD image. Como dice la introducción del manual de PHP, con esa extensión se pueden
crear y manipular archivos de imágenes en una variedad de formatos como GIF, PNG, JPEG entre otros.
Algunas funciones de esa extensión como imagechar() que nos permite dibujar un caracter en la imagen
nos servirán para crear el CAPTCHA de texto. Otras funciones como imagerotate() nos permitirán girar la
imagen. También podemos aplicar filtros con imagefilter() pudiendo hacer cosas como contrastar,
difuminar o superficializar la imagen. Con estos u otros filtros conseguiremos aplicar las deformaciones
necesarias a la imagen del texto de nuestro CAPTCHA.
Podemos hacer pruebas con esa extensión en localhost, pero si al final vamos a poner un CAPTCHA en
nuestro sitio en producción hemos de saber si tiene activada la extension GD (lo podemos saber viendo el
info.php). En las instalaciones de PHP en Windows (como es mi caso en localhost para aprender),
necesitamos que esta extensión haya sido instalada inicialmente. Si no es así podemos modificar la
instalación tal como comento en este grupo de capturas de pantalla:
Instalando GD2 de PHP
11/7/2014 Cómo se hace un formulario de contacto: Uso de CAPTCHA
http://www.wextensible.com/como-se-hace/formulario-contacto/captcha.html 4/12
1/7
En agregar o quitar programas de Windows podemos ir a la aplicación de PHP para hacercambios
3. Un CAPTCHA con GD2 de PHP
Una forma de aprender cómo funciona un CAPTCHA es hacer un ejemplo. Tras probarlo intentaré ponerlo en
11/7/2014 Cómo se hace un formulario de contacto: Uso de CAPTCHA
http://www.wextensible.com/como-se-hace/formulario-contacto/captcha.html 5/12
el formulario de contacto de este sitio. El ejemplo lo haré primero para ser ejecutado en localhost. Se trata
de una página PHP con un formulario que reenvía los datos al script en el mismo documento. En el último
apartado de este tema comentaré algunas cosas sobre el script. Por ahora me detengo en los detalles de
concepto. En la imagen de la izquierda aparece una captura de pantalla del formulario para hacer pruebas
generando CAPTCHAs. El botón "Ver texto" y la cadena adjunta nos ofrece el texto a verificar mientras
estamos haciendo pruebas, pero en una versión definitiva el texto a resolver no será enviado bajo ninguna
circustancia al navegador del usuario. Sólo la respuesta correcta nos llegará al servidor. Y aún así habría que
considerar la posibilidad de que pueda ser interceptada en el camino, pero esa es otra díficil cuestión que ni
me atrevo a abordar por ahora. Estos son unos ejemplos de imágenes de CAPTCHA que se obtienen:
La robustez se basa en generar imágenes que contengan el menor número posible de invariantes. Estas
son características que no varían entre una imagen y otra. Serían esas las que podrían tomarse como
patrones para resolver el problema. El tema es interesante pero muy complejo y sobrepasa mis
conocimientos. Pero hay un par de principios mínimos que tendré en cuenta al generar un texto CAPTCHA:
Generar cadenas con caracteres elegidos aleatoriamente.
Caracteres posicionados en el eje horizontal tratando de que se peguen entre ellos, buscando el
equilibrio adecuado para evitar un exceso de solapamiento que dificulte la usabilidad.
Posicionamiento aleatorio de cada caracter en el eje vertical. Con esto se busca que los caracteres
estén unidos entre sí no siempre por los mimos sitios.
La imagen final ha de tener el menor tamaño original posible, ajustándolo al tamaño de la fuente y
aplicando un escalado final suficiente para no perder usabilidad.
La imagen se rota un cierto ángulo positivo o negativo, elegido al azar entre un rango predeterminado
para deformar los caracteres.
La imagen se distorsiona aplicando filtros de difuminado, contraste o superficialización.
En estas dos imágenes se muestra una cadena sin aplicar ninguna deformación y como resulta con ellas:
11/7/2014 Cómo se hace un formulario de contacto: Uso de CAPTCHA
http://www.wextensible.com/como-se-hace/formulario-contacto/captcha.html 6/12
Se puede usar un conjunto de caracteres a predeterminar. En este ejemplo uso los rangos 0..9, a..z, A..Z.
La cadena de texto se genera seleccionando aleatoriamente 6 caracteres, tamaño que se puede
predeterminar también, adaptándose el script automáticamente a ese tamaño. Elegir al azar los caracteres
tiene la desventaja de que pueden formarse combinaciones más débiles apareciendo caracteres aislados que
no se conectan con los adyacentes. Pero la posibilidad de construir un diccionario de palabras robustas no
me parece apropiado. Por un lado supone un mayor coste de recursos, pues habrá que cargar esa lista de
palabras en alguna estructura como un array. Además las palabras resueltas podrían ser reutilizadas de
alguna forma. Por lo tanto la solución pasa por generarlos aleatoriamente y corregir ciertas deficiencias en
el script, como acercando más algunos caracteres como i,l,1 que tienen un ancho efectivo de caracter
menor.
Se usa la fuente GD que viene por defecto con la extensión. Sería una mejora usar otras fuentes True Type
e incluso poner varias de forma aleatoria. Pero hay que saber donde están instaladas en el servidor en
producción. Y el script creo que debe basarse sólo en lo que genere dinámicamente, sin tener que tocar para
nada recursos de disco que en principio podemos desconocer. Con eso lo hacemos más rápido y más fiable
al no depender de recursos externos. La imagen final ni siquiera se guarda en archivo, como veremos más
abajo.
Se puede actuar sobre la resolución del trazo de los caracteres, usando un tamaño de fuente más pequeño y
luego escalando la imagen al presentarla. Cuanto más pequeña sea la imagen generada más robuto será el
CAPTCHA, pues al aplicar luego el escalado la imagen pierde resolución. El ajuste estará en el punto donde
no pierda usabilidad. La fuente GD sólo permite tamaños 1 a 5. En los ejemplos uso el tamaño máximo 5 y
un escalado (o zoom) de 2. Pero es cuestión de hacer pruebas. Por ejemplo, con fuente 2 y zoom 5 la
imagen obtenida tiene poca resolución y en muchos casos resulta ilegible:
11/7/2014 Cómo se hace un formulario de contacto: Uso de CAPTCHA
http://www.wextensible.com/como-se-hace/formulario-contacto/captcha.html 7/12
4. Mejorando la usabilidad del CAPTCHA
Hay que tener en cuenta que el que uno vea bien los caracteres no significa que sea así para otras
personas. Cuando hacemos pruebas nos vamos acostumbrando a resolver los CAPTCHAs, por lo que será
conveniente hacer un muestreo y comprobar que personas ajenas al desarrollo web pueden resolverlos en
una proporción aceptable. El script acompaña una opción para guardar un número determinado de archivos
de imágenes. Extraje 25 CAPTCHAs y los inserte en esta página para comprobar la usabilidad. Le he pedido
a algunas personas para que me den las soluciones. Aunque no es una demostración muy rigurosa dado el
bajo número de encuestados, he podido extraer algunas conclusiones.
Usar un conjunto de letras mayúsculas y minúsculas ofrece mayor robustez pero dificulta la usabilidad. Por
ejemplo, a veces no es fácil diferenciar las letras "P" mayúscula y "p" minúscula. No conviene usar
minúsculas o mayúsculas solamente, pues hay muchas letras que son diferentes en ambos conjuntos lo que
supone una ventaja. Es mejor usar las dos formas y mejorar la usabilidad asimilando letras como las del
ejemplo anterior.
Por lo tanto la mejora de la usabilidad se refieren a caracteres que tienen un gran parecido. Son 0, o, O,
es decir, el cero y las "oes", el dígito "1" con la letra "l" minúscula, el dígito "5" con la letra "S"
mayúscula y algunas cuyas formas minúscula y mayúscula pueden ser confundidas. En estos casos prefiero
mantener esos caracteres en la lista de los posibles y admitir el intercambio entre ellos. Por ejemplo, si hay
un cero y alguién ve una O mayúscula, o al revés, se dará por bueno.
Hay otra mejora de usabildad relacionada con el posicionamiento. Vea estas imágenes:
11/7/2014 Cómo se hace un formulario de contacto: Uso de CAPTCHA
http://www.wextensible.com/como-se-hace/formulario-contacto/captcha.html 8/12
Se generaron sin usar la opción de no cerrar las letras "C, c, G, q". En este caso cuando la letra C es
seguida de alguna otra como "H" o "E" la anterior no queda bien definida, pareciéndose a la letra "O" o un
cero. Con la opción de no cerrar esas letras obligamos a que la siguiente se desplace verticalmente.
Forzando el script para usar la misma cadena de la última imagen "aVsCEL", vemos que en todos los casos
desplaza la siguiente letra "E" verticalmente para que la "C" no pierda legibilidad:
5. Mejorando la robustez del CAPTCHA
El factor de acercamiento horizontal entre caracteres es clave para dar mayor robustez al CAPTCHA. Lo ideal
sería que se solapen, pero se perdería legibilidad. Para posicionar un caracter tomamos la última posición
horizontal del anterior, le sumamos un ancho de la fuente y luego reducimos algo para que queden lo más
pegados posible:
$x += $ancho_fuente * (1 - $pegarx * $pegarmas);
El factor $pegarx tiene un valor de 1/10. Así el siguiente caracter se separa un ancho de la fuente y se
reduce luego una décima parte. La última imagen del apartado anterior se generó con ese factor. Si lo
modificamos a 1/5 y forzamos la misma cadena "aVsCEL" (aunque variarán los otros parámetros como
posición vertical y deformación) obtenemos una de las imágenes así:
11/7/2014 Cómo se hace un formulario de contacto: Uso de CAPTCHA
http://www.wextensible.com/como-se-hace/formulario-contacto/captcha.html 9/12
Los caracteres se pegan más entre sí, pero la "s" minúscula pierde legibilidad. El otro factor de
acercamiento es $pegarmas. Es el que ya mencioné para pegar aún más algunos caracteres más estrechos,
como "i,l,1". Tiene un valor de 2.5 que al multiplicarlo por el otro factor de 0.1 queda en 0.25, con lo que
acercamos más esos caracteres estrechos.
Hay un filtro para deformación llamado superficialización que consiste en aligerar el trazo de los caracteres.
Esto deforma aún más cada caracter. Con la misma cadena de antes y a con un factor de pegado de 1/10
tenemos este ejemplo:
Hay más cosas que podríamos hacer para mejorar la robustez. Como usar otras deformaciones como el
tachado u otros filtros. También podríamos utilizar ángulos aleatorios para situar cada caracter con fuentes
True Type. Esto lo podríamos hacer con la función de PHP imagettftext().
6. Estructura de la página de prueba del CAPTCHA
La página de prueba contiene un script PHP con un formulario que se remite al mismo script para validar el
CAPTCHA. No voy a exponer todo el código aquí, pues si lo desea puede ver el código completo. Presentaré
la parte del script PHP que va antes del HTML, aunque omitiendo algunas cosas para abreviar:
session_start(); //Declaramos variables para configurar el CAPTCHA...//Declaramos variables para recibir el formulario
11/7/2014 Cómo se hace un formulario de contacto: Uso de CAPTCHA
http://www.wextensible.com/como-se-hace/formulario-contacto/captcha.html 10/12
$texto = "";$texto_verif = "";$nombre = "";$email = "";$mensaje = "";$verificado = false;$primera_sesion = true;if (isset($_SESSION["texto"]) && ($_SESSION["texto"]!="") && 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 "texto-verif": $texto_verif = preg_replace("/[ \s]+/", "", $valor); break; } } $texto1 = $texto_verif; $texto2 = $_SESSION["texto"]; ... if ($texto1 == $texto2) { //Si se verifica destruimos sesión $verificado = true; $_SESSION = array(); if (ini_get("session.use_cookies")) { $params = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $params["path"], $params["domain"], $params["secure"], $params["httponly"] ); } session_destroy(); //Aquí va el proceso del formulario, enviar por email //por ejemplo o remitir a otra página } else { $texto_verif = ""; $texto = ""; }
11/7/2014 Cómo se hace un formulario de contacto: Uso de CAPTCHA
http://www.wextensible.com/como-se-hace/formulario-contacto/captcha.html 11/12
}if (!$verificado){ //Aquí construimos el CAPTCHA ... //La cadena de texto generada se guarda en la variable de sesión if (!isset($_SESSION["texto"])) { $primera_sesion = true; } else { $primera_sesion = false; } $_SESSION["texto"] = $texto; ... //Creamos imagen GD y posicionamos los caracteres. Luego aplicamos filtros para deformar $imagen = imagecreate($ancho_imagen, $alto_imagen); ... //Luego abrimos un búfer para extraer la imagen codificada en base64 ob_start(); imagepng($imagen); imagedestroy($imagen); $buffer = ob_get_clean(); $imagen_data = base64_encode($buffer); }
Abrimos una sesión con session_start(). Establecemos los valores iniciales de las variables y si ya existe
una sesión, buscamos en el GET si hay algo recibido del formulario. El texto a verificar viene en el campo
texto-verif y le quitamos todos los espacios. Comprobamos que es igual que el texto que tenemos
almacenado en $_SESSION["texto"]. Si se verifica destruimos completamente esa sesión. En ese punto
podemos remitir el proceso a otro destino, por ejemplo ejecutar el envío por email o redireccionar a otra
página. En este ejemplo sacamos el resultado en la misma página controlando que $verificado = true.
Esa página de prueba no tiene ninguna medida de seguridad para formularios, pues su único propósito es probar la extensiónGD en localhost para generar CAPTCHA. Para una versión definitiva se debería tener en cuenta cosas como lo expuesto enformularios seguros.
11/7/2014 Cómo se hace un formulario de contacto: Uso de CAPTCHA
http://www.wextensible.com/como-se-hace/formulario-contacto/captcha.html 12/12
Si !$verificado (será también la situación inicial) pasamos a construir el CAPTCHA. Generamos
aleatoriamente la cadena de texto y la guardamos en $_SESSION["texto"]. Luego creamos una imagen GD
con la función imagecreate() para posicionar en ella los caracteres y aplicar los filtros de deformación con
imagerotate() e imagefilter(). Finalmente tenemos la imagen en la variable $imagen, pero aún no
podemos usarla en el HTML que iría a continuación.
La función imagepng($imagen, $archivo) convierte esa imagen en un archivo con formato png. Pero si no
queremos usar recursos de disco, es mejor abrir un búfer y lanzar la imagen en él. Con ob_start() lo
abrimos y entonces con imagepng($imagen) lo que hacemos es enviarlo a ese búfer. Liberamos la memoria
destruyendo la variable y luego volcamos todo lo que hay en el búfer con $buffer=ob_get_clean().
Finalmente lo codificamos en base 64 con $imagen_data=base64_encode($buffer). Así tenemos la imagen
en forma de texto plano que luego insertaremos en el elemento <img> quedando su atributo como
src="data:image/png;base64,<?php echo $imagen_data;?>. La imagen no supera los 2KB, por lo que
codificarlas en base64 y enviarlas con la página no supone un coste excesivo. Y además no hay que tocar
recursos de disco en operaciones intermedias.
11/7/2014 Cómo se hace un formulario de contacto: Un mailer con PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/mail-user-agent.html 1/7
27
DIC
2011
Un Mail User Agent con PHP cuando mail() no está disponible
Inicio» Cómo se hace» I. El formulario de contacto c...» II. Incorporando seguridad al f...» III. Evitar el uso automático
de...» IV. Un Mail User Agent con ...
1. Hacer un mailer con PHP
2. Una clase en PHP para crear un Agente de Usuario SMTP
3. Conversación SMTP
1. Hacer un 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
wextensible
11/7/2014 Cómo se hace un formulario de contacto: Un mailer con PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/mail-user-agent.html 2/7
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.
2. 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";
11/7/2014 Cómo se hace un formulario de contacto: Un mailer con PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/mail-user-agent.html 3/7
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:
<?phprequire "smtp.php";
11/7/2014 Cómo se hace un formulario de contacto: Un mailer con PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/mail-user-agent.html 4/7
$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>
11/7/2014 Cómo se hace un formulario de contacto: Un mailer con PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/mail-user-agent.html 5/7
<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í:
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í:
11/7/2014 Cómo se hace un formulario de contacto: Un mailer con PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/mail-user-agent.html 6/7
3. Conversación SMTP
El protocolo SMTP y lo necesario para hacer un Agente de Usuario de email se basa en las siguientes
especificaciones :
RFC5321 Simple Mail Transfer Protocol Protocolo simple de transferencia de correo. Actualiza la versión
anterior RFC2821, que a su vez actualizó la RFC821
RFC5322 Internet Message Format Formato de mensajes de Internet. Actualiza las versiones RFC2822
y ésta a su vez a RFC822.
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 +0000Conectando a smtp.localemail......220 smtp.localemail ESMTP server ready.EHLO smtp.localemail250-smtp.localemail Hello smtp.localemail; ESMTPs are:250-TIME250-SIZE 0
11/7/2014 Cómo se hace un formulario de contacto: Un mailer con PHP
http://www.wextensible.com/como-se-hace/formulario-contacto/mail-user-agent.html 7/7
250-8BITMIME250 HELPMAIL FROM:<[email protected]>250 Sender OK - send RCPTs.RCPT TO:<[email protected]>250 Recipient OK - send RCPT or DATA.DATA354 OK, send data, end with CRLF.CRLFFrom: [email protected]: [email protected]: Probando SMTPSubject: pruebaDate: Thu, 15 Dec 2011 13:28:03 +0000
Mensaje.250 Data received OK.QUIT221 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.