mailbox: Inboxurile Tale ca API HTTP și Server MCP Care Nu Stochează Niciun Octet

Am cinci conturi de email. Un Gmail personal pe care îl am de când eram puști, o adresă de serviciu, un “burner” pe care îl folosesc la înregistrări în care n-am încredere, o adresă pe domeniul blogului ăstuia și încă una de care uitasem complet până am găsit parola într-un fișier text. Cinci inboxuri, cinci seturi de credențiale, cinci taburi, cinci badge-uri de necitite care mă mint în legătură cu cât de nasoală mi-e viața. Orice aplicație de “inbox unificat” de pe planeta asta vrea să-mi rezolve problema luându-mi în custodie toate cele cinci conturi, sincronizându-le în cloudul ei, rulând “categorizare inteligentă cu AI” pe mailurile mele și dându-mi un UI șmecher în schimbul singurului lucru pe care îl am și chiar merită protejat: conținutul inboxului meu.

Nu. În pula mea. În email am extrase de cont, coduri de recuperare 2FA, chestii medicale, contracte și destule date personale cât să-mi distrugă un deceniu din viață dacă se scurg. Nu trimit alea vreunui startup de “inbox zero” finanțat cu bani de VC, ca să le scurgă injecția SQL a stagiarului lor peste optsprezece luni. Așa că am construit mailbox: un container, un fișier YAML, un API HTTP și un server MCP pe același port, zero octeți din mailurile mele stocați altundeva decât pe serverele de mail unde stau oricum.

Cum Te Fraierește Orice Inbox Unificat

Orice produs de “inbox unificat” urmează aceeași schemă, și e aceeași schemă pe care o aplică orice firmă de SaaS la absolut orice:

  • Pasul unu: te convinge să predai credențiale IMAP/OAuth pentru fiecare cont pe care îl ai. Ăsta e tot modelul de business: agregarea e produsul.
  • Pasul doi: sincronizează tot în baza lor de date. Acum există o a doua copie a întregului tău istoric de mail pe o infrastructură pe care n-o controlezi și n-o poți audita.
  • Pasul trei: o “îmbogățește”. Rezumare cu AI, categorizare inteligentă, scoruri de “inbox prioritar”: toate înseamnă că îți trec corespondența privată printr-un model pe care nu l-ai ales, găzduit undeva unde nu poți să te uiți.
  • Pasul patru: se duce dracului printr-o breșă. Nu “dacă”, ci “când”. Orice agregator centralizat de mail e o singură țintă, frumoasă și extrem de valoroasă. O scurgere și fiecare cont pe care l-ai conectat e compromis deodată, nu eșalonat pe cinci furnizori diferiți cu cinci echipe de securitate diferite, ci tot, odată, într-un singur dump, într-o singură postare pe forum.

Și chiar lăsând riscul de breșă deoparte, există o obiecție mai simplă: de ce are nevoie de o bază de date o unealtă care îmi citește mailul? IMAP e deja baza de date. Serverul de mail stochează deja mailul, îl indexează și servește rezultate de căutare. Să-i mai pui o copie deasupra nu face nimic mai rapid și nici mai sigur, doar creează încă un lucru care poate să se scurgă, să iasă din sincron sau să fie cerut de un procuror.

Așa că n-am construit un motor de sincronizare. Am construit un proxy fără stare care deschide o conexiune, face exact chestia pe care i-ai cerut-o și închide.

Te Conectezi, Îți Faci Treaba, Mori

mailbox (demonul se cheamă mailboxd) e o aplicație FastAPI, cu imaplib și smtplib din stdlib dedesubt, care transformă oricâte conturi IMAP/SMTP îi arunci în un singur API HTTP și un singur server MCP, pe exact același port. Transportul MCP e montat direct în aceeași instanță de aplicație FastAPI, ca sub-rută:

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

Același proces, același port, aceeași poartă de autentificare bearer peste amândouă. Nu e niciun al doilea demon, niciun al doilea port de băgat în firewall, niciun bridge stdio care rulează ca proces copil și care ar putea crăpa independent de stratul HTTP. Un singur pool de workeri uvicorn servește și cereri REST și sesiuni MCP streamable-HTTP de pe același socket.

Partea care chiar mă interesează e modelul de stocare, care e: nu există. Fiecare apel IMAP trece printr-un context manager care deschide o conexiune proaspătă, se loghează, face exact operația cerută și dărâmă conexiunea într-un bloc finally, fie că a mers, fie că nu:

@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

Absolut fiecare funcție IMAP: list_folders, list_messages, search_messages, fetch_message, fetch_raw, mark_seen: își învelește treaba într-un bloc proaspăt with _connect(cfg) as conn:. Din v0.6.0 contextul ăla verifică lanțul de certificate și hostname-ul și pe ssl și pe starttls, fără niciun setting nicăieri care să-l oprească, deci un server de mail privat înseamnă să-ți montezi bundle-ul de CA și să pui SSL_CERT_FILE pe el, nu să ridici din umeri și să accepți orice apare. Socketurile renunță după 30 de secunde în loc să atârne la nesfârșit pe un server care a încetat să răspundă. Fără pooling, fără keep-alive, fără vreun thread de sincronizare în fundal care îți citește mailul fie că ai cerut-o, fie că nu. Acum există și un watcher opțional, mailboxd watch, care ține un IMAP IDLE deschis și dă POST la webhookuri semnate HMAC-SHA256 când aterizează mail nou: doar headere și verdicte de spam, niciodată corpuri. E un proces separat în spatele unui profil de Compose, deci rulează pentru că l-ai pornit tu. SMTP face la fel, cu un context manager simplu în jurul lui smtplib.SMTP_SSL sau smtplib.SMTP: deschide, autentifică, trimite, închide, de fiecare dată când e lovit /send.

Nu există niciun tabel de mesaje nicăieri în codul ăsta. Fără Redis, fără motor de sincronizare, fără a doua copie a mailului tău. v0.6.0 a recunoscut în sfârșit un fișier SQLite, la /var/lib/mailboxd/state.db, și ține exact cele șase lucruri care nu pot sta pe un server IMAP: logul de audit, contoarele orare de trimitere, checkpointurile de polling, revendicările de draft, leaseurile watcherului și coada de livrare a webhookurilor. Șase tabele, și niciuna nu are vreo coloană pentru corpul unui mesaj. Montează un volum pe directorul lui, altfel îți pierzi limitele de rată și urma de audit la restart, și asta e toată raza de explozie. Mailul în sine tot nu se scrie niciodată nicăieri: omoară containerul în mijlocul unui fetch și nu rămâne nimic de-al tău de recuperat, pentru că singura copie a fost dintotdeauna pe serverul de mail. Nu e o căsuță bifată de conformitate pe care am lipit-o pentru articol: e toată arhitectura. Zero octeți din mailul tău stocați nu e o replică de marketing, e ce primești când refuzi să scrii un motor de sincronizare.

Un Fișier YAML, Oricâte Conturi Vrei

Toate cele cinci inboxuri ale mele stau într-un singur fișier de config, iar fiecare mailbox declară orice combinație de imap și smtp îi trebuie de fapt: un cont doar de citit poate sări complet peste SMTP, un releu doar de trimis poate sări peste 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 validează tot la încărcare: name trebuie să se potrivească cu [a-zA-Z0-9_-]+ și să fie unic (devine un segment de URL și e handle-ul pe care un agent îl pasează ca argument MCP mailbox), fiecare mailbox are nevoie de cel puțin unul dintre imap/smtp altfel încărcarea configului pică de tot, iar IMAP merge implicit pe 993/ssl în timp ce SMTP merge pe 587/starttls, dacă nu cumva furnizorul tău e ciudat. Orice valoare de tip string de acolo poate fi scrisă ${SOME_VAR} și se rezolvă din mediu la pornire, cu o variabilă lipsă ca eroare dură de pornire, nu ca parolă goală, deci credențialele n-au de ce să stea vreodată în YAML. Mailboxul pe care îl țintești la orice apel e ales prin același name, sau, pe MCP și în filtrul /inbox, prin adresa de email propriu-zisă a contului. Argumentul MCP mailbox rezolvă oricare dintre ele, deci un agent nu trebuie să țină minte dacă i-ai zis “personal” sau “gmail” acum șase luni.

API-ul HTTP

Treizeci și una de rute, toate JSON la intrare, JSON la ieșire, cu erori de forma {"error": {"code", "message"}, "detail": "..."}: obiectul error e cel pe care faci match, iar detail-ul plat a rămas acolo ca să nu se spargă nimic din ce parsa forma veche. Astea zece sunt cele pe care chiar o să le tastezi:

  • GET /health: mereu deschis, fără bearer, pune-ți sonda de liveness pe el și mergi mai departe.
  • GET /mailboxes: listează mailboxurile pe care le vede tokenul tău, adresele lor, identitățile de expeditor și ce protocoale are fiecare.
  • GET /inbox: ăsta e cel pe care îl vrei de fapt. Se răsfiră peste fiecare mailbox cu IMAP în paralel printr-un ThreadPoolExecutor, rulează aceeași căutare structurată pe fiecare, îmbină rezultatele cu cele mai noi primele și marchează fiecare mesaj cu contul din care a venit. Un cont mort aterizează într-un array errors în loc să arunce în aer tot apelul: nouă mailboxuri în picioare și unul căzut tot îți dau celelalte nouă.
  • GET /mailboxes/{name}/folders: listează folderele IMAP ale unui cont.
  • GET /mailboxes/{name}/messages: headere cu cele mai noi primele, cu parametru search IMAP brut dacă vrei să-i dai direct ceva de genul UNSEEN.
  • GET /mailboxes/{name}/search: aceleași filtre structurate ca la /inbox (from/to/recipient/subject/body/text/since/before, flagurile Seen/Flagged/Answered, limite de mărime, cuvinte cheie IMAP, has_attachments și label), limitate la un cont, cu paginare prin cursor opac și un since_cursor care întoarce doar ce a aterizat de la ultimul tău apel.
  • GET /mailboxes/{name}/messages/{uid}: un mesaj, decodat complet, cu un reader=true opțional.
  • DELETE /mailboxes/{name}/messages/{uid}: mută mesajul în Trash, cu UID MOVE unde serverul îl are și cu COPY plus \Deleted plus UID EXPUNGE unde nu. Înainte punea flagul și făcea expunge pe loc, ceea ce era o metodă excelentă ca un agent să-ți șteargă mailul definitiv de la primul apel de tool greșit. Ștergerea reală e permanent=true, și merge doar din interiorul lui Trash și doar pentru un token care are scope-ul delete, deci trebuie s-o vrei de două ori.
  • POST /mailboxes/{name}/messages/{uid}/seen: pune sau scoate flagul Seen.
  • POST /mailboxes/{name}/send: trimitere prin SMTP, multipart/alternative dacă îi dai și un corp text și unul HTML.

Celelalte douăzeci și una au venit cu v0.6.0 și sunt mai mult despre scris decât despre citit. Sursa brută și atașamentele individuale ca descărcări (.../messages/{uid}/raw, .../attachments/{part_id}), căutare de conversații după Message-ID și References (.../thread, .../threads) și răspunsuri derivate pe server la .../messages/{uid}/reply, unde destinatarii, subiectul cu Re:, In-Reply-To și References sunt toate scoase din original, deci un client nu poate strica threadingul predându-le el. Apoi cele cinci rute de draft, care sunt alea care contează dacă lași un agent pe lângă chestia asta: scrie mesajul, îl lasă în folderul tău Drafts, iar tu decizi. Plus seen și delete în batch, labeluri, mutări, creare de foldere, identități, statistici pe folder și patru rute de admin: health în adâncime, logul de audit și coada de livrare a webhookurilor cu replay pe ea.

Peste tot UID-uri, niciodată numere de secvență, deci identificatorii rămân stabili în timp ce mailboxul se mișcă pe sub tine între apeluri. Iar endpointul de trimitere nu e leneș în privința asta: pune un header Date cum trebuie, derivă domeniul Message-ID din adresa expeditorului ca să nu se bată cap în cap cu headerul From:, ceea ce e o capcană clasică de filtru de spam și genul de detaliu pe care îl nimerești doar dacă ți-a picat efectiv mail din cauza lui. Înainte mai falsifica și un User-Agent de Thunderbird peste asta, pe teoria că lipsa lui pare ușor suspectă euristicilor furnizorilor; v0.6.0 l-a scos. Ce face în schimb acum e să depună copia trimisă: la o trimitere reușită, mesajul, cu Bcc intact, e adăugat în folderul tău Sent, iar dacă adăugarea pică primești warnings: ["sent_copy_failed"], nu o trimitere eșuată pe care deja ai făcut-o. Gmail își depune singur mailul de ieșire, deci un mailbox de Gmail vrea save_sent: false altfel îl vezi de două ori. Și expeditorul pe care îl dai la /send nu mai e text liber: identity (sau vechiul from_address) trebuie să se rezolve la o identitate configurată pentru mailboxul ăla, altfel apelul se întoarce cu 422 invalid_identity, și uite așa un agent nu mai poate trimite în numele oricui vrea.

Un Singur Bearer, Niciun Al Doilea Sistem de Auth de Învățat

Autentificarea e o listă de tokenuri în fișierul YAML. Listă goală, nu intră nimeni: din v0.5.0 o listă de tokenuri goală sau lipsă pică închis și tot în afară de /health dă 401, deci nu poți uita de auth și livra un mailbox deschis. Altfel fiecare cerere în afară de GET /health are nevoie de un header Authorization: Bearer <token> care se potrivește cu unul dintre ele, verificat cu hmac.compare_digest ca un atac de timing să nu poată pescui tokenuri valide caracter cu caracter. Mai multe tokenuri înseamnă că poți roti fără downtime: adaugi unul nou, îți muți clienții pe el, îl ștergi pe cel vechi, nu observă nimeni.

Din v0.6.0 un token e un mapping, nu un string gol: {name, token, scopes, mailboxes, dry_run}, cu șase scope-uri de ales: read, draft, organize, send, delete, admin. Deci agentul căruia îi dai voie să-ți citească mailul nu capătă automat și dreptul să trimită în numele tău, un token care listează două mailboxuri primește “not found” pentru celelalte trei în loc de un forbidden care îi spune că există, iar dry_run: true îți dă înapoi o previzualizare a mesajului în loc să-l pună pe fir. Fiecare scriere, și fiecare scriere refuzată, aterizează în logul de audit sub name-ul acelui token, care e motivul real pentru care tokenurile au căpătat nume. Un string gol tot merge și tot acordă fiecare scope pe fiecare mailbox, și loghează un avertisment la pornire care îți zice să te oprești. Peste scope-uri mai e și un plafon orar glisant de trimiteri, numărat per mailbox și per token (send_guard.per_hour, 60 implicit, suprascriptibil per mailbox cu smtp.per_hour), care răspunde cu 429 și un Retry-After. Un agent blocat într-o buclă își arde propria cotă, nu reputația ta de expeditor.

Partea interesantă e cum e legat. E un middleware ASGI brut, nu BaseHTTPMiddleware din FastAPI, și asta intenționat:

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 buferează corpul răspunsului ca să-l poți inspecta sau modifica, ceea ce e exact ce strică o conexiune SSE în flux, de care transportul MCP are nevoie. Așa că poarta de auth e făcută de mână la nivel ASGI, verifică headerele brute din scope înainte ca ceva din aval să înceapă măcar să răspundă, și lasă sesiunea MCP să curgă neatinsă odată ce e autentificată. Aceeași poartă, aceeași listă de tokenuri, același comportament de 401 cu WWW-Authenticate, fie că dai cu curl în /inbox, fie că deschizi o sesiune MCP la /mcp. Un singur lucru de configurat, un singur lucru de rotit, un singur lucru care te poate încuia pe dinafară dacă dai greșit la tastat.

Serverul MCP: Un Set Plat de Tooluri, Indiferent Câte Conturi Ai

Asta e partea care transformă “un API de email” în “ceva ce un agent chiar poate conduce”. /mcp expune douăzeci și opt de tooluri peste transportul streamable-HTTP al SDK-ului oficial MCP pentru Python: fără prostii cu stdio, fără proces separat:

  • mailboxes: descoperire: listează conturile configurate și ce poate fiecare.
  • inbox: citirea unificată, peste conturi, cu aceeași răsfirare ca /inbox din HTTP.
  • Citire pe un singur cont: list_folders, list_messages, search, get_message, get_raw_message, get_attachment, get_thread, list_threads, identities, mailbox_stats.
  • Organizare: mark_seen, set_labels, move_messages, create_folder, delete_message.
  • Scriere: send, reply, create_draft, list_drafts, update_draft, delete_draft, send_draft.
  • Admin: audit_log, health_check, webhook_deliveries, replay_webhook.

Douăzeci și opt de tooluri. Nu douăzeci și opt per mailbox: douăzeci și opt în total, pentru totdeauna, fie că ai un cont configurat, fie că ai o sută. Fiecare tool care lucrează pe un mailbox primește un argument mailbox care acceptă ori numele din config, ori adresa de email propriu-zisă, iar tools/list întoarce doar toolurile permise de scope-urile tokenului care apelează, deci un agent read-only nu vede send deloc. Ba chiar scoate argumentele pe care le-ar refuza: niciun flag permanent pe delete_message fără scope-ul delete, iar reply ajunge cu draft fixat pe true pentru un token care nu poate trimite. Fără butoane moarte, fără tooluri numite send_personal și send_work și send_burner care să umfle fereastra de context a modelului la fiecare apel. Un agent cheamă mailboxes o dată, vede ce există, și de atunci încolo pasează numele pe care îl vrea ca parametru. E aceeași filozofie de design pe care am folosit-o la setupul de Telegram-ca-MCP din telethon-plus: îți expui propriul cont ca o suprafață de tooluri pe care un agent o poate conduce, în loc să construiești un chatbot care trebuie să te roage să-i lipești chestii. Aceeași idee, alt protocol: acolo e contul tău de Telegram, aici e inboxul tău.

Mai e și un plugin de bridge stdio↔HTTP pentru runtimeuri de agenți în stil OpenClaw care știu să vorbească doar cu servere MCP locale pe stdio: iese prin mcp-remote spre instanța ta de mailboxd care rulează și pasează mai departe tokenul bearer, deci catalogul de tooluri de mărime constantă apare și în agenți care nu știu nativ streamable-HTTP. Același set de tooluri, aceeași auth, doar o țeavă mai subțire în față.

Modul Reader, Pentru Că Mailurile de Marketing Sunt Scene de Crimă în HTML

Adaugi reader=true la un fetch de mesaj și primești un câmp body_reader lângă body_text/body_html-ul brut: corpul HTML trecut prin html2text, cu imaginile ignorate și word-wrapul oprit, cu stiluri, scripturi și pixeli de tracking scoși, cu titlurile transformate în markdown și tabelele aplatizate. Majoritatea mailurilor tranzacționale și de marketing vin ca multipart/alternative, unde partea text e un ciot inutil de “vezi în HTML”, iar conținutul real există doar în partea HTML: modul reader scoate cuvintele fără să te târască prin supa de <table><tr><td style="..."> pe care cei din email marketing par să creadă că suntem tot în 2003. E aditiv: corpurile originale se întorc neatinse, deci un UI poate randa HTML-ul în timp ce un agent citește markdownul. v0.6.0 a adăugat cealaltă jumătate: safe_html=true întoarce body_html_safe, același HTML trecut printr-un allow-list nh3 cu imaginile remote scoase, deci un UI îl poate randa fără să pingăie serverul de tracking al expeditorului, plus un bloc html_analysis care semnalează text ascuns față de un cititor uman și linkuri care mint: hosturi punycode, scurtătoare de URL-uri, IP-uri goale, ținte javascript: și o etichetă vizibilă al cărei domeniu nu se potrivește cu href-ul din spate. Care, pentru mailul care apare pretinzând că e de la banca ta, e exact partea pe care chiar vrei s-o vadă un agent.

Detaliul de Lanț de Aprovizionare de Care Nu Se Sinchisește Nimeni Altcineva

Asta e partea cu care chiar vreau să mă laud, pentru că aproape nimeni n-o face. pyproject.toml fixează o dată exclude-newer pentru uv:

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

Orice instalare de dependențe refuză să tragă vreo versiune de pachet publicată după timestampul ăla, punct. De ce contează: atacul modern pe lanțul de aprovizionare nu e vreo operațiune elaborată de stat, e tokenul de npm/PyPI al unui maintainer luat prin phishing și un point release malițios care iese la 3 noaptea înainte să observe cineva. Dacă buildul tău face doar uv sync pe latest, poți trage releaseul otrăvit în câteva ore de la publicare. Cu un exclude-newer fix, orice e publicat după limita ta e invizibil pentru resolver: un release malițios proaspăt de la 3 noaptea pur și simplu nu poate fi instalat până nu se mută limita.

Problema cu limitele fixe e de obicei că putrezesc într-o datorie: merg la nesfârșit, până când sunt vechi și acum nu mai poți lua nici patch-uri de securitate. Așa că limita nu e întreținută de mână: scripts/bump_exclude_newer.sh o rescrie la miezul nopții UTC cu șapte zile înainte de ziua curentă, și e legată în fiecare target make pkg-* care schimbă o dependență (pkg-add, pkg-remove, pkg-update, pkg-upgrade; un pkg-lock simplu doar re-rezolvă pe limita pe care o are deja), deci poarta se re-ancorează singură la “acum o săptămână” automat, de fiecare dată când chiar se schimbă o dependență, ceea ce înseamnă că nimic mai tânăr de șapte zile nu intră niciodată, indiferent când rulezi. Primești protecția ferestrei de întârziere fără să trebuiască vreodată să ții minte să muți o dată cu mâna.

Teste Care Chiar Pun Octeți Pe Un Socket

Calea SMTP nu e testată pe un mock care doar verifică dacă s-a apelat o funcție: tests/test_smtp_integration.py pornește un server SMTP real, făcut de mână, pe un socket TCP real, folosind doar socketserver din stdlib. Vorbește SMTP pe bune: răspunde la HELO/EHLO, MAIL FROM, RCPT TO, dansul cu terminatorul de DATA, RSET, NOOP și QUIT, rulând pe propriul thread de fundal. Testul conduce apoi endpointul real /send al aplicației FastAPI printr-un TestClient, care cheamă clientul smtplib real, care deschide un socket real către serverul ăla fals și trimite octeți de protocol SMTP pe el. Testul decodează apoi ce a aterizat efectiv cu email.message_from_bytes și face aserțiuni pe headerele parsate și pe plic, inclusiv confirmând că destinatarii BCC ajung în lista RCPT TO a plicului SMTP dar nu se scurg niciodată în headerele To sau Cc ale corpului mesajului, ceea ce e tot rostul lui BCC și un lucru pe care e șocant de ușor să-l greșești într-un expeditor de mail făcut de mână.

Mailul Tău Rămâne Al Tău

Nu încerc să te conving că să-ți găzduiești singur propriul proxy de IMAP e ce ar trebui să facă “oamenii normali”. Nu e. Ce îți spun e că, dacă ești genul de om care deja își rulează propria infrastructură de AI, și dacă ai citit aigate, probabil ești: nu există niciun motiv ca mailul tău să fie singurul lucru pe care tot îl dai unei terțe părți. Containerul ăsta le dă agenților tăi, scripturilor tale și pipelineurilor tale de curl de la 3 noaptea atâta control de citit/trimis/șters peste atâtea mailboxuri câte ai, cât alegi să dai fiecărui token, pe un singur port, cu zero octeți din mailul tău propriu-zis atingând vreodată discul lui. Omoară-l, repornește-l, mută-l pe alt host: singurul lucru care merită cărat mai departe e un fișier SQLite mic cu rânduri de audit și contoare, iar mailul tău n-a fost niciodată în el.

Ia-l de la github.com/psyb0t/docker-mailbox sau trage psyb0t/mailbox direct de pe Docker Hub. Sursa e WTFPL, fă ce pula mea vrei cu ea. Imaginea publicată e alt animal: împachetează html2text, care e GPL-3.0, deci imaginea ca întreg e o operă combinată GPL (THIRD_PARTY.md din repo are detaliile). Pune-o pe mailul tău și nu te mai gândi la ea niciodată.

Cum Ți-l Instalezi În Agent

Un agent care îți poate citi mailul ar trebui măcar să știe API-ul fără să-i spui de două ori. Tot ce e sub .agents/ e catalogat într-un singur marketplace, deci sunt două comenzi:

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

Codex folosește același marketplace cu alt verb: codex plugin add mailbox@psyb0t, pentru că nu există codex plugin install. Găsește skill-ul și singur într-un checkout al repoului, pentru că scanează nativ .agents/skills/ fără să fie instalat absolut nimic. E listat acum și pe MCP Registry-ul oficial, deci un client care își rezolvă serverele de acolo îl poate găsi fără să-i dea cineva un URL.