Files
fn_registry/python/functions/infra/imap_move_message.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

100 lines
4.2 KiB
Python

"""Mueve un mensaje IMAP a otro mailbox por UID, con fallback COPY+EXPUNGE.
Funcion IMPURA: traslada un mensaje (identificado por su UID) del mailbox
seleccionado a `dest_mailbox` sobre una conexion `imaplib.IMAP4_SSL` ya
autenticada. Intenta primero el comando UID MOVE (RFC 6851, soportado por Gmail
y la mayoria de servidores modernos): es atomico y eficiente. Si el servidor NO
anuncia la capacidad MOVE, cae a la secuencia clasica equivalente: UID COPY al
destino, STORE +FLAGS (\\Deleted) en el origen y EXPUNGE para materializar el
borrado del origen.
Nunca lanza: devuelve un dict con `status` ("ok"/"error") y `method` para indicar
que camino se uso. No abre la conexion ni resuelve credenciales: el caller pasa
`conn` ya conectado, autenticado y con `conn.select("<mailbox origen>")` hecho.
"""
def _server_supports_move(conn) -> bool:
"""Devuelve True si la conexion anuncia la capability MOVE (RFC 6851).
Inspecciona ``conn.capabilities`` (tupla de capacidades que imaplib cachea
tras el login). La comparacion es case-insensitive porque distintos servidores
devuelven "MOVE" en mayusculas/minusculas. Cualquier problema accediendo a las
capacidades se trata como "no soportado" para forzar el fallback seguro.
"""
try:
caps = getattr(conn, "capabilities", ()) or ()
return any(str(c).upper() == "MOVE" for c in caps)
except Exception: # noqa: BLE001
return False
def imap_move_message(conn, uid: int, dest_mailbox: str) -> dict:
"""Mueve el mensaje `uid` del mailbox seleccionado a `dest_mailbox`.
Camino preferido (``method="move"``): ``conn.uid("MOVE", str(uid),
dest_mailbox)``, atomico. Si el servidor no soporta MOVE (no esta en
``conn.capabilities``) o el comando MOVE falla, se usa el fallback
(``method="copy_delete"``): ``UID COPY`` al destino, ``UID STORE +FLAGS
(\\Deleted)`` en el origen y ``EXPUNGE``.
Args:
conn: objeto ``imaplib.IMAP4_SSL`` (o ``IMAP4``) YA conectado,
autenticado y con el mailbox ORIGEN seleccionado (``conn.select(...)``).
uid: UID del mensaje en el mailbox origen. Operacion siempre por UID, no
por numero de secuencia.
dest_mailbox: nombre del mailbox destino. En Gmail los nombres llevan el
prefijo ``[Gmail]/`` (ej. ``"[Gmail]/Trash"`` para la papelera,
``"[Gmail]/Spam"``). Una carpeta de usuario es simplemente su nombre.
Returns:
dict. En exito: ``{"status": "ok", "uid": uid, "dest": dest_mailbox,
"method": "move" | "copy_delete"}``. En fallo (sin lanzar):
``{"status": "error", "error": str}``.
"""
try:
# Camino 1: UID MOVE (atomico) si el servidor lo anuncia.
if _server_supports_move(conn):
try:
typ, data = conn.uid("MOVE", str(uid), dest_mailbox)
if typ == "OK":
return {
"status": "ok",
"uid": uid,
"dest": dest_mailbox,
"method": "move",
}
# MOVE anunciado pero rechazado: caemos al fallback.
except Exception: # noqa: BLE001
# MOVE no soportado en la practica pese a la capability: fallback.
pass
# Camino 2 (fallback): COPY al destino + marcar \\Deleted en origen + EXPUNGE.
typ, data = conn.uid("COPY", str(uid), dest_mailbox)
if typ != "OK":
return {
"status": "error",
"error": f"imap_move_message: COPY a {dest_mailbox!r} devolvio {typ!r}: {data!r}",
}
typ, data = conn.uid("STORE", str(uid), "+FLAGS", "(\\Deleted)")
if typ != "OK":
return {
"status": "error",
"error": f"imap_move_message: STORE \\Deleted devolvio {typ!r}: {data!r}",
}
conn.expunge()
return {
"status": "ok",
"uid": uid,
"dest": dest_mailbox,
"method": "copy_delete",
}
except Exception as e: # noqa: BLE001
return {"status": "error", "error": str(e)}
if __name__ == "__main__":
print("imap_move_message: importable. Uso real requiere un conn IMAP autenticado.")