feat(browser): auto-commit con 178 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:22:23 +02:00
parent 7d100e7f3e
commit 763e06c127
178 changed files with 19917 additions and 317 deletions
+100
View File
@@ -0,0 +1,100 @@
"""Busca mensajes en un buzon IMAP por criterio y devuelve sus UIDs.
Funcion IMPURA: hace I/O de red sobre una conexion `imaplib` viva (la produce
`imap_connect`). Opcionalmente cambia de buzon con `select(mailbox)` y luego
ejecuta `conn.uid("SEARCH", None, criteria)`.
Usa SIEMPRE UIDs (Unique IDentifiers), no numeros de secuencia: los UID son
estables dentro de un buzon mientras no cambie el UIDVALIDITY, mientras que los
numeros de secuencia se renumeran cuando se borran mensajes. Asi un UID
guardado sigue apuntando al mismo mensaje en una sesion posterior.
NUNCA lanza: devuelve un dict con `status` ("ok"/"error").
"""
def imap_search(conn, criteria: str = "UNSEEN", mailbox: str = "") -> dict:
"""Busca mensajes y devuelve la lista de UIDs que casan el criterio.
Si `mailbox` no esta vacio, hace `conn.select(mailbox)` antes de buscar.
Luego ejecuta `conn.uid("SEARCH", None, criteria)` y parsea la respuesta a
una lista de enteros (UIDs).
Args:
conn: objeto `imaplib.IMAP4[_SSL]` vivo y autenticado (de `imap_connect`).
criteria: expresion de busqueda IMAP cruda (RFC 3501 SEARCH). Ejemplos:
``"UNSEEN"`` (no leidos), ``"ALL"`` (todos),
``"FROM foo@bar.com"``, ``"SUBJECT factura"``,
``"SINCE 01-Jan-2026"``, ``"UNSEEN SINCE 01-Jun-2026"``,
``'HEADER Message-ID "<id@host>"'``.
mailbox: si no esta vacio, se selecciona ese buzon antes de buscar
(ej. ``"[Gmail]/Sent Mail"``). Vacio usa el buzon ya seleccionado.
Returns:
Dict de estado. En exito::
{"status": "ok", "uids": [123, 456, ...], "count": <int>}
En fallo (conn invalido, criterio mal formado, buzon inexistente)::
{"status": "error", "error": <str>}
"""
if conn is None:
return {"status": "error", "error": "imap_search: conn es None"}
if not criteria or not str(criteria).strip():
return {"status": "error", "error": "imap_search: criteria vacio"}
criteria = str(criteria).strip()
try:
if mailbox:
typ, data = conn.select(mailbox)
if typ != "OK":
reason = _first_str(data)
return {
"status": "error",
"error": f"imap_search: SELECT {mailbox!r} fallo: {reason}",
}
typ, data = conn.uid("SEARCH", None, criteria)
if typ != "OK":
reason = _first_str(data)
return {
"status": "error",
"error": f"imap_search: SEARCH {criteria!r} devolvio {typ}: {reason}",
}
uids = _parse_uids(data)
return {"status": "ok", "uids": uids, "count": len(uids)}
except Exception as exc: # noqa: BLE001 — contrato: nunca lanzar.
return {"status": "error", "error": f"imap_search: {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_uids(data) -> list:
"""Parsea la respuesta de SEARCH (lista con un bytes de UIDs separados por espacio)."""
uids: list[int] = []
if not data:
return uids
for chunk in data:
if chunk is None:
continue
if isinstance(chunk, bytes):
text = chunk.decode("ascii", errors="replace")
else:
text = str(chunk)
for token in text.split():
try:
uids.append(int(token))
except ValueError:
# Token no numerico (raro): lo ignoramos.
continue
return uids