Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
passson 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_tokendedicada. - No reemplaza al browser para el flujo interactivo del usuario (ver tabla arriba).
imap_save_draftno construye el MIME: recibe bytes RFC822 ya serializados; el caller los arma conemail.message.EmailMessage().as_bytes()(stdlib) o conemail_build_*+ serialización.
Gotchas
connes un objeto vivo dentro del dict: estas funciones se componen en heredocs/apps Python, NO porfn run(que no puede serializar el socket). Cerrar siempre conconn.logout().- UID, no número de secuencia: los seq se renumeran al borrar; los UID son estables
mientras no cambie
UIDVALIDITYdel buzón. - Gmail
\Deleted≠ borrar: marcar\Deletedsolo quita la etiqueta de la carpeta actual. Para borrar de verdad hay que mover a[Gmail]/Trashconimap_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_messagedecodifica 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.