feat(browser): auto-commit con 178 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user