Filtro de spam y remitentes
Motor explicable, niveles, listas de bloqueo y permitidos, aprendizaje y correos externos.
En esta página
BloomX clasifica el correo entrante con un motor de spam explicable: la puntuación (0–100) es la suma ponderada de señales, y cada señal tiene un identificador, un peso y un motivo legible en español e inglés. El administrador elige el nivel de filtrado, las listas de bloqueo y permitidos y la política de correos externos desde /admin/spam; cada usuario puede afinarlo en *Ajustes → Spam*.
Qué es y qué no es
Es un filtro de heurísticas + aprendizaje por usuario con autenticación SPF/DKIM/DMARC, no un servicio de reputación externa ni un antivirus (para adjuntos ver Seguridad). Las cifras de esta página se midieron sobre un corpus sintético escrito por el autor del motor: sirven para comparar niveles y detectar regresiones, no son una tasa de error en producción.
Qué ocurre al llegar un correo#
- Blocklist («ni entra»): antes de guardar nada se comprueban el remitente del sobre (Return-Path), el
Fromy el dominio de la firma DKIM contra la lista de bloqueo del dominio y la personal de cada destinatario. Si acierta: no se guarda el correo (ni cuerpo, ni adjuntos, ni el crudo), se responde200al proveedor para que no reintente, no se genera rebote (evita *backscatter*), se cuenta el acierto y se anota un evento sin contenido. El correo de la propia organización nunca se bloquea por lista. - Motor v2 por destinatario: señales de autenticación, cabeceras, contenido, enlaces, adjuntos y suplantación + contexto del buzón (contactos, hilos propios, historial) + aprendizaje del usuario.
- Lista de permitidos: entrega normal, salvo que falle la autenticación alineada (se marca *posible suplantación*) o haya un adjunto peligroso (se trata igual).
- Decisión por banda: entregar, entregar con advertencia o carpeta Spam. Se guarda
spamScore, el veredicto con los motivos (≤ 2 KB) y si es externo; las reglas v2 pueden leerspamScoreeisExternal.
Si el motor v2, la configuración o las tablas fallan, el correo nunca se pierde: se cae al comportamiento anterior (cabeceras X-Spam-* del origen y SPAM_SCORE_THRESHOLD). Las importaciones de buzón y otras ingestas internas no pasan por este filtro.
Señales y pesos#
Pesos base (antes del multiplicador de familia). Cada familia tiene un tope de puntos positivos para que una sola no baste por sí sola, y el contexto solo resta (suelo −40, y se recorta al 40 % si hay una señal grave: una cuenta conocida comprometida sigue siendo peligrosa). Varias familias independientes que coinciden suman un refuerzo (+8 con 3, +14 con 4).
| Familia | Tope | Señales (peso) |
|---|---|---|
| Autenticación | 55 | DMARC fail (30), SPF fail (18), DKIM fail (18), SPF softfail (8), sin resultados (8), sin alineación (10), ARC fail (8), sin cabecera de autenticación (6); resta: DMARC pass (−6, no en correo gratuito), ARC válido en reenvío (−8) |
| Cabeceras | 40 | Message-ID ausente (8) o de otro dominio (3), fecha ausente (6) / futura (8), Reply-To de otro dominio (8), sobre ≠ From (3), Received con IP literal (5) / anómalo (4), X-Mailer de envío masivo (8), TLD de riesgo en el remitente (14), ≥ 20 / ≥ 50 destinatarios (6 / 12), *bulk* sin baja (6); resta: List-Unsubscribe (−5, marca «promocional») |
| Contenido | 60 | Léxico es/en/pt por categoría: phishing (tope 34), estafa/premio (46), cripto (34), factura/envío falsos (24), farmacia (34), adultos (30), apuestas (26), préstamos (20), marketing agresivo (18), urgencia (8), fraude del director (30); densidad de frases (6/12/18), mayúsculas y !!!, texto oculto (12), solo imagen (10), base64 enorme (8), homoglifos (14), ancho cero (8), texto partido (8), asunto codificado (5), HTML roto (5), solo un enlace (6) |
| Enlaces | 55 | Host IP (22), homógrafo IDN (20) / punycode (8), dominio parecido a una marca (20), texto≠destino (18; 4 si es un rastreador conocido), credenciales embebidas (16), TLD de riesgo (12), puerto raro (8), acortadores (4–8), demasiados enlaces (5–10), redirección larga (5) |
| Adjuntos | 60 | Ejecutable o peligroso por contenido (55), macros (28), HTML/SVG (22), doble extensión (22), comprimido con contraseña citada en el cuerpo (28), comprimido (6), tipo real distinto del declarado (8) |
| Suplantación | 60 | Nombre de marca con dominio ajeno (28 + 10 sin autenticación), dominio parecido a una marca (30), dominio de marca con autenticación fallida (20), dominio propio falsificado (30), nombre con otra dirección (22; 32 si es la propia) o dominio (15; 25), rol de organización en cuenta gratuita (14), permitido pero suplantado (40) |
| Origen | 50 | X-Spam-Flag/Status/Score del proveedor como una señal más (30–45) |
| Contexto | −40 | Remitente en contactos (−25), ya le respondiste (−20), responde a un mensaje tuyo (−25), historial legítimo (hasta −20); historial de spam (hasta +25); primera vez solo suma (+4/+7) cuando ya hay otras señales |
| Aprendizaje | ±20 | Clasificador bayesiano por usuario (ver más abajo) |
Las marcas vigiladas (PayPal, Microsoft, Google, Apple, Amazon, bancos, transportistas, etc.) se detectan en el nombre visible solo si coincide con la marca o con «marca + palabra de servicio» (p. ej. «PayPal Seguridad»), para no marcar «Apple Valley Dental». Los parecidos usan distancia de edición 1–2 sobre etiquetas de ≥ 6 letras, plegado de leet (paypa1) y homoglifos.
Niveles, bandas y acciones#
| Nivel | Umbral | Uso |
|---|---|---|
| Desactivado | — | No puntúa (la lista de bloqueo sigue funcionando) |
| Bajo | 80 | Casi sin falsos positivos; deja pasar spam dudoso |
| Equilibrado (por defecto) | 65 | Punto de partida recomendado |
| Estricto | 50 | Buzones con mucho spam |
| Máximo | 35 | Solo si aceptas revisar la carpeta Spam a menudo |
| Personalizado | 1–100 | Umbral numérico avanzado (también lo usa SPAM_SCORE_THRESHOLD si no hay configuración guardada) |
Tres bandas: *limpio* (< umbral − 15), *sospechoso* (entre umbral − 15 y umbral) y *spam* (≥ umbral). Acción por banda: entrega normal, entrega con advertencia (banner «Por qué» en el lector) o carpeta Spam. «Rechazar» no existe por puntuación: solo la lista de bloqueo explícita impide que un correo entre. Si el dominio lo permite, cada usuario puede mover el umbral ±1 nivel (±10 puntos).
Medido sobre el corpus sintético (385 legítimos, 410 spam/phishing; es/en/pt) con el contexto del buzón:
| Nivel | Falsos positivos | Detección (carpeta Spam) | Detección incl. sospechosos |
|---|---|---|---|
| Bajo (80) | 0 % | 69,5 % | 90,5 % |
| Equilibrado (65) | 0 % | 90,5 % | 96,8 % |
| Estricto (50) | 0 % | 96,8 % | 99,8 % |
| Máximo (35) | 2,1 % | 99,8 % | 100 % |
Sin ningún contexto del destinatario (peor caso), Equilibrado baja al 80,7 % con 0 % de falsos positivos. El spam difícil (autenticación válida desde dominios desechables, textos mínimos, ofuscación) se detecta menos: es la razón de la banda sospechosa y del aprendizaje.
Listas de bloqueo y permitidos#
- Tipos: dirección exacta, dominio (con opción de subdominios), comodín
*@dominio, TLD y expresión regular acotada (la misma defensa que las reglas: sin cuantificadores anidados, sin referencias hacia atrás ni *lookaround*, ≤ 200 caracteres) evaluada sobre la dirección completa. - Campos: motivo, caducidad opcional, contador de aciertos y último acierto. Tope 10 000 entradas por lista (2 000 en la whitelist de externos), con validación y deduplicado atómico (índice único), también con altas concurrentes.
- Importar / exportar CSV (
type,value,include_subdomains,reason,expires_at; ≤ 2 MB y 10 000 filas; el informe indica la línea de cada error; la exportación neutraliza fórmulas de hoja de cálculo). - Guardas: no se puede bloquear el propio dominio de la instancia (ni un padre con subdominios, ni un TLD o regex que lo cubra) ni las direcciones de los administradores (
ADMIN_EMAILS). - Permitidos no eluden la seguridad: si un remitente permitido falla DMARC/DKIM alineados se marca *posible suplantación* y se trata como sospechoso; los adjuntos peligrosos se bloquean igual.
Aprendizaje por usuario#
Las acciones Es spam / No es spam se enganchan en el servidor (al mover a o desde la carpeta Spam, sea cual sea la interfaz) y alimentan un clasificador bayesiano ligero: tokens del asunto, la vista previa y el dominio del remitente, hash truncado de 32 bits, suavizado de Laplace y combinación por log-odds de los 15 términos más informativos. La contribución al score está acotada a ±20 y solo cuenta con ≥ 8 marcas y ≥ 3 términos conocidos. El modelo guarda 5 000 tokens por usuario (expulsión LRU). Además se llevan contadores por remitente y dominio (spam / legítimo) que ajustan la confianza.
En *Ajustes → Spam* el usuario puede desactivar «Aprender de mis marcas» (activado por defecto) y borrar lo aprendido. Las marcas no leen el cuerpo completo (viven en el almacenamiento de objetos): usan asunto y vista previa, por eso el modelo es deliberadamente conservador.
Correos externos#
Se consideran internos los dominios propios (TOP_DOMAIN y el host de NEXT_PUBLIC_APP_URL), los «dominios internos adicionales» que añada el administrador y el dominio del destinatario. El resto son externos, salvo los de una whitelist propia de confiables (dominio y personal). Política (todo opcional): aviso en el lector (estilo información o advertencia, texto propio por idioma, plano y saneado), insignia «Externo» en la lista, etiqueta [EXTERNO] solo visual en el asunto de la lista (nunca se modifica el asunto guardado), aviso reforzado cuando el nombre visible coincide con un compañero o con el dominio propio pero la dirección es externa (la whitelist no lo silencia), aviso de primera vez, confirmación antes de abrir enlaces a un dominio distinto del remitente y aviso antes de descargar adjuntos. En hilos el aviso sale por mensaje y se puede cerrar por mensaje.
Consola: /admin/spam#
| Pestaña | Qué hace |
|---|---|
| Nivel de filtrado | Presets, umbral, acción por banda, multiplicador 0–2 por familia, interruptores del motor y vista previa: «con este ajuste, de tus últimos 200 correos evaluados X habrían ido a spam» (usa las señales guardadas, no relee cuerpos) |
| Bloqueados / Permitidos | Tablas con búsqueda, filtros, orden, paginación, alta, borrado, CSV y contador frente al tope |
| Correos externos | Política, textos, dominios internos adicionales y whitelist |
| Registro | Decisiones (entregado / advertencia / spam / bloqueado / «No es spam» / marcado) con puntuación y motivos, sin contenido. Retención configurable (30 días por defecto) |
| Probar | Pega cabeceras y texto (o elige un correo propio) y ve la puntuación por señal, sin guardar nada ni contar aciertos |
| Estadísticas | Spam, bloqueados, advertencias y externos por día, principales dominios y falsos positivos reportados |
Todas las rutas /api/admin/spam/** pasan por requireAdmin, limitan la tasa de peticiones, validan con esquemas estrictos (400 sin repetir lo recibido) y auditan los cambios (admin.spam.*). La configuración es AdminSetting spamConfig (JSON versionado y saneado) con caché de ≤ 30 s e invalidación por versión en la base de datos, como la cuota; las instancias ven un cambio en ≤ 5 s.
Datos#
Tablas aditivas creadas por npm run db:ensure (IF NOT EXISTS); sin ellas todo cae al comportamiento anterior.
| Objeto | Contenido |
|---|---|
SpamList | Listas: scope (domain/user), ownerKey, kind (allow/block/external), matchType, value, includeSubdomains, reason, expiresAt, createdBy, hits, lastHitAt; índice único contra duplicados |
SpamToken | Modelo bayesiano: userId, tokenHash, spam, ham, updatedAt; la fila __n guarda los contadores de mensajes |
SpamSender | Contadores de marcas por remitente (e:dirección) y dominio (d:dominio) |
SpamEvent | Registro sin contenido: remitente, destinatario, decisión, puntuación, regla y motivos (ids) |
Email.spamScore, Email.spamReasons, Email.isExternal | Columnas aditivas (SQL crudo): puntuación, veredicto ≤ 2 KB e indicador de externo |
Cómo afinar#
- Empieza en Equilibrado y revisa una semana el Registro y la carpeta Spam de un par de buzones.
- Si se cuelan correos: sube a Estricto o, mejor, bloquea el dominio/TLD concreto; prueba antes con Probar y la vista previa.
- Si caen legítimos: añádelos a Permitidos (no eluden la suplantación) o baja a Bajo; pide a los usuarios pulsar No es spam (enseña al modelo).
- Usa la banda sospechosa con advertencia para no perder nada mientras calibras.
- Ajusta un multiplicador de familia solo si ves un patrón (p. ej.
contenta 0,5 si tus propios boletines puntúan alto).
Límites conocidos#
- La evaluación usa las cabeceras que entrega el webhook (y las que aporta la API de recepción de Resend); si ninguna trae
Authentication-Results, no hay señales de autenticación (soloauth.none). - El contenido se analiza sobre el texto/HTML que recibe el webhook; los adjuntos grandes se descargan de forma asíncrona, así que su contenido lo valida el antivirus opcional y el validador de tipo, no el motor (sí se puntúan nombre y extensión).
- El aprendizaje usa asunto y vista previa (no el cuerpo completo) y necesita marcas propias del usuario para tener efecto.
- Las listas por regex se evalúan sobre la dirección, no sobre el asunto ni el cuerpo.
- No hay reputación global de IP/dominio ni consultas DNSBL (no se contacta con servicios externos).