Website: parnoldx.github.io/mailbox-cli
mailbox is a fast, agent-oriented CLI and background daemon for email, calendars, tasks, daily habits, and contacts.
Instead of hitting IMAP, SMTP, CalDAV, CardDAV, and ManageSieve servers on every command, a single background daemon maintains a local SQLite Mirror of server state. Read commands are answered directly from the local mirror in milliseconds with zero network latency, while write operations synchronize with remote servers to guarantee consistency.
┌────────────────────────────────────────────────────────┐
│ Agents / Scripts / Widgets │
│ (mailbox CLI, mailbox.clock, Skills) │
└───────────────────────────┬────────────────────────────┘
│ NDJSON over Unix Socket
┌───────────────────────────▼────────────────────────────┐
│ mailbox daemon │
│ ┌─────────────────────┐ ┌──────────────────┐ │
│ │ SQLite Mirror │ │ Durable Outbox │ │
│ └──────────▲──────────┘ └─────────┬────────┘ │
└──────────────┼────────────────────────────┼────────────┘
│ IMAP / CalDAV / CardDAV │ SMTP
┌──────────────▼────────────────────────────▼────────────┐
│ Mail & DAV Servers │
└────────────────────────────────────────────────────────┘
- Instant Local Reads: Commands like
mailbox status,mailbox box view,mailbox agenda, ormailbox searchread from the local SQLite mirror (~1ms response time) and work offline. - Write-Through Consistency: Modifying commands (
move,seen,event add,todo done,route set) wait for the server acknowledgment before updating the mirror, ensuring an exit code of0means the change is live. - Durable Outbox: Sent mail is staged in a durable local queue before SMTP submission and retained for auditing and automatic retry.
- Server-Side Sieve Routing: Owns and compiles Sieve filtering scripts on the server to automatically route mail into
Inbox,Feed,Paper Trail,Screener, orBlock. - First-Class Agent Support: Built-in
--jsonenvelopes with mirror freshness metadata, self-describing command discovery (mailbox commands), predictable exit codes, and an agent skill (skill/SKILL.md). - Unified Personal Data Surface: Covers email threads and attachments alongside RFC 5545 iCalendar (events, todos, habits) and RFC 6350 vCard (contacts).
- Login Codes Collected, Not Delivered: A one-time code, magic link or registration link is recognised on arrival, copied to the clipboard with a desktop notification, marked read so it never raises a new-mail alert or a screener decision, and binned a quarter of an hour later.
The daemon keeps an offline-capable SQLite database representing the read model of all configured mailboxes and collections.
- Freshness & Behind Notices: Every response includes mirror sync metadata. If the mirror has pending changes or disconnected networks,
mailboxreportsbehind: truerather than failing or blocking. - Disposable: The mirror can be deleted and reconstructed at any time; no authoritative local-only data is lost.
The outbox stores outgoing messages on disk. If a network disruption occurs during submission or if the daemon restarts, the message enters a Held state rather than being silently dropped or endlessly duplicated.
Mail routing uses a structured Sieve script managed directly on the server:
- Inbox: Important mail requiring attention.
- Feed: Newsletters, automated updates, and reading material (marked read on arrival).
- Paper Trail: Receipts, bills, delivery notices, and transactions (marked read on arrival).
- Screener: Mail from first-time or unknown senders awaiting a routing decision.
- Block: Unwanted senders dropped at the server.
A login code is not mail to read, it is a token to collect: worth thirty seconds
and then worth nothing. The daemon recognises one as it arrives in the Inbox or
the Screener, copies it to the clipboard (wl-copy), raises a notification
(notify-send), marks the mail read, and moves it to Trash fifteen minutes
after it landed.
A magic link or a registration/activation link is the same errand and is treated the same way: the URL itself goes on the clipboard, and the notification names the host rather than the token. Copied, never followed — opening it would log you in, or confirm an address, from a notification you had not read yet.
Nothing about a pickup asks for a decision. It never counts as unread, never
appears in mailbox screener, and a watch is told pickup rather than
added/new — the sender is a login form you used once, not somebody to route.
A notification is not always a way to reach you: under Do Not Disturb it is
silenced, and the code is already on the clipboard with nothing on screen
saying so. So a collected pickup raises the mailbox bar icon — a key glyph for
a minute after the code landed, which is the window in which it is still worth
something — and the panel under it lists what is ready to paste for the whole
fifteen minutes the mail is kept, clicking a row or its button to copy it
again. Both read mailbox pickup list, which is the same $pickup mail the
expiry scan bins, so nothing can outlive the code:
mailbox pickup list # Codes and links still held, with their age
mailbox pickup copy Screener:2340 # Hand one over againDetection gates on the subject and only then reads the body. That is the opposite of the usual approach and it is a measured choice: over 1445 real messages, body-first matching on keywords, bare code-shaped lines or opaque token URLs produced 200+ hits and one true positive, because order confirmations and booking references are indistinguishable from one-time codes once you stop reading the subject line. English and German are both covered. Only mail that arrived in the last fifteen minutes is considered at all.
A mail whose subject matched but which carried nothing to collect is logged rather than acted on, so the phrase list can be tuned against real arrivals:
journalctl --user -u mailbox.service -f | grep pickupExpired pickups go to Trash, not oblivion, so a code you turn out to still need is recoverable for as long as the server holds it.
Because the daemon takes the urgent mail out of the Screener before anything else sees it, the bar widget has no screener in it at all — not an icon, not a count, not a triage row. Screening is a decision owed whenever you next sit down, so it lives in the desktop client. The widget only says "new inbox mail arrived".
- Linux (systemd user session recommended)
- Go 1.22+
- Node.js (optional, for running plugin test suites)
# Build the binary into bin/mailbox
make build
# Run unit tests
make test
# Install the CLI to ~/.local/bin/mailbox
make install
# Install the agent skill to ~/.agents/skills and ~/.claude/skills
make skill
# Optional extras (each is opt-in):
make install-gui # the Qt desktop client -> ~/.local/bin/mailbox-gui + .desktop entry (needs Qt 6 + WebEngine)
make install-plugins # the Quickshell bar widgets -> ~/.config/omarchy/plugins/
make install-all # CLI + skill + GUI + widgets, all of the above
make register-mailto # make mailbox-gui the default handler for mailto: links (after install-gui)Run the interactive setup wizard to discover folders, calendars, address books, and configure credentials:
mailbox setupConfiguration is stored securely in ~/.config/mailbox/config.toml (mode 0600).
Optional settings that the wizard does not ask about:
[pickup]
expiry = "15m" # how long a collected login code's mail is kept before Trash
[fileee]
address = "abc123@in.fileee.com" # where the GUI's "send to fileee" button mails a PDFYou can run the daemon directly in the foreground:
mailbox daemonOr enable the systemd user service and socket (configured during mailbox setup):
systemctl --user enable --now mailbox.socketmailbox status # Summary of unread/screener mail & mirror health
mailbox box list # List all boxes and message counts
mailbox box view Screener --limit 20 # View recent messages in a box
mailbox message view 36722 # View full headers and text of a message
mailbox thread 36722 # View an entire conversation thread
mailbox search "rechnung" --in feed # Full-text search across mirrored messages
mailbox attachment list 36722 # List files attached to a message
mailbox attachment save 36722:1 # Save attachment to diskmailbox compose --to user@example.com --subject "Hello" --body "Message body"
mailbox reply 36722 --body "Thanks for the update."
mailbox forward 36722 --to colleague@example.com
mailbox compose --to user@example.com --subject "Draft" --body "WIP" --draft # File in drafts instead of sending
mailbox rsvp 36722 --accept # Accept a meeting invite (iMIP + calendar)
mailbox unsubscribe 36722 # Leave the list this came from (one-click POST, mailto, or a URL to open)
mailbox draft list # Mail written but not yet sent
mailbox draft send 12 # Send a draft (optionally override --to/--subject/--body)
mailbox outbox list # View queue status and held messages
mailbox outbox retry 1 # Retry a held outgoing messagemailbox screener # List senders waiting for a routing decision
mailbox route set sender@domain --to feed # Route sender to Feed
mailbox route set sender@domain --to inbox # Route sender to Inbox
mailbox route set sender@domain --to paper # Route sender to Paper Trail
mailbox route set sender@domain --to block # Block sender at server level
mailbox route set @stripe.com --to paper # Every address at that domain
mailbox aside add 36722 # Put a message into the read-later pile
mailbox reply-later add 36722 # Put a message into the reply-later pile
mailbox move 36722 --to Archive # Move message to a box
mailbox seen 36722 # Mark message as read
mailbox unseen 36722 # Mark message as unread
mailbox trash 36722 # Move message to Trash
mailbox spam 36722 # Move message to Junk
mailbox label add 36722 --to Rechnungen # Put a label on a message
mailbox label list # Labels, and how much mail carries each
mailbox label view Rechnungen # Mail carrying a labelLogin codes need no command: the daemon collects them by itself. To watch them happen, or to hand one to a script of your own:
mailbox watch --events pickup # One line per code collected
mailbox watch --events pickup --run-async 'echo "$MAILBOX_CODE"'To find one again after the clipboard has moved on — the bar icon does the same thing when you click it:
mailbox pickup list # Codes and links still held, with their age
mailbox pickup copy Screener:2340 # Back on the clipboardmailbox agenda --days 7 # Show upcoming events and due tasks
mailbox calendar list # List all discovered calendars and task lists
mailbox event add "Team Sync" --start "tomorrow 10:00" --end "tomorrow 11:00"
mailbox todo list # List active tasks
mailbox todo add "Pay invoice" --due "2026-09-01"
mailbox todo done 42 # Mark task completed
mailbox habit list # View daily habits and streaks
mailbox habit done "meditation" # Log today's habit completion
mailbox contact search "Jane" # Search address books
mailbox contact add "Jane Doe" --email jane@example.com --phone "+1 555 0199"mailbox doctor # Inspect daemon, socket, mirror, and server connectivity
mailbox sieve get # Retrieve active server Sieve script
mailbox commands # Output machine-readable command registry
mailbox version # Print version informationEvery command supports --json, returning a structured envelope:
{
"ok": true,
"data": { ... },
"mirror": {
"behind": false,
"synced_at": "2026-08-30T16:00:00Z"
}
}0: Success (server acknowledged for writes, mirror answered for reads).1: Usage error (invalid syntax, missing argument, unknown flag).2: Not found (no message, event, contact, or item matches the ID).7: Server or daemon error (network down, credential rejected, server failure).9: No daemon listening on the Unix socket.
- Agent Skill:
skill/SKILL.mdprovides instruction mappings for AI coding assistants. - Calendar Bar Widget:
plugins/mailbox.clock/— Omarchy / Quickshell calendar and reminder widget backed directly by the daemon socket. - Mail Notification Widget:
plugins/mailbox.email/— Omarchy bar widget and dropdown panel for new inbox mail alerts, also on the daemon socket. - Desktop Client:
gui/— a HEY-style Qt desktop mail client that follows the live Omarchy theme.
| Variable | Description | Default |
|---|---|---|
MAILBOX_CONFIG |
Path to configuration file | ~/.config/mailbox/config.toml |
MAILBOX_SOCKET |
Path to Unix socket | $XDG_RUNTIME_DIR/mailbox.sock |
MAILBOX_MIRROR |
Path to SQLite mirror database | ~/.local/share/mailbox/mirror.db |
MAILBOX_OUTBOX |
Path to outbox spool directory | ~/.local/share/mailbox/outbox/ |
MAILBOX_FOLDER |
Comma-separated boxes to mirror (dev only) | (All boxes) |
# Run unit tests across all packages
make test
# Format code
make fmt
# Static analysis and live tag verification
make vet
# Run live server integration tests (requires live config in ~/.config/mailbox/config.toml)
make live LIVE=./internal/sievedrv/
make live LIVE=./internal/davdrv/
make live LIVE=./internal/imapdrv/Design notes and the numbered decisions (ADR-00xx, cited from the code) are in docs/DESIGN.md; the vocabulary is in CONTEXT.md.
MIT — see LICENSE.

