nota técnica
Un formulario de contacto sin una sola dependencia
El formulario de Hablemos manda correo de verdad. Para hacerlo no hay instalada ninguna librería: el proyecto entero tiene cero dependencias de npm en tiempo de ejecución, y eso incluye el envío de correo.
Cada dependencia externa que se añade a un proyecto trae consigo una cierta carga que hay que mantener. Si se puede obtener lo mismo sin dependencies y con esfuerzo menor, a la larga paga dividendos.
El SDK que no se instaló
El proveedor de correo publica su SDK oficial. Revisando la documentación de dicho SDK,
es una llamata http simple: una petición POST con una cabecera de autorización y
un objeto JSON. La plataforma ya trae fetch.
POST https://api.resend.com/emails
Authorization: Bearer <clave>
{ "from": ..., "to": ..., "subject": ..., "text": ..., "reply_to": ... }
El adaptador que envuelve esa llamada ocupa un archivo. Está aislado a propósito: cambiar de proveedor es reescribir ese archivo, sin tocar el formulario ni el resto del servidor.
La regla que aplicamos es la misma en todos los proyectos: antes de añadir una librería revisamos si ya lo hace el lenguaje, la plataforma o unas pocas líneas. Aquí lo hacía la plataforma.
Lo que valida el navegador es comodidad, no un control
El formulario valida en el navegador porque avisa antes de enviar y eso se agradece. No cuenta como seguridad: cualquiera puede mandar la petición directamente, sin pasar por la página.
Por eso el servidor lo vuelve a comprobar todo, con límites explícitos:
- el cuerpo entero se corta en 16 KB, antes incluso de leerlo completo;
- el correo, 254 caracteres, que es el máximo real de una dirección;
- el mensaje, 4 000 caracteres;
- lo que no cumple se responde con un
400y nunca llega al proveedor.
El detalle que evita que te secuestren el formulario
Un correo se compone de cabeceras separadas por saltos de línea. Si un valor que viene de fuera lleva un salto de línea dentro, deja de ser un valor y se convierte en una cabecera nueva. Así se secuestra un formulario de contacto para mandar spam en nombre de otro.
La defensa es aburrida y funciona: los valores de configuración se rechazan si
contienen un salto de línea, y la dirección de quien escribe nunca se usa como
remitente. Va en Reply-To, que es donde sirve para responder y donde
no controla nada.
Si falta una variable de configuración, la aplicación no arranca. Preferimos eso a que arranque a medias y el primer mensaje se pierda sin que nadie se entere.
Un límite de envíos que cabe en la memoria del proceso
Hay dos contadores:
- 5 envíos por dirección IP cada 10 minutos;
- 60 envíos por hora en total, como tope de la cuota del proveedor.
Al superarlos la respuesta es 429 con Retry-After, y el
mensaje no llega a salir. Los contadores viven en la memoria del proceso: no hay
Redis, ni base de datos, ni servicio externo.
Es suficiente porque el sitio corre en una sola máquina. Un almacén compartido habría sumado una dependencia sin comprar exactitud.
El límite que hay que decir en voz alta: con más de una instancia, cada una aplicaría su propio contador, y el límite efectivo sería el número por el de instancias. Está escrito en el propio módulo, para que quien escale el sitio lo lea antes de que le sorprenda.
La cabecera en la que solo se confía en el sitio correcto
Para contar por IP hay que saber la IP. Detrás de la plataforma de despliegue, la dirección del socket es la del proxy, y la del visitante viene en una cabecera que el proxy escribe.
El problema es evidente en cuanto se enuncia: una cabecera la puede inventar cualquiera. Si el servidor se la cree siempre, saltarse el límite es cambiar un valor en cada petición.
Así que esa cabecera solo se lee cuando el proceso está corriendo en esa plataforma, detectado por una variable que la propia plataforma inyecta. Fuera de ahí se ignora y se usa la dirección del socket.
La trampa que sí se dejó puesta
Hay un campo oculto que una persona nunca llena. Cuando llega con contenido, el
servidor responde 200 y no envía nada. El bot cree que lo consiguió y
no reintenta.
Sin CAPTCHA. Un CAPTCHA le cobra la molestia a todos los visitantes reales para resolver un problema que todavía no existe. Si algún día el spam pasa, entonces se evalúa.
Qué se gana con todo esto
- el navegador no carga ni un script del proveedor de correo;
- ninguna clave privada sale del servidor;
- cambiar de proveedor es tocar un archivo;
- la lista de dependencias que mantener dentro de tres años sigue vacía.