# 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/-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.:993` | `mail.: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](../../python/functions/infra/imap_connect.md) | `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](../../python/functions/infra/imap_list_mailboxes.md) | `imap_list_mailboxes(conn) -> dict` | Lista carpetas decodificando modified-UTF-7 (Gmail: `[Gmail]/Sent Mail`, etc.). Impura. | | [imap_search_py_infra](../../python/functions/infra/imap_search.md) | `imap_search(conn, criteria='UNSEEN', mailbox='') -> dict` | Busca por criterio IMAP crudo (UNSEEN, FROM, SINCE…) y devuelve UIDs. Impura. | | [imap_fetch_message_py_infra](../../python/functions/infra/imap_fetch_message.md) | `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](../../python/functions/infra/imap_mark_seen.md) | `imap_mark_seen(conn, uid, seen=True) -> dict` | Añade/quita la bandera `\Seen`. Impura. | | [imap_move_message_py_infra](../../python/functions/infra/imap_move_message.md) | `imap_move_message(conn, uid, dest_mailbox) -> dict` | Mueve por UID (UID MOVE RFC 6851, fallback COPY+EXPUNGE). Impura. | | [imap_delete_message_py_infra](../../python/functions/infra/imap_delete_message.md) | `imap_delete_message(conn, uid, expunge=True) -> dict` | Marca `\Deleted` y opcionalmente EXPUNGE. Impura. | | [imap_save_draft_py_infra](../../python/functions/infra/imap_save_draft.md) | `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](../../python/functions/infra/email_build_html.md) | `email_build_html(from_addr, to, subject, body_html) -> EmailMessagePy` | Construye un mensaje HTML inmutable. Pura. | | [smtp_send_py_infra](../../python/functions/infra/smtp_send.md) | `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): ```python 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/-apppass`). - 2FA activado en las cuentas Google.