Files
fn_registry/python/functions/infra/imap_connect.py
T
egutierrez 763e06c127 feat(browser): auto-commit con 178 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-20 18:22:23 +02:00

120 lines
4.3 KiB
Python

"""Abre y autentica una conexion IMAP (SSL por defecto) y selecciona un buzon.
Funcion IMPURA: hace I/O de red. Construye un `imaplib.IMAP4_SSL(host, port)`
(o `imaplib.IMAP4(host, port)` si `use_ssl=False`), hace `login(user, password)`
y `select(mailbox)`, y devuelve el objeto de conexion VIVO dentro del dict de
resultado para que las demas funciones del grupo (`imap_list_mailboxes`,
`imap_search`, `imap_fetch_message`) operen sobre el.
Es la primera pieza de un sistema propio (sin browser/CDP) de lectura de correo
multi-proveedor. La autenticacion es usuario + app-password (16 caracteres en
Gmail, o user+pass del proveedor): NO usa OAuth. Las credenciales NO se
resuelven aqui — las pasa la capa de aplicacion (via `pass`/vault).
NUNCA lanza: devuelve un dict con `status` ("ok"/"error"). En error el campo
`conn` no esta presente; el caller debe comprobar `status` antes de usar `conn`.
"""
import imaplib
def imap_connect(
host: str,
port: int = 993,
user: str = "",
password: str = "",
mailbox: str = "INBOX",
use_ssl: bool = True,
timeout_s: float = 30.0,
) -> dict:
"""Conecta, autentica y selecciona un buzon IMAP.
Abre el socket IMAP (SSL por defecto), hace `login` con usuario +
app-password y `select(mailbox)`. El objeto `imaplib.IMAP4[_SSL]` vivo se
devuelve dentro del dict para componer el resto de operaciones del grupo.
Args:
host: servidor IMAP (ej. ``"imap.gmail.com"``). Vacio -> status error.
port: puerto IMAP. Default 993 (IMAPS). Para STARTTLS/plano suele ser 143.
user: direccion de correo / usuario de la cuenta.
password: app-password (16 chars en Gmail) o contrasena del proveedor.
NO OAuth. Requiere 2FA activado para emitir app-passwords en Gmail.
mailbox: buzon a seleccionar tras autenticar. Default ``"INBOX"``.
use_ssl: True usa ``IMAP4_SSL`` (cifrado de extremo a extremo desde el
saludo). False usa ``IMAP4`` en claro (solo redes de confianza/test).
timeout_s: timeout del socket en segundos para conectar y operar.
Returns:
Dict de estado. En exito::
{
"status": "ok",
"conn": <imaplib.IMAP4_SSL vivo, autenticado y con mailbox seleccionado>,
"mailbox": <mailbox>,
"num_messages": <int>, # mensajes en el buzon (respuesta de SELECT)
}
En fallo (host vacio, auth invalida, red, buzon inexistente)::
{"status": "error", "error": <str>}
"""
if not host or not host.strip():
return {"status": "error", "error": "imap_connect: host vacio"}
host = host.strip()
conn = None
try:
if use_ssl:
conn = imaplib.IMAP4_SSL(host, int(port), timeout=float(timeout_s))
else:
conn = imaplib.IMAP4(host, int(port), timeout=float(timeout_s))
# login lanza imaplib.IMAP4.error si las credenciales son invalidas.
conn.login(user, password)
typ, data = conn.select(mailbox)
if typ != "OK":
# data suele traer el motivo (buzon inexistente, etc.).
reason = _first_str(data)
try:
conn.logout()
except Exception:
pass
return {
"status": "error",
"error": f"imap_connect: SELECT {mailbox!r} fallo: {reason}",
}
num_messages = _parse_int(data)
return {
"status": "ok",
"conn": conn,
"mailbox": mailbox,
"num_messages": num_messages,
}
except Exception as exc: # noqa: BLE001 — contrato: nunca lanzar.
if conn is not None:
try:
conn.logout()
except Exception:
pass
return {"status": "error", "error": f"imap_connect: {exc}"}
def _first_str(data) -> str:
"""Devuelve el primer elemento de una respuesta imaplib como str legible."""
if not data:
return ""
item = data[0]
if isinstance(item, bytes):
return item.decode("utf-8", errors="replace")
return str(item)
def _parse_int(data) -> int:
"""Parsea el numero de mensajes de la respuesta de SELECT (lista de bytes)."""
try:
return int(_first_str(data))
except (ValueError, TypeError):
return 0