Files
fn_registry/docs/capabilities/email.md
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

8.1 KiB

Email — Gestionar cuentas de correo por IMAP + SMTP (tecnología propia)

Tag: email. Grupo de funciones Python (solo stdlib: imaplib, smtplib, email) para leer, hacer CRUD y enviar correo hablando los protocolos directamente — sin browser CDP y sin el MCP Gmail de claude.ai. Es la base de un sistema multi-proveedor de gestión de cuentas: una conexión IMAP por buzón + SMTP para envío, con las credenciales resueltas desde pass/vault por la capa de aplicación.

Filtro MCP: mcp__registry__fn_search query="" tag="email".

Cuándo usar este grupo (y cuándo NO)

Caso Vía
Leer/buscar/clasificar/mover/borrar/enviar correo de forma programática y fiable, multi-cuenta Este grupo (IMAP+SMTP directo).
Leer correo interactivo del usuario en su sesión (códigos de verificación al instante en su Gmail logueado) Browser MCP sobre Gmail web (perfil 9222). Ver memoria correos-por-browser-no-mcp-gmail.
El MCP Gmail de claude.ai queda descartado en ambos casos (indexa con latencia).

IMAP directo no sustituye al browser para el flujo interactivo del usuario; lo complementa para automatización fiable con credenciales propias.

Autenticación

Usuario + app-password (NO OAuth). Gmail exige 2FA activado y un App Password de 16 chars (myaccount.google.com/apppasswords). Otros proveedores con IMAP/SMTP clásico (Dovecot, dominio propio) aceptan user+pass directo. La credencial se guarda en pass (email/<cuenta>-apppass) y la resuelve la capa app, nunca se hardcodea ni se pasa a estas funciones desde el código del registry.

Outlook/Hotmail/Office365 NO entran por aquí: Microsoft desactivó basic auth para IMAP/SMTP; requieren OAuth2 (pista aparte, no cubierta por este grupo hoy).

Servidores comunes

Proveedor IMAP SMTP
Gmail imap.gmail.com:993 (SSL) smtp.gmail.com:465 (SSL) o 587 (STARTTLS)
Dominio propio (Dovecot+Postfix) mail.<dominio>:993 mail.<dominio>:465/587

Funciones del grupo

Núcleo IMAP — el primer argumento conn de toda operación es el objeto imaplib.IMAP4_SSL vivo que produce imap_connect. Todas operan por UID (estable), nunca por número de secuencia, y devuelven dict {"status": "ok"|"error", ...} sin lanzar.

ID Firma corta Qué hace
imap_connect_py_infra imap_connect(host, port=993, user, password, mailbox='INBOX', use_ssl=True, timeout_s=30) -> dict Abre IMAP4_SSL, login + select(mailbox), devuelve el conn vivo + num_messages. Impura.
imap_list_mailboxes_py_infra imap_list_mailboxes(conn) -> dict Lista carpetas decodificando modified-UTF-7 (Gmail: [Gmail]/Sent Mail, etc.). Impura.
imap_search_py_infra imap_search(conn, criteria='UNSEEN', mailbox='') -> dict Busca por criterio IMAP crudo (UNSEEN, FROM, SINCE…) y devuelve UIDs. Impura.
imap_fetch_message_py_infra imap_fetch_message(conn, uid, mark_seen=False) -> dict Baja y parsea un mensaje (from/to/cc/subject/date/body_text/body_html/attachments). BODY.PEEK no marca leído. Impura.
imap_mark_seen_py_infra imap_mark_seen(conn, uid, seen=True) -> dict Añade/quita la bandera \Seen. Impura.
imap_move_message_py_infra imap_move_message(conn, uid, dest_mailbox) -> dict Mueve por UID (UID MOVE RFC 6851, fallback COPY+EXPUNGE). Impura.
imap_delete_message_py_infra imap_delete_message(conn, uid, expunge=True) -> dict Marca \Deleted y opcionalmente EXPUNGE. Impura.
imap_save_draft_py_infra imap_save_draft(conn, raw_rfc822, mailbox='[Gmail]/Drafts', flags='\Draft') -> dict Guarda un borrador (bytes MIME) vía APPEND. Impura.

Construir + enviar (SMTP):

ID Firma corta Qué hace
email_build_html_py_infra email_build_html(from_addr, to, subject, body_html) -> EmailMessagePy Construye un mensaje HTML inmutable. Pura.
smtp_send_py_infra smtp_send(cfg, from_addr, to, subject, body_html='', body_text='', cc, bcc, attachments, headers) -> None Conecta SMTP, arma MIME y envía en un paso (TLS/STARTTLS/claro). Impura.

Ejemplo canónico end-to-end

Conectar a Gmail con app-password resuelto desde pass, listar no leídos, leer el primero, marcarlo leído, y enviar una respuesta. Las funciones se componen en un heredoc Python que importa del registry (no reescribe protocolo):

import sys, os, subprocess
sys.path.insert(0, os.path.join("python", "functions"))
from infra.imap_connect import imap_connect
from infra.imap_search import imap_search
from infra.imap_fetch_message import imap_fetch_message
from infra.imap_mark_seen import imap_mark_seen
from infra.smtp_send import smtp_send, SMTPConfigPy

EMAIL = "gutierenmanuel15@gmail.com"
# Credencial desde pass (o usar pass_get_secret del registry). NUNCA hardcodear.
PW = subprocess.run(["pass", "show", "email/gmail-enmanuel-apppass"],
                    capture_output=True, text=True).stdout.splitlines()[0]

# 1. Conectar (IMAP) — el conn vivo viaja dentro del dict
c = imap_connect(host="imap.gmail.com", port=993, user=EMAIL, password=PW, mailbox="INBOX")
assert c["status"] == "ok", c
conn = c["conn"]

# 2. Buscar no leídos y leer el primero (PEEK: no marca leído)
s = imap_search(conn, criteria="UNSEEN")
print("no leídos:", s["count"])
if s["uids"]:
    uid = s["uids"][0]
    m = imap_fetch_message(conn, uid)["message"]
    print(m["from"], "—", m["subject"])
    imap_mark_seen(conn, uid)                       # marcar leído

# 3. Enviar (SMTP) — mismo app-password
smtp_send(
    SMTPConfigPy(host="smtp.gmail.com", port=465, username=EMAIL, password=PW, tls_mode="tls"),
    from_addr=EMAIL, to=["dest@example.com"],
    subject="Probando IMAP+SMTP propios", body_text="Enviado sin browser, protocolo directo.",
)
conn.logout()                                       # cerrar siempre

Fronteras

  • No gestiona la cuenta multi-proveedor: estas son primitivas de protocolo. El registro de N cuentas (host/port/auth_type por buzón) y la resolución de credenciales desde pass son responsabilidad de una app (p. ej. apps/mail_manager), no de este grupo.
  • No hace OAuth: solo user+app-password. Outlook/Office365 (basic auth muerto) quedan fuera hasta que exista una función *_oauth_token dedicada.
  • No reemplaza al browser para el flujo interactivo del usuario (ver tabla arriba).
  • imap_save_draft no construye el MIME: recibe bytes RFC822 ya serializados; el caller los arma con email.message.EmailMessage().as_bytes() (stdlib) o con email_build_* + serialización.

Gotchas

  • conn es un objeto vivo dentro del dict: estas funciones se componen en heredocs/apps Python, NO por fn run (que no puede serializar el socket). Cerrar siempre con conn.logout().
  • UID, no número de secuencia: los seq se renumeran al borrar; los UID son estables mientras no cambie UIDVALIDITY del buzón.
  • Gmail \Deleted ≠ borrar: marcar \Deleted solo quita la etiqueta de la carpeta actual. Para borrar de verdad hay que mover a [Gmail]/Trash con imap_move_message.
  • Nombres de carpeta Gmail llevan prefijo [Gmail]/ ([Gmail]/Sent Mail, [Gmail]/Drafts, [Gmail]/Trash, [Gmail]/Spam).
  • App-password requiere 2FA activado en la cuenta Google; sin 2FA no se puede generar.
  • Charsets: imap_fetch_message decodifica RFC 2047 en cabeceras y respeta el charset de cada parte del cuerpo; aun así correos malformados pueden traer texto degradado.

Prerequisitos

  • python/.venv (solo stdlib, sin dependencias nuevas).
  • App-password de cada cuenta guardado en pass (email/<cuenta>-apppass).
  • 2FA activado en las cuentas Google.