763e06c127
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
120 lines
4.3 KiB
Python
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
|