Files
fn_registry/python/functions/cybersecurity/guess_email_formats.py
T
egutierrez eb8dbf66a1 feat(infra): auto-commit con 88 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-11 00:16:46 +02:00

85 lines
3.4 KiB
Python

"""Genera candidatos de email a partir de nombre, apellidos y dominio.
Funcion pura de OSINT pasivo: produce los patrones de direccion de correo
mas habituales en organizaciones, sin tocar la red. Util como punto de
partida para verificacion posterior (MX, catch-all, validacion SMTP, etc.).
"""
import unicodedata
def _ascii_lower(text: str) -> str:
"""Normaliza un texto a ASCII en minusculas.
Translitera acentos y caracteres latinos (a, e, n, ...) a su forma
ASCII, pasa a minusculas y elimina cualquier caracter que no sea
alfanumerico. No introduce separadores (a diferencia de un slug).
Args:
text: texto de entrada (puede contener acentos, ñ, mayusculas).
Returns:
cadena ASCII en minusculas formada solo por [a-z0-9].
"""
# NFKD descompone los caracteres acentuados en base + diacritico.
decomposed = unicodedata.normalize("NFKD", text)
stripped = "".join(c for c in decomposed if not unicodedata.combining(c))
lowered = stripped.lower()
return "".join(c for c in lowered if c.isalnum())
def guess_email_formats(nombre: str, apellidos: str, dominio: str) -> list:
"""Genera candidatos de email comunes a partir de identidad y dominio.
Combina el nombre y los apellidos en los patrones de direccion mas
frecuentes (nombre.apellido, inicial+apellido, apellido.nombre, etc.),
normalizando acentos/ñ a ASCII y minusculas. Si hay dos apellidos
tambien genera variantes que los unen. Deduplica preservando el orden
de aparicion.
Args:
nombre: nombre de pila (puede incluir varios tokens separados por
espacio; se usa el primero como nombre principal).
apellidos: uno o dos apellidos separados por espacio.
dominio: dominio de correo sin arroba (ej. "empresa.com").
Returns:
lista de strings "<local>@<dominio>" con los candidatos, en el
orden en que se generan los patrones y sin duplicados.
"""
n = _ascii_lower(nombre.split()[0]) if nombre.split() else ""
apellido_tokens = [_ascii_lower(a) for a in apellidos.split() if _ascii_lower(a)]
a1 = apellido_tokens[0] if apellido_tokens else ""
a2 = apellido_tokens[1] if len(apellido_tokens) > 1 else ""
dom = dominio.strip().lstrip("@").lower()
ni = n[0] if n else ""
a1i = a1[0] if a1 else ""
apellido_full = "".join(apellido_tokens) # apellido1apellido2 unidos
locals_raw = [
n, # nombre
f"{n}.{a1}" if n and a1 else "", # nombre.apellido
f"{n}{a1}" if n and a1 else "", # nombreapellido
f"{ni}.{a1}" if ni and a1 else "", # n.apellido
f"{ni}{a1}" if ni and a1 else "", # napellido
f"{n}_{a1}" if n and a1 else "", # nombre_apellido
f"{a1}.{n}" if a1 and n else "", # apellido.nombre
f"{n}{a1i}" if n and a1i else "", # nombre+inicial_apellido
f"{n}.{apellido_full}" if n and a2 else "", # nombre.apellido1apellido2
f"{n}{apellido_full}" if n and a2 else "", # nombreapellido1apellido2
f"{n}.{a2}" if n and a2 else "", # nombre.apellido2
f"{ni}{apellido_full}" if ni and a2 else "", # n+apellidos unidos
]
seen = set()
out = []
for local in locals_raw:
if not local:
continue
email = f"{local}@{dom}"
if email not in seen:
seen.add(email)
out.append(email)
return out