"""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": , "mailbox": , "num_messages": , # mensajes en el buzon (respuesta de SELECT) } En fallo (host vacio, auth invalida, red, buzon inexistente):: {"status": "error", "error": } """ 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