mailbox: Tus Bandejas como API HTTP y Servidor MCP Que No Guarda Ni Un Byte

Tengo cinco cuentas de correo. Un Gmail personal que arrastro desde que era un crío, una dirección del trabajo, una de quemar para registros en los que no confío, una dirección en el dominio de este blog, y otra que había olvidado por completo hasta que encontré la contraseña en un fichero de texto. Cinco bandejas, cinco juegos de credenciales, cinco pestañas, cinco contadores de no leídos mintiéndome sobre lo mal que va mi vida. Todas las apps de “bandeja unificada” de este planeta quieren arreglarme eso quedándose con la custodia de mis cinco cuentas, sincronizándolas en su nube, pasando mi correo por su “categorización inteligente con IA” y devolviéndome una interfaz bonita a cambio de la única cosa que tengo y que de verdad merece protección: el contenido de mi bandeja.

No. A la mierda. Mi correo tiene extractos bancarios, códigos de recuperación 2FA, cosas médicas, contratos, y suficientes datos personales para destrozarme una década si se filtran. Eso no se lo entrego a ninguna startup de “inbox zero” hinchada de capital riesgo para que lo deje escapar por la inyección SQL de su becario dentro de dieciocho meses. Así que construí mailbox: un contenedor, un fichero YAML, una API HTTP y un servidor MCP en el mismo puerto, cero bytes de mi correo guardados en otro sitio que no sean los servidores de correo donde ya vive.

Cómo Te Folla Cualquier Bandeja Unificada

Todos los productos de “bandeja unificada” siguen el mismo guion, y es el mismo guion que cualquier empresa de SaaS aplica a absolutamente cualquier cosa:

  • Paso uno: convencerte de entregar credenciales IMAP/OAuth de cada cuenta que tengas. Ese es todo el modelo de negocio: la agregación es el producto.
  • Paso dos: sincronizarlo todo en su base de datos. Ahora existe una segunda copia de todo tu historial de correo en una infraestructura que no controlas y no puedes auditar.
  • Paso tres: “enriquecerlo”. Resúmenes con IA, categorización inteligente, puntuaciones de “bandeja prioritaria”: todo eso significa pasar tu correspondencia privada por un modelo que no elegiste, alojado en un sitio donde no puedes mirar.
  • Paso cuatro: se va todo a la mierda en una brecha. No “si”, sino “cuándo”. Cualquier agregador centralizado de correo es un objetivo único, gordo y de altísimo valor. Una filtración y todas las cuentas que conectaste caen de golpe, no repartidas entre cinco proveedores con cinco equipos de seguridad distintos, sino todas, a la vez, en un solo volcado, en un solo post de foro.

E incluso dejando a un lado el riesgo de brecha, hay una objeción más simple: ¿para qué necesita una base de datos una herramienta que me lee el correo? IMAP ya es la base de datos. El servidor de correo ya guarda el correo, lo indexa y sirve resultados de búsqueda. Apilar una copia encima no hace nada más rápido ni más seguro, solo crea una cosa más que puede filtrarse, desincronizarse o acabar reclamada por un fiscal.

Así que no construí un motor de sincronización. Construí un proxy sin estado que abre una conexión, hace exactamente lo que le pediste, y cuelga.

Conectas, Haces El Trabajo, Mueres

mailbox (el demonio se llama mailboxd) es una app FastAPI, con imaplib y smtplib de la stdlib por debajo, que convierte tantas cuentas IMAP/SMTP como le eches en una sola API HTTP y un solo servidor MCP, en exactamente el mismo puerto. El transporte MCP va montado directamente en la misma instancia de la app FastAPI, como subruta:

app.router.routes.append(Mount(_MCP_PATH, app=mcp_manager.handle_request))
app.add_middleware(_MCPSlashNormalizer)
app.add_middleware(_BearerASGI, cfg=cfg)

Mismo proceso, mismo puerto, misma puerta de autenticación bearer sobre los dos. No hay un segundo demonio, ni un segundo puerto que abrir en el firewall, ni un puente stdio corriendo como proceso hijo capaz de morirse por su cuenta y dejar vivo el HTTP. Un único pool de workers uvicorn sirve tanto peticiones REST como sesiones MCP streamable-HTTP desde el mismo socket.

La parte que de verdad me importa es el modelo de almacenamiento, que es: no hay. Cada llamada IMAP pasa por un context manager que abre una conexión nueva, se autentica, hace exactamente la operación pedida y derriba la conexión en un bloque finally, haya ido bien o no:

@contextmanager
def _connect(cfg: ImapConfig) -> Iterator[imaplib.IMAP4]:
    if cfg.tls == "ssl":
        conn: imaplib.IMAP4 = imaplib.IMAP4_SSL(
            cfg.host,
            cfg.port,
            ssl_context=ssl.create_default_context(),
            timeout=_SOCKET_TIMEOUT_SECONDS,
        )
    else:
        conn = imaplib.IMAP4(cfg.host, cfg.port, timeout=_SOCKET_TIMEOUT_SECONDS)
    try:
        if cfg.tls == "starttls":
            conn.starttls(ssl_context=ssl.create_default_context())
        try:
            conn.login(cfg.username, cfg.password)
        except imaplib.IMAP4.error as e:
            raise ImapError(f"login failed: {e}") from e
        yield conn
    finally:
        try:
            conn.logout()
        except (imaplib.IMAP4.error, OSError):
            ...  # logged, then the socket gets shut down

Absolutamente cada función IMAP: list_folders, list_messages, search_messages, fetch_message, fetch_raw, mark_seen: envuelve su trabajo en un bloque with _connect(cfg) as conn: recién hecho. Desde la v0.6.0 ese contexto verifica la cadena de certificados y el nombre de host tanto en ssl como en starttls, sin ningún ajuste en ningún sitio para desactivarlo, así que un servidor de correo privado significa montar tu bundle de CA y apuntar SSL_CERT_FILE ahí, no encogerse de hombros y aceptar lo que aparezca. Los sockets se rinden a los 30 segundos en vez de quedarse colgados para siempre contra un servidor que dejó de responder. Sin pooling, sin keep-alive, sin ningún hilo de sincronización en segundo plano leyéndote el correo lo hayas pedido o no. Ahora hay además un watcher opcional, mailboxd watch, que mantiene abierto un IMAP IDLE y hace POST a webhooks firmados con HMAC-SHA256 cuando llega correo nuevo: solo cabeceras y veredictos de spam, nunca cuerpos. Es un proceso aparte detrás de un perfil de Compose, así que corre porque tú lo arrancaste. SMTP hace lo mismo, con un context manager simple alrededor de smtplib.SMTP_SSL o smtplib.SMTP: abrir, autenticar, enviar, cerrar, cada vez que se golpea /send.

No existe ninguna tabla de mensajes en ninguna parte de este código. Sin Redis, sin motor de sincronización, sin una segunda copia de tu correo. La v0.6.0 por fin reconoció un fichero SQLite, en /var/lib/mailboxd/state.db, que guarda exactamente las seis cosas que no pueden vivir en un servidor IMAP: el registro de auditoría, los contadores de envío por hora, los puntos de control del polling, las reservas de borrador, los leases del watcher y la cola de entrega de webhooks. Seis tablas, y ninguna tiene una columna para el cuerpo de un mensaje. Monta un volumen en su directorio, si no pierdes los límites de tasa y el rastro de auditoría al reiniciar, y ese es todo el radio de la explosión. El correo en sí sigue sin escribirse nunca en ninguna parte: mata el contenedor en medio de un fetch y no queda nada tuyo que recuperar, porque la única copia siempre estuvo en el servidor de correo. Esto no es una casilla de cumplimiento pegada ahí para el artículo: es toda la arquitectura. Cero bytes de tu correo guardados no es un remate de marketing, es lo que te sale cuando te niegas a escribir un motor de sincronización.

Un Fichero YAML, Tantas Cuentas Como Quieras

Mis cinco bandejas viven en un solo fichero de configuración, y cada mailbox declara la combinación de imap y smtp que realmente necesita: una cuenta de solo lectura puede saltarse SMTP entero, un relay de solo envío puede saltarse IMAP:

auth:
  tokens:
    - name: agent
      token: "long-random-token-1"
      scopes: [read, draft, organize, send]
      mailboxes: "*"
mailboxes:
  - name: personal
    description: "Gmail"
    imap:
      host: imap.gmail.com
      port: 993
      tls: ssl
      username: [email protected]
      password: "app-password"
      default_folder: INBOX
    smtp:
      host: smtp.gmail.com
      port: 465
      tls: ssl
      username: [email protected]
      password: "app-password"
      from_address: "Me <[email protected]>"
  - name: work
    imap:  { host: mail.work.com, port: 143, tls: starttls, username: me, password: "...", default_folder: INBOX }
    smtp:  { host: mail.work.com, port: 587, tls: starttls, username: me, password: "...", from_address: [email protected] }

Pydantic valida todo al cargar: name tiene que encajar con [a-zA-Z0-9_-]+ y ser único (se convierte en un segmento de URL y es el identificador que un agente pasa como argumento MCP mailbox), cada mailbox necesita al menos uno de imap/smtp o la carga de la configuración se cae en seco, e IMAP cae por defecto en 993/ssl mientras SMTP cae en 587/starttls, salvo que tu proveedor sea raro. Cualquier valor de texto de ahí dentro puede escribirse ${SOME_VAR} y se resuelve desde el entorno al arrancar, con una variable que falta como error duro de arranque y no como contraseña vacía, así que las credenciales no tienen por qué vivir nunca en el YAML. La bandeja a la que apuntas en cada llamada se elige con ese mismo name, o, en MCP y en el filtro de /inbox, con la dirección de correo real de la cuenta. El argumento MCP mailbox resuelve cualquiera de las dos, así que un agente no tiene que acordarse de si la llamaste “personal” o “gmail” hace seis meses.

La API HTTP

Treinta y una rutas, todas JSON a la entrada, JSON a la salida, con errores de la forma {"error": {"code", "message"}, "detail": "..."}: el objeto error es el que miras, y el detail plano se quedó ahí para no romper nada de lo que parseaba la forma vieja. Estas diez son las que vas a teclear de verdad:

  • GET /health: siempre abierta, sin bearer, apunta ahí tu sonda de liveness y sigue con tu vida.
  • GET /mailboxes: lista las bandejas que ve tu token, sus direcciones, sus identidades de remitente y qué protocolos tiene cada una.
  • GET /inbox: esta es la que realmente quieres. Se abre en abanico sobre cada bandeja con IMAP en paralelo con un ThreadPoolExecutor, ejecuta la misma búsqueda estructurada en cada una, fusiona los resultados con los más nuevos primero y etiqueta cada mensaje con la cuenta de la que vino. Una cuenta muerta aterriza en un array errors en vez de reventar la llamada entera: nueve bandejas en pie y una caída te siguen dando las otras nueve.
  • GET /mailboxes/{name}/folders: lista las carpetas IMAP de una cuenta.
  • GET /mailboxes/{name}/messages: cabeceras con los más nuevos primero, con un parámetro search IMAP en crudo si quieres pasarle directamente algo como UNSEEN.
  • GET /mailboxes/{name}/search: los mismos filtros estructurados que /inbox (from/to/recipient/subject/body/text/since/before, los flags Seen/Flagged/Answered, cotas de tamaño, palabras clave IMAP, has_attachments y label), limitados a una cuenta, con paginación por cursor opaco y un since_cursor que devuelve solo lo que ha aterrizado desde tu última llamada.
  • GET /mailboxes/{name}/messages/{uid}: un mensaje, decodificado completo, con un reader=true opcional.
  • DELETE /mailboxes/{name}/messages/{uid}: mueve el mensaje a la papelera, con UID MOVE donde el servidor lo tiene y con COPY más \Deleted más UID EXPUNGE donde no. Antes ponía el flag y hacía expunge en el sitio, que era una forma excelente de que un agente te borrara el correo para siempre en la primera llamada de herramienta mal hecha. El borrado real es permanent=true, y solo funciona desde dentro de la papelera y solo para un token que lleve el scope delete, así que hay que quererlo dos veces.
  • POST /mailboxes/{name}/messages/{uid}/seen: pone o quita el flag Seen.
  • POST /mailboxes/{name}/send: envío por SMTP, multipart/alternative si le das un cuerpo de texto y otro HTML.

Las otras veintiuna llegaron con la v0.6.0 y van más de escribir que de leer. La fuente en crudo y los adjuntos individuales como descargas (.../messages/{uid}/raw, .../attachments/{part_id}), búsqueda de conversaciones por Message-ID y References (.../thread, .../threads), y respuestas derivadas en el servidor en .../messages/{uid}/reply, donde los destinatarios, el asunto con Re:, In-Reply-To y References salen todos del original, así que un cliente no puede romper el threading pasándolos él. Luego las cinco rutas de borrador, que son las que importan si dejas a un agente cerca de esto: redacta el mensaje, lo deja en tu carpeta Drafts, y decides tú. Más seen y delete en lote, etiquetas, movimientos, creación de carpetas, identidades, estadísticas por carpeta, y cuatro rutas de admin: el health en profundidad, el registro de auditoría, y la cola de entrega de webhooks con el replay encima.

UIDs en todas partes, nunca números de secuencia, así que los identificadores se mantienen estables mientras la bandeja se mueve bajo tus pies entre llamadas. Y el endpoint de envío no es perezoso con esto: pone una cabecera Date correcta, deriva el dominio del Message-ID de la dirección del remitente para que no contradiga la cabecera From:, que es una trampa clásica de los filtros antispam y el tipo de detalle que solo aciertas si de verdad has perdido correo por su culpa. Antes también falsificaba un User-Agent de Thunderbird encima, con la teoría de que no tener ninguno parece ligeramente sospechoso para las heurísticas de los proveedores; la v0.6.0 lo quitó. Lo que hace ahora en su lugar es depositar la copia enviada: en un envío exitoso, el mensaje, con el Bcc intacto, se añade a tu carpeta Sent, y si ese añadido falla recibes warnings: ["sent_copy_failed"], no un envío fallido que ya has hecho. Gmail deposita su correo saliente por su cuenta, así que una bandeja de Gmail quiere save_sent: false o lo ves dos veces. Y el remitente que pasas a /send ya no es texto libre: identity (o el viejo from_address) tiene que resolverse a una identidad configurada para esa bandeja, o la llamada vuelve con 422 invalid_identity, y así es como un agente ya no puede enviar en nombre de quien le dé la gana.

Un Solo Bearer, Ningún Segundo Sistema De Auth Que Aprender

La autenticación es una lista de tokens en el fichero YAML. Lista vacía, no entra nadie: desde la v0.5.0 una lista de tokens vacía o ausente falla cerrada y todo salvo /health devuelve un 401, así que no puedes olvidarte de la auth y publicar una bandeja abierta. Si no, cada petición que no sea GET /health necesita una cabecera Authorization: Bearer <token> que encaje con uno de ellos, comprobada con hmac.compare_digest para que un ataque de temporización no pueda pescar tokens válidos carácter a carácter. Varios tokens significan que puedes rotar sin caída: añades uno nuevo, mueves tus clientes a él, borras el viejo, nadie se entera.

Desde la v0.6.0 un token es un mapping, no una cadena pelada: {name, token, scopes, mailboxes, dry_run}, con seis scopes a elegir: read, draft, organize, send, delete, admin. Así que el agente al que dejas leerte el correo no se queda automáticamente con el derecho a enviar en tu nombre, un token que lista dos bandejas recibe “not found” para las otras tres en vez de un forbidden que le enseña que existen, y dry_run: true te devuelve una previsualización del mensaje en vez de ponerlo en el cable. Cada escritura, y cada escritura rechazada, aterriza en el registro de auditoría bajo el name de ese token, que es la razón real por la que los tokens acabaron con nombre. Una cadena pelada sigue funcionando y sigue concediendo todos los scopes en todas las bandejas, y escribe al arrancar un aviso que te dice que pares. Por encima de los scopes hay también un techo de envío por hora deslizante, contado por bandeja y por token (send_guard.per_hour, 60 por defecto, sobreescribible por bandeja con smtp.per_hour), que responde 429 con un Retry-After. Un agente atascado en un bucle quema su propia cuota, no tu reputación de remitente.

La parte interesante es cómo está enganchado. Es un middleware ASGI puro, no el BaseHTTPMiddleware de FastAPI, y eso es a propósito:

class _BearerASGI:
    """Pure-ASGI bearer-auth wrapper around the whole app.
    Pure ASGI (not BaseHTTPMiddleware) so it doesn't buffer streaming
    responses — required for the MCP streamable-HTTP transport mounted
    under /mcp to keep working.
    """

BaseHTTPMiddleware mete el cuerpo de la respuesta en un buffer para que puedas inspeccionarlo o modificarlo, que es exactamente lo que rompe una conexión SSE en streaming, la que el transporte MCP necesita. Así que la puerta de auth está hecha a mano a nivel ASGI, mira las cabeceras en crudo del scope antes de que nada más abajo empiece siquiera a responder, y deja que la sesión MCP fluya intacta una vez autenticada. Misma puerta, misma lista de tokens, mismo comportamiento de 401 con WWW-Authenticate, tanto si le das con curl a /inbox como si abres una sesión MCP en /mcp. Una sola cosa que configurar, una sola cosa que rotar, una sola cosa que puede dejarte fuera con una errata.

El Servidor MCP: Un Set Plano De Herramientas, Da Igual Cuántas Cuentas Tengas

Esta es la parte que convierte “una API de correo” en “algo que un agente puede conducir de verdad”. /mcp expone veintiocho herramientas sobre el transporte streamable-HTTP del SDK oficial de MCP para Python: sin chapuzas de stdio, sin proceso aparte:

  • mailboxes: el descubrimiento: lista las cuentas configuradas y qué puede hacer cada una.
  • inbox: la lectura unificada, multicuenta, con el mismo abanico que el /inbox de HTTP.
  • Lectura sobre una sola cuenta: list_folders, list_messages, search, get_message, get_raw_message, get_attachment, get_thread, list_threads, identities, mailbox_stats.
  • Organización: mark_seen, set_labels, move_messages, create_folder, delete_message.
  • Escritura: send, reply, create_draft, list_drafts, update_draft, delete_draft, send_draft.
  • Admin: audit_log, health_check, webhook_deliveries, replay_webhook.

Veintiocho herramientas. No veintiocho por bandeja: veintiocho en total, para siempre, tengas una cuenta configurada o cien. Cada herramienta que trabaja sobre una bandeja recibe un argumento mailbox que acepta o el nombre de la configuración o la dirección de correo real, y tools/list devuelve solo las herramientas que permiten los scopes del token que llama, así que un agente de solo lectura no ve send en absoluto. Incluso quita los argumentos que rechazaría: ningún flag permanent en delete_message sin el scope delete, y reply llega con draft fijado en true para un token que no puede enviar. Sin botones muertos, sin herramientas llamadas send_personal y send_work y send_burner inflando la ventana de contexto del modelo en cada llamada. Un agente llama a mailboxes una vez, ve lo que hay, y de ahí en adelante pasa el nombre que quiera como parámetro. Es la misma filosofía de diseño que usé en el montaje de Telegram-como-MCP de telethon-plus: expones tu propia cuenta como una superficie de herramientas que un agente puede conducir, en vez de construir un chatbot que tiene que suplicarte que le pegues cosas. Misma idea, otro protocolo: allí era tu cuenta de Telegram, aquí es tu bandeja.

Hay también un plugin puente stdio↔HTTP para runtimes de agentes del estilo de OpenClaw que solo saben hablar con servidores MCP locales por stdio: sale por mcp-remote hacia tu instancia de mailboxd en marcha y pasa el token bearer, así que el catálogo de herramientas de tamaño constante aparece también en agentes que no conocen streamable-HTTP de forma nativa. Mismo set de herramientas, misma auth, solo una tubería más fina delante.

El Modo Lectura, Porque Los Correos De Marketing Son Escenas Del Crimen En HTML

Añade reader=true a un fetch de mensaje y te sale un campo body_reader al lado de los body_text/body_html en crudo: el cuerpo HTML pasado por html2text, con las imágenes ignoradas y el salto de línea automático desactivado, con los estilos, scripts y píxeles de seguimiento fuera, los encabezados convertidos a markdown y las tablas aplanadas. La mayoría del correo transaccional y de marketing llega como multipart/alternative, donde la parte de texto es un muñón inútil de “ver en HTML” y el contenido real solo existe en la parte HTML: el modo lectura saca las palabras sin arrastrarte por la sopa de <table><tr><td style="..."> con la que la gente del email marketing parece creer que seguimos en 2003. Es aditivo: los cuerpos originales vuelven intactos, así que una interfaz puede renderizar el HTML mientras un agente lee el markdown. La v0.6.0 añadió la otra mitad: safe_html=true devuelve body_html_safe, el mismo HTML pasado por una lista blanca de nh3 con las imágenes remotas fuera, así que una interfaz puede renderizarlo sin pingar el servidor de tracking del remitente, más un bloque html_analysis que señala el texto oculto a un lector humano y los enlaces que mienten: hosts en punycode, acortadores de URL, IPs peladas, destinos javascript:, y una etiqueta visible cuyo dominio no encaja con el href de detrás. Que, para el correo que aparece diciendo que viene de tu banco, es justo la parte que quieres que vea un agente.

El Detalle De Cadena De Suministro Del Que Nadie Más Se Ocupa

Esta es la parte de la que de verdad quiero presumir, porque casi nadie lo hace. pyproject.toml fija una fecha exclude-newer para uv:

[tool.uv]
exclude-newer = "2026-10-06T00:00:00Z"

Cualquier instalación de dependencias se niega a bajar una versión de paquete publicada después de esa marca de tiempo, y punto. Por qué importa: el ataque moderno a la cadena de suministro no es una operación estatal elaborada, es el token de npm/PyPI de un mantenedor pescado por phishing y una release de parche maliciosa publicada a las tres de la mañana antes de que nadie se dé cuenta. Si tu build solo hace uv sync sobre la última versión, puedes bajar la release envenenada a las pocas horas de salir. Con un exclude-newer fijo, todo lo publicado después de tu corte es invisible para el resolvedor: una release maliciosa recién salida a las tres de la mañana simplemente no se puede instalar hasta que el corte se mueva.

El problema de los cortes fijos suele ser que se pudren hasta volverse un lastre: funcionan indefinidamente, hasta que son viejos y ahora tampoco te llegan los parches de seguridad. Así que el corte no se mantiene a mano: scripts/bump_exclude_newer.sh lo reescribe a medianoche UTC siete días antes del día actual, y está enganchado en cada target make pkg-* que cambia una dependencia (pkg-add, pkg-remove, pkg-update, pkg-upgrade; un pkg-lock simple solo vuelve a resolver sobre el corte que ya tiene), así que la puerta se reancla sola en “hace una semana” automáticamente, cada vez que una dependencia cambia de verdad, lo que significa que nada más joven de siete días entra nunca, da igual cuándo lo ejecutes. Te llevas la protección de la ventana de retraso sin tener que acordarte nunca de mover una fecha a mano.

Tests Que Ponen Bytes De Verdad En Un Socket

El camino SMTP no se prueba contra un mock que solo comprueba si se llamó a una función: tests/test_smtp_integration.py arranca un servidor SMTP real, hecho a mano, en un socket TCP real, usando solo socketserver de la stdlib. Habla SMTP de verdad: responde a HELO/EHLO, MAIL FROM, RCPT TO, el baile del terminador de DATA, RSET, NOOP y QUIT, corriendo en su propio hilo de fondo. El test conduce después el endpoint /send real de la app FastAPI a través de un TestClient, que llama al cliente smtplib real, que abre un socket real hacia ese servidor falso y le manda bytes de protocolo SMTP. Luego el test decodifica lo que realmente aterrizó con email.message_from_bytes y afirma sobre las cabeceras parseadas y sobre el sobre, incluido confirmar que los destinatarios en BCC sí llegan a la lista RCPT TO del sobre SMTP pero nunca se filtran a las cabeceras To o Cc del cuerpo del mensaje, que es el sentido entero del BCC y algo escandalosamente fácil de hacer mal en un remitente de correo hecho a mano.

Tu Correo Sigue Siendo Tuyo

No estoy intentando convencerte de que autoalojar tu propio proxy de IMAP sea lo que deberían hacer “las personas normales”. No lo es. Lo que te digo es que si eres del tipo que ya se monta su propia infraestructura de IA, y si has leído aigate, probablemente lo seas: no hay ninguna razón para que tu correo sea la única cosa que sigues entregando a un tercero. Este contenedor les da a tus agentes, a tus scripts y a tus pipelines de curl de las tres de la mañana tanto control de leer/enviar/borrar sobre tantas bandejas como tengas como el que elijas darle a cada token, en un solo puerto, con cero bytes de tu correo real tocando nunca su disco. Mátalo, reinícialo, muévelo a otro host: lo único que merece llevarse es un fichero SQLite pequeño con filas de auditoría y contadores, y tu correo nunca estuvo dentro.

Cógelo en github.com/psyb0t/docker-mailbox o baja psyb0t/mailbox directo de Docker Hub. El código es WTFPL, haz lo que te dé la gana con él. La imagen publicada es otra bestia: empaqueta html2text, que es GPL-3.0, así que la imagen entera es una obra combinada GPL (el THIRD_PARTY.md del repo tiene los detalles). Ponla sobre tu correo y no vuelvas a pensar en ella.

Cómo Instalártelo En El Agente

Un agente que puede leerte el correo debería al menos conocer la API sin que se lo digas dos veces. Todo lo que hay bajo .agents/ está catalogado en un solo marketplace, así que son dos comandos:

claude plugin marketplace add psyb0t/agents
claude plugin install mailbox@psyb0t

Codex usa el mismo marketplace con otro verbo: codex plugin add mailbox@psyb0t, porque codex plugin install no existe. También encuentra la skill por su cuenta en un checkout del repo, porque escanea .agents/skills/ de forma nativa sin que haya absolutamente nada instalado. Ahora además está listado en el MCP Registry oficial, así que un cliente que resuelva sus servidores desde ahí puede encontrarlo sin que nadie le pase una URL.