postr lets you send, receive, and manage emails from a single scrolling terminal buffer — all powered by your own Cloudflare account. Inbound mail arrives via Cloudflare Email Routing, each mailbox is isolated in its own Durable Object with a SQLite database, and attachments are stored in R2. An AI layer on top of Workers AI can summarize threads, draft replies, search across mail, and triage your inbox.
The interaction model borrows from Claude Code: a single scrolling column, a › prompt, slash commands, and a small set of single-key bindings. No panes, no mouse, no chrome.
Inspired by Cloudflare's agentic-inbox — postr re-targets the same Workers + Durable Objects + Email Routing architecture onto a terminal client written in Rust, with the worker also ported to Rust via workers-rs.
-
Build and deploy the worker
cd worker ./tools/install.sh # one-time, installs pinned worker-build into worker/tools/ npx wrangler deploy
You'll be prompted for
DOMAINS— the domain you want to receive mail for. -
Set up Email Routing — Create a catch-all rule on your domain that forwards to this worker.
-
Enable Email Service — The worker needs the
send_emailbinding for outbound mail. See Email Service docs. -
Set the CLI token
npx wrangler secret put CLI_TOKEN
-
Build, log in, create a mailbox
cd ../cli && cargo build --release ./target/release/postr login https://<your-worker>.workers.dev ./target/release/postr mailbox add me@yourdomain.com --name "Your Name" --alias work ./target/release/postr tui
mailbox addwrites the marker R2 object the worker checks for; the address must be on a domain whose Email Routing forwards to this worker. The optional--nameis attached to outboundFrom:headers, e.g."Your Name" <me@yourdomain.com>. The optional--aliasis a short label for/switch <alias>in the TUI. Manage existing mailboxes withpostr mailbox list,postr mailbox update <addr> --name "..." --alias "..."(use--clear-name/--clear-aliasto remove), andpostr mailbox remove <addr>.To populate a fresh mailbox with curated sample messages — useful for screenshots or a quick tour — run
postr demo-seed <addr>. It's idempotent.
worker-build 0.8.4 is the sweet spot: it externalizes cloudflare:email in its esbuild step (broken in 0.8.1) but doesn't yet pass --force-enable-abort-handler to wasm-bindgen (added in 0.8.5, which requires an externref table that Rust's wasm32-unknown-unknown doesn't currently emit). tools/install.sh drops 0.8.4 into worker/tools/worker-build/; wrangler.jsonc's build.command points there. Tracked in TODO.md.
Token rejected.— The worker'sCLI_TOKENwas rotated. Runpostr loginagain.Could not resolve "cloudflare:email"/externref table required for catch wrappers— Run./worker/tools/install.shto (re-)install the pinnedworker-build.- Boxes instead of glyphs in the TUI — Use a Nerd-Font-capable terminal font.
- Keyboard-first TUI — single scroll buffer,
›prompt, slash commands, no mouse - Multi-mailbox — switch with
/switch <addr|alias>, or/switch allfor a unified inbox; rows show a#mailboxcolumn so you can tell at a glance - Folders —
/folderfor a picker (inbox, archive, sent, drafts, trash) or/folder <name>to jump directly - Soft-delete + 30-day trash —
dmoves to trash; inside trash the same key permanently purges. A daily cron sweeps trashed messages older than 30 days and frees the R2 attachment blobs - Multi-selection —
Spacetoggles a row, thens/e/d/mapply over the whole set; the inbox title shows the count - Read state —
mtoggles read/unread on the highlighted row (or every selected row);/mark-all-readclears the whole folder - Reply + forward —
r/a/fopen compose pre-seeded withOn <date>, <sender> wrote:+ quoted body; per-mailbox--namepopulatesFrom:for outbound mail - AI slash commands —
/summarize,/draft <prompt>,/ask <query>,/triagebacked by Workers AI; suggested-reply pills feed straight into compose - Per-mailbox isolation — each mailbox runs in its own Durable Object with
state.storage().sql()+ R2 for attachments - Bring-your-own-cloud — runs entirely on your Cloudflare account; no Anthropic, no Gmail, no third-party email server
| Where | Keys |
|---|---|
| Inbox nav | j/k/↑/↓ select · 1–9 jump · ⏎ open · g/G top/bottom |
| Inbox actions | s star · e archive · d trash/delete · m read · u undo · c compose · r refresh |
| Multi-select | Space toggle · Esc clear · s/e/d/m batch over selection |
| Reading | j/k next/prev message · z toggle quoted · r/a reply · f forward · e/d/s/m apply to open message |
| Prompt | / opens command popover · type to filter · ⏎ runs · Esc clears |
| Anywhere | ? shortcuts overlay · q back to inbox (and q again to quit) · ⌃c hard quit |
- CLI / TUI: Rust, ratatui, crossterm,
tui-textarea, tokio, reqwest (rustls),keyring - Worker: Rust,
workers-rs0.8.5,mail-parser, hand-rolledworker::Router(no Hono) - Storage: Durable Object SQLite (one DB per mailbox, 8 migrations preserved from agentic-inbox) + R2 attachments
- AI: Workers AI —
@cf/moonshotai/kimi-k2.5(summarize/draft/triage) +@cf/meta/llama-4-scout-17b-16e-instruct(ask filter inference) - Auth: Bearer token for the CLI (
CLI_TOKENset viawrangler secret put)
After deploying the worker, run postr login <worker-url> then postr tui. Press / in the TUI to open the slash menu, or run postr --help for the CLI.
- Cloudflare account with a domain
- Email Routing + Email Service enabled
- Workers AI enabled
- Rust 1.95+ with the
wasm32-unknown-unknowntarget - A truecolor terminal with a Nerd-Font-capable monospace font
┌─────────────┐ HTTPS+Bearer ┌────────────────┐ stub.fetch ┌──────────────┐
│ postr CLI │ ─────/api/v1/*────▶│ postr-worker │─────/rpc/*──────▶│ MailboxDO │
│ ratatui │ │ workers-rs │ │ SQLite + R2 │
└─────────────┘ └────────┬───────┘ └──────────────┘
│
env.ai("AI") │ Workers AI
▼
/summarize /draft /ask /triage
Inbound: Email Routing → #[event(email)] → mail-parser → MailboxDO
Outbound: Compose → /rpc/create_email → env.send_email("EMAIL").send(...)
Apache 2.0 — see LICENSE.
