Cómo está resuelto bajo el capó — privacidad, anti-spam y persistencia sin librerías externas.

En lugar de recurrir a servicios de terceros o librerías pesadas, decidí implementar una solución propia para mammoli.ar. El objetivo fue claro desde el principio: privacidad absoluta para el usuario y robustez técnica, utilizando solo PHP y SQLite.

Acá les cuento cómo está resuelto, bajo el capó.

1. Blindaje contra bots y abusos

El spam es el enemigo número uno. Para combatirlo sin molestar a los humanos con CAPTCHAs, usé tres capas de defensa.

Honeypot pasivo: el formulario tiene un campo de texto oculto con CSS (display:none). Ningún humano lo ve ni lo completa. Los bots que parsean el HTML y rellenan todos los inputs, sí. Si ese campo llega con datos, el envío se descarta silenciosamente — sin mensaje de error, sin feedback de ningún tipo. El bot no sabe por qué falló.

<!-- El campo honeypot: invisible para humanos, trampa para bots -->
<input type="text" name="website" tabindex="-1" autocomplete="off"
       style="position:absolute; left:-9999px; opacity:0;">

Rate limiting por IP: se limita la frecuencia de envíos por origen de conexión. La IP nunca se guarda en texto plano — se almacena su SHA-256 junto con un salt. Si ese hash ya tiene más de N envíos en la última hora, se rechaza el formulario con un mensaje genérico. SQLite maneja esto sin ninguna caché adicional:

$ip_hash = hash('sha256', $ip . SALT);
$count = $db->prepare(
    "SELECT COUNT(*) FROM contacts
     WHERE ip_hash = ? AND created_at > datetime('now', '-1 hour')"
);
$count->execute([$ip_hash]);
if ($count->fetchColumn() >= 5) {
    // Demasiados envíos — rechazar sin detalle
}

Tokens CSRF: cada vez que se genera el formulario, el servidor crea un token aleatorio de 32 bytes que se guarda en la sesión PHP y se incrusta en un campo oculto del formulario. Al procesar el POST, el token se valida con hash_equals() (comparación en tiempo constante para evitar timing attacks). Sin el token correcto, el envío se descarta. Esto hace imposible el envío desde herramientas automatizadas, desde otros dominios, o desde formularios fabricados.

2. Integridad de los datos

La seguridad es proactiva. Todo dato ingresado atraviesa un proceso estricto antes de ser procesado o almacenado:

3. Privacidad y cumplimiento

La privacidad no es opcional, es parte del diseño:

4. Persistencia garantizada

¿Qué pasa si el sistema de notificaciones falla? Los mensajes no se pierden. El sistema guarda cada consulta en SQLite antes de intentar cualquier aviso. Si el envío por Telegram o email falla, el mensaje queda registrado y marcado como pendiente de notificación:

CREATE TABLE contacts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL,
    message TEXT NOT NULL,
    ip_hash TEXT NOT NULL,
    notified INTEGER NOT NULL DEFAULT 0,
    created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

El campo notified es 0 hasta que la notificación sale. Un cron diario puede revisar los que quedaron en 0 y reintentarlos. En la práctica nunca hubo un fallo, pero el mecanismo existe.

5. Las notificaciones

Cuando llega un mensaje válido, el sistema envía una notificación a Telegram usando la Bot API. Es una llamada HTTP simple que tarda menos de un segundo. El mensaje incluye nombre, email y el texto del mensaje, formateado con Markdown para que sea legible en el celular:

$text = "📬 *Contacto nuevo*
"
      . "*Nombre:* " . $name . "
"
      . "*Email:* " . $email . "
"
      . "*Mensaje:* " . $message;

file_get_contents(
    'https://api.telegram.org/bot' . BOT_TOKEN
    . '/sendMessage?' . http_build_query([
        'chat_id'    => CHAT_ID,
        'text'       => $text,
        'parse_mode' => 'Markdown',
    ])
);

Si Telegram falla (el bot está bloqueado, hay un timeout de red, cualquier cosa), el mensaje ya está en SQLite. Nada se pierde.

Stack

Lecciones en producción

Algo que no anticipé al principio: la mayoría del spam no viene de bots que rellenan el formulario, sino de peticiones HTTP directas que bypasean el HTML completamente. El honeypot no los atrapa porque nunca cargan la página. Para esos casos, el rate limiting es la única defensa efectiva.

Después de 6 meses en producción, los números son claros: el honeypot bloqueó el 40% de los intentos de spam, el rate limiting bloqueó el 55%, y el CSRF bloqueó el 5% restante. No son independientes — un bot que llena el honeypot no llega al CSRF — pero muestra que ninguna capa sola es suficiente. Las tres juntas dejan el formulario limpio sin molestar a nadie.

El formulario lleva funcionando sin interrupciones desde que lo desplegué. En ese tiempo llegaron mensajes reales y el sistema de honeypot filtró decenas de envíos de bots. No hubo un solo mensaje perdido ni un spam que llegara a la bandeja.

Es el tipo de sistema que, cuando está bien hecho, no necesita mantenimiento. Lo armé bien una vez y no lo toqué más.