Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

What Postio is

Postio is a local-first, keyboard-first email client built for people who have too much email.

Read less. Find anything. Act faster.

If you’ve used a mail client that makes you wait — for the inbox to load, for search to come back, for a click to register — Postio is built to never do that. It keeps a full copy of your mail in a local database with a built-in search index, so opening the app, searching, and moving around never touch the network. Every action you take — archive, flag, move, delete, undo — applies instantly to that local copy and is sent to the server in the background. You are never staring at a spinner waiting for your own mailbox to respond.

The three things it has to be better at

Speed. Startup, navigation, and search are held to a real budget — under half a second to a usable inbox, under 100ms for a search — and it’s checked automatically, not just claimed.

Search. Search isn’t a box in the corner; it’s a primary way to move through your mail. from:ada after:2026-01-01 has:attach is a query you can type, save, or pin as a folder — one language, everywhere it appears.

Keyboard. Every action has a shortcut. j/k move, e replies, a archives, u undoes anything, / searches, Ctrl+K opens the command palette, ? shows the full cheat sheet. The mouse works too, and is never required.

What v1 does

One IMAP + SMTP account, authenticated with a password or an app-specific password. Inbox, folders, threads. Read/unread, archive, delete, flag, move. HTML and plain-text reading, attachments, quoted-message folding. Compose, reply, reply-all, forward, drafts. Local full-text search with operators. Vim-style navigation and a command palette, every binding rebindable. Background sync, full offline reading, undo.

What it deliberately doesn’t do yet

Postio is Linux-only for now (GTK4/libadwaita), and v1 has no AI features — not because they aren’t planned, but because shipping AI over a mediocre mail client would just produce a mediocre mail client with AI in it. Core mail, search, and the keyboard come first. Rules, contacts management, and snooze/scheduled send are also out of v1, each with its own tracked issue.

Ready to try it? See Installing Postio.

This is the reference documentation. The Postio home page is the wider tour: what it looks like, what it is for, and where the project stands.

Installing Postio

Postio is pre-release and, for now, Linux only: GTK4/libadwaita, Wayland first, X11 where it happens to work. There’s no packaged release yet, so installing it means building from source.

System dependencies

Fedora 40+:

sudo dnf install gtk4-devel libadwaita-devel webkitgtk6.0-devel \
                 sqlite-devel libsecret-devel glib2-devel pkgconf-pkg-config

Ubuntu 26.04 (earlier releases ship a GTK older than Postio’s floor):

sudo apt install build-essential pkg-config libgtk-4-dev libadwaita-1-dev \
                 libwebkitgtk-6.0-dev libsqlite3-dev libsecret-1-dev \
                 libglib2.0-dev libpango1.0-dev

Rust is pinned by the project’s rust-toolchain.toml — with rustup installed, the right compiler arrives automatically on your first cargo command.

Build and install

git clone https://github.com/dlapiduz/postio.git
cd postio
scripts/install-local.sh               # builds --release, installs to ~/.local

That puts postio on your $PATH and adds it to your app grid, with its icon. scripts/install-local.sh --uninstall removes exactly what it installed.

Prefer to just try it without installing anything?

cargo run -p postio-app

Prefer a sandboxed build? Postio ships a Flatpak manifest under flatpak/ that builds against the GNOME 50 runtime — see flatpak/README.md in the repository for the one-time SDK setup. Postio isn’t on Flathub yet, so for now that means building the Flatpak yourself too.

First run

First run opens onto a one-screen setup: type your email address, and Postio’s autoconfig probe fills in the server settings for you (checking a built-in provider table, then Thunderbird’s autoconfig service, then DNS SRV records — or you can enter everything manually). Your password goes straight into your desktop’s keyring; it is never written to a file. iCloud accounts need an app-specific password, generated at https://account.apple.com.

From there, drive it from the keyboard: j/k to move, Enter to open, e to reply, a to archive, u to undo anything, / to search (from:ada is:unread …), Ctrl+K for the command palette, ? for the full cheat sheet. Every binding is rebindable — see the keyboard reference.

Troubleshooting

cargo build fails looking for a library (a pkg-config error naming gtk4, libadwaita-1, webkitgtk-6.0, sqlite3, or libsecret-1): a system dependency from the list above is missing or too old. Check what you have against what’s needed with pkg-config --modversion gtk4 (and so on for the others).

The window fails to open, or opens with broken rendering: only Wayland is verified. If you’re on X11 and hit a rendering issue, try a Wayland session first, or force the X11 backend explicitly with GDK_BACKEND=x11 cargo run -p postio-app before filing an issue.

Onboarding won’t save the account, or every launch reopens onboarding: Postio stores credentials in your OS keyring over the Secret Service D-Bus API, which needs a running keyring daemon — GNOME Keyring or KWallet’s Secret Service integration are the common ones. Minimal desktop environments often don’t start one by default; on Fedora, sudo dnf install gnome-keyring and make sure your session starts it. A locked keyring blocks the same way — unlock it and try again.

Keyboard reference

Every command below is also in the Ctrl+K palette and the ? cheat sheet, because all three are generated from one table.

Bindings come from the design canvas — e replies, not r. docs/PRODUCT.md §8 records that resolution; this table is the registry.

Rebinding

Every binding is overridable from the [keys] section of config.toml, keyed by the command id in the last column:

[keys]
archive = "y"
first_message = "g g"

A chord joins modifiers to a key with + (ctrl+k); a sequence separates chords with a space (g g). Shift is written into the character, so A is what you get by holding shift — a and A are different bindings. An override that cannot be used, or that collides with a key already taken in the same place, is reported in the settings panel and the command keeps its default.

mod is the primary accelerator: Control here, Command on macOS. Every default above uses it, which is why the same config.toml means the same thing on both. Writing ctrl instead pins the binding to Control everywhere.

While you are typing, single-key bindings do not fire. Only Escape, the function keys, and chords holding Ctrl, Alt or Super reach a command from inside a text field.

Bindings

KeysCommandWhereUndoId
j or DownNext messageList, conversation, reader, searchnext_message
k or UpPrevious messageList, conversation, reader, searchprev_message
g gFirst messageList, conversation, reader, searchfirst_message
GLast messageList, conversation, reader, searchlast_message
Return or l or RightOpen messageList, conversation, searchopen_message
xToggle selectionList, conversation, reader, searchtoggle_selection
J or shift+DownExtend selection downList, reader, searchextend_selection_down
K or shift+UpExtend selection upList, reader, searchextend_selection_up
ctrl+aSelect allList, conversation, reader, searchselect_all
h or LeftPrevious viewList, conversation, readerprev_view
EscapeBackEverywhereback
oToggle result orderSearchtoggle_result_order
JNext message in conversationConversationnext_in_conversation
KPrevious message in conversationConversationprev_in_conversation
zFold or unfold this messageConversationtoggle_fold
ctrl+oView originalList, conversation, readerview_original
OExpand allConversationexpand_all
IHide or show the conversation railConversationtoggle_rail
eReplyList, conversation, reader, composerreply
EReply to allList, conversation, reader, composerreply_all
fForwardList, conversation, reader, composerforward
aArchiveList, conversation, readerUndoablearchive
AArchive threadList, conversation, readerUndoablearchive_thread
dDeleteList, conversation, readerUndoabledelete
mMove to…List, conversation, readerUndoablemove
sFlagList, conversation, readerUndoableflag
UMark unreadList, conversation, readerUndoablemark_unread
bSnoozeList, conversation, readerUndoablesnooze
BUnsnoozeList, conversation, readerUndoableunsnooze
LAdd label…List, conversation, readerUndoableadd_label
/SearchList, conversation, readersearch
ctrl+sSave search as folderSearchsave_search
cComposeList, conversation, readercompose
ctrl+ReturnSendComposerUndo brieflysend
ctrl+shift+ReturnSchedule send…Composerschedule_send
ctrl+sSave draftComposersave_draft
ctrl+dDiscard draftComposerAsks firstdiscard_draft
ctrl+shift+mMark as sentList, composermark_sent
ctrl+shift+rRetry sendList, composerretry_send
ctrl+shift+xCancel sendList, composercancel_send
ctrl+shift+aAttach file…Composerattach_file
ctrl+shift+oDetach composerComposerdetach_composer
ctrl+shift+cCc and BccComposercopy_fields
ctrl+shift+gInsert image…Composerinsert_image
ctrl+bBoldComposerbold
ctrl+iItalicComposeritalic
ctrl+shift+8Bulleted listComposerbullet_list
ctrl+shift+7Numbered listComposernumbered_list
ctrl+shift+kInsert link…Composerinsert_link
ctrl+shift+9Quote blockComposerquote_block
uUndoList, conversation, reader, account listundo
ctrl+kCommand paletteEverywherecommand_palette
?Keyboard shortcutsList, conversation, readercheat_sheet
ctrl+commaSettingsEverywheresettings
ctrl+shift+nAdd accountEverywhereadd_account
ctrl+eEdit configurationList, conversation, readeredit_config
ctrl+bToggle sidebarList, conversation, readertoggle_sidebar
g fFocus the folder listList, conversation, reader, searchfocus_sidebar
tabNext paneList, conversation, reader, folder listcycle_pane
shift+tabPrevious paneList, conversation, reader, folder listcycle_pane_back
j or DownNext folderFolder listnext_folder
k or UpPrevious folderFolder listprev_folder
spaceExpand or collapse folderFolder listtoggle_folder
rRename saved searchFolder listrename_saved_search
shift+UpMove saved search upFolder listmove_saved_search_up
shift+DownMove saved search downFolder listmove_saved_search_down
dDelete saved searchFolder listAsks firstdelete_saved_search
ReturnEnable or disable accountAccount listtoggle_account_enabled
dRemove accountAccount listUndoableremove_account
cUpdate account credentialAccount listupdate_credential
rRebuild search indexAccount listrebuild_account_index
mSet as default accountAccount listset_default_account
MMap mailbox roleAccount listUndoablemap_mailbox_role
g aNext scopeList, folder listnext_scope
F5 or RRefreshList, conversation, readerrefresh
pShow message partsReaderopen_parts
j or DownNext partParts panelnext_part
k or UpPrevious partParts panelprev_part
ReturnOpen partParts panelopen_part
sSave partParts panelsave_part
SSave all partsParts panelsave_all_parts
xOpen part externallyParts panelopen_part_externally
HRender part onceParts panelrender_part_once
Page_Down or spaceScroll reading pane downList, conversation, readerscroll_reader_down
Page_Up or shift+spaceScroll reading pane upList, conversation, readerscroll_reader_up

Configuration reference

~/.config/postio/config.toml is the settings – there is no separate store. A missing or empty file is not an error: every key below has a working default, and Postio writes a starter file on first run so there is something to find and edit rather than a blank buffer. The file is watched and re-parsed live; a key this build does not recognise survives a round trip untouched, in case a newer Postio wrote it.

[ui]

KeyTypeDefaultDescription
densitystring"airy"Message-list row height: airy, comfortable or compact.
themestring"system"Light/dark preference: system (follows the desktop), light or dark.
show_hover_actionsbooleantrueShow per-row actions when the pointer rests over a row.
show_key_hintsbooleantrueShow the focused row’s key hints (e reply, a archive). Off leaves every binding in force – this only stops the row from naming them.
sender_avatarsbooleantrueShow each row’s sender-initials chip.

[sync]

KeyTypeDefaultDescription
check_for_mailstring"idle"How Postio learns about new mail: idle (hold an IDLE connection on INBOX for push delivery), poll (no IDLE, every mailbox reconciled on poll_interval_secs), or manual (never checks on its own).
poll_interval_secsinteger300Polling interval for folders without IDLE, in seconds.
max_connectionsinteger5Maximum simultaneous IMAP connections per account.
sync_on_startupbooleantrueStart a sync as soon as the app opens.
body_fetchstring"lazy"When message bodies are downloaded: lazy (headers first, bodies backfilled) or eager.
attachment_fetchstring"on_open"When an attachment’s bytes are downloaded: on_open, eager, or never.
max_inline_bytesinteger262144The largest inline part fetched with the message’s text rather than left on the payload axis. A cid: image under this size arrives with the body, so HTML mail reads correctly offline; 0 turns the rule off.
initial_sync_messagesinteger5000How many messages the first sync reaches back for, newest first.
notifybooleantrueMaster switch for desktop notifications on new mail.
notify_rolesarray of strings["inbox"]Which mailbox roles produce a notification when mail arrives in them.

[storage]

KeyTypeDefaultDescription
max_bytesintegerunset (no limit)Ceiling on the local blob store, in bytes. Omit the key for no limit – the store is a cache and may evict what is refetchable, never message text or drafts.

[compose]

KeyTypeDefaultDescription
signature_on_replystring"above_quote"Where the signature goes on a reply: above_quote or below_quote.
signature_on_forwardstring"above_quote"Where the signature goes on a forward.

[logging]

KeyTypeDefaultDescription
levelstring"info"How much to say, when filter does not say something more specific: off, error, warn, info, debug or trace.
filterstring""A per-target override in EnvFilter syntax, e.g. "postio_sync=debug,io_imap=trace". Empty means “just use level”.
timestampsbooleantruePrefix each log line with the time it was emitted.

[keys]

Overrides a command’s binding, keyed by the command id. See the keyboard reference for every id and its default.

[keys]
archive = "y"
first_message = "g g"
command_palette = "mod+p"

mod is the primary accelerator – Control on Linux, Command on macOS – so one file means the same thing on both. Write ctrl when you mean the Control key specifically; it stays literal everywhere.

[accounts.<id>]

One table per account, keyed by a short id you choose. Servers, security and the login name – never a password, which lives in the OS keyring and never touches this file.

[accounts.personal]
email = "ada@example.com"
display_name = "Personal"
default = true

[accounts.personal.imap]
host = "imap.example.com"
port = 993
security = "implicit-tls"

[accounts.personal.smtp]
host = "smtp.example.com"
port = 465
security = "implicit-tls"

[filters.<id>]

A named, pinned search – one table per saved search, keyed the same way accounts are.

[mailboxes]

Maps a role Postio already knows (archive, sent, trash, …) to the exact folder path your server uses for it, when autodetection guesses wrong. Keyed by role, valued by path – the way [keys] is keyed by the thing you mean and valued by its spelling.

[mailboxes]
archive = "Archive/2024"

This table applies to every account. That is the right default for the ordinary installation, which has one account, and the wrong one the moment two accounts disagree about where their sent mail lives – a fix for iCloud that breaks Gmail on the same machine. So it is the default, not the answer: each account can map a role itself, in Settings -> Accounts, and its own choice wins (ADR 0035).

The full precedence, per account and per role:

  1. the account’s own map, chosen in Settings -> Accounts
  2. this [mailboxes] table
  3. the server’s SPECIAL-USE attribute
  4. a guess from the folder’s name

Two consequences worth knowing:

  • A choice made in settings takes effect on the next sync pass, because discovery reads the store’s map every time. Editing this file needs a restart, because the file is read once at startup.
  • A mapping that names a folder the account no longer has is shown as dangling in Settings -> Accounts rather than silently ignored. A role quietly falling back to a guess is how mail ends up filed somewhere the user did not choose and cannot see they did not choose.

Nothing here moves mail. Re-pointing a role changes which folder wears the label from that moment on; the messages already in the old folder stay where they are.

Every account ends up with a folder for all six roles. When one resolves to nothing after all four tiers, Postio creates it on the server – once, never for the Inbox, and named after the role. A server that refuses is not asked again: the role is shown as unmapped in Settings -> Accounts with the server’s own words beside it, which is usually a permission and usually something you can fix.

How sync works

Postio keeps a complete local replica of your mail: a SQLite database for everything listable and searchable, plus a content-addressed blob store for raw messages and attachments. Every screen you look at — the inbox, a thread, a search result — is read from that local copy, never fetched live from the server. That’s what makes the app instant: there’s nothing to wait for.

What happens in the background

A sync engine, running separately from anything you’re looking at, keeps that local copy up to date:

  • New mail arrives quickly. Where the server supports IMAP IDLE, Postio holds a connection open on your inbox for push delivery. Where it doesn’t, Postio polls at an interval you can configure.
  • Message text backfills to completion, not just the newest few hundred. Every message in every folder you haven’t excluded eventually gets its full text pulled down, in the background, so search and offline reading cover your whole mailbox — not just what’s recent. You can exclude a folder explicitly; nothing is excluded by default.
  • Attachments are lazy. Attachment bytes are typically nine-tenths of a mailbox by weight and contribute nothing to search but their filename, so they download when you open or save one, not proactively. (Small inline images used for rendering HTML mail are the exception — those come with the text, so HTML mail reads correctly offline.) You can ask Postio to fetch attachments eagerly if you want a complete offline archive, or turn attachment fetching off entirely on a metered connection.
  • A dropped connection reconnects on its own, backing off if the server or network keeps failing, and picks up where it left off.

Every action is instant, and queues afterward

When you archive, delete, flag, move, or send something, Postio doesn’t wait for the server before showing you the result. The sequence is always: write to the local database, add the change to a queue, update what you see — in that order, and all of it happens before anything touches the network. The sync engine drains that queue in the background and reconciles with the server afterward.

That’s also why undo is instant: pressing u reverses the local change right away, without waiting for a round trip. A burst of actions — say, archiving twelve messages in a row — counts as one undoable unit, not twelve.

Fully usable offline

Because reading, search, and every mutating action work against the local copy first, Postio works fully offline after its first sync: read, search, compose, reply, forward, archive, delete, move, label, and mark read/unread all work with no connection. It’s not a special “offline mode” — it’s the same code path either way, which is exactly why it’s reliable. Anything you do offline queues locally and reconciles automatically once the connection comes back.

Privacy and security

Email is probably the most sensitive thing on your computer, and mail is attacker-controlled content that actively tries to phone home. Postio’s commitment is one sentence: nothing leaves this machine that you did not ask for. Concretely, that means:

  • Remote images and tracking pixels are blocked by default, and stay blocked per sender until you explicitly allow them. There is no global “always load images” switch — that would defeat the point.
  • Read receipts are never sent automatically. Disposition-Notification-To is tracking with a friendly name, and Postio treats it as such.
  • One-click unsubscribe (List-Unsubscribe) only fires when you deliberately click it. Sending it automatically would confirm to a sender that your address is live — which is exactly what a spammer wants to learn.
  • No link prefetch, no favicon fetching, no speculative connections of any kind. The message reader has JavaScript and network access turned off entirely; inline (cid:) images resolve from the local blob store, not the network.
  • Forwarding and replying can’t be used to smuggle out an attack. Quoted content is sanitized on the way in, and the mail Postio sends is generated fresh from its own internal document — never a pass-through of whatever HTML arrived. A phishing email you forward can’t make the recipient’s client run something your own client already protected you from.
  • No telemetry, no crash reporting, no update ping. Postio doesn’t know you’re using it, and neither does anyone else.
  • Your local mail store is encrypted at rest. Because Postio backfills a complete copy of your mailbox rather than a recent slice, this machine ends up holding all of it — so that copy is encrypted, and the key lives in your OS keyring, not next to the data it protects. What that does and does not cover is worth reading in full, below.
  • Credentials live in your OS keyring, never in a config file and never in a log. Postio connects over TLS wherever the server offers it.
  • Logs never contain message content — no bodies, subjects, or recipient addresses, at any log level. Just ids, counts, and outcomes, which is enough to debug a sync problem without ever writing down what your mail says.

What encryption at rest protects — and what it doesn’t

“Encrypted at rest” gets oversold a lot, so here’s exactly what it means for Postio, stated honestly rather than left to your assumptions.

Protected: a stolen or discarded disk; a backup, an rsync copy, or a cloud sync of Postio’s data directory that wanders somewhere it shouldn’t; another user on a shared machine who gets past your account’s file permissions; anyone reading those files while your OS keyring is locked or you’re logged out. In every one of those cases, what they get is ciphertext — the database and every blob (message and attachment) are encrypted, and without the key sitting in your keyring, that’s all it is.

Not protected — and this matters: a live, unlocked session. If someone is running as you while your keyring is unlocked, they can read the encryption key exactly the way Postio does, because that’s what unlocking the keyring means. This is not a gap Postio can close from inside a mail client; it’s why full-disk encryption stays recommended even though Postio encrypts its own store — the two protect different moments. Postio covers the disk at rest and copies that wander; full-disk encryption additionally covers the machine while it’s off or between boot and login. Neither one covers a session an attacker already has open.

The keyring entry is part of your mailbox, not an accessory to it. If you copy the Postio data directory to another machine without also moving the key, you’ve copied ciphertext with no way to open it — that’s not a bug, it’s the same property that makes a wandering backup safe. If you lose the keyring entry on your own machine (a wiped keyring, a fresh OS install without a keyring backup), the recovery path is the same in both cases: a resync from the server. Nothing about that loses mail — everything but drafts and queued outgoing messages is a cache of what the server already has — but it does mean the key is not something to treat as disposable.

This applies to the documentation site too

A privacy-first mail client whose own website loads a third-party font or an analytics beacon would be making a claim its product contradicts, so this site holds itself to the same rules:

  • No analytics. Not Google’s, not a “privacy-friendly” alternative, not a self-hosted page-view counter. A visit to these docs is not logged anywhere.
  • No CDN, no third-party fonts. The typefaces on this page — the same ones the application itself uses — are served from this site’s own origin. Loading this page never causes your browser to request anything from a third-party server.
  • No embedded video, no third-party search widget, no comment system. The search box on this site runs entirely in your browser, against an index shipped with the page.

When the answer might be “not yet”

Phishing and link warnings, and PGP/S-MIME support, are not implemented in v1. They’re real gaps, not oversights, and they’re tracked like everything else Postio hasn’t built yet.

FAQ

Is Postio ready to use as my daily mail client?

Postio is pre-release and under active development. v1 supports a single IMAP + SMTP account with a password or app-specific password. If that covers your setup and you’re comfortable building from source, it’s usable today — but treat it as early software, and keep your existing client around until you’re confident in it.

Why Linux only?

v1 targets GTK4/libadwaita on Linux because that’s where the team could build something excellent fastest, not because other platforms are ruled out. The engine underneath the UI has no GTK in it and no SQLite in the view layer — that boundary is enforced automatically, specifically so a macOS or Windows frontend over the same engine stays possible later. Neither is currently scheduled.

Does Postio support multiple accounts?

Multiple accounts are in scope for Postio but not yet built. Today’s v1 is single-account.

Does Postio support OAuth (Gmail, Outlook, etc.)?

OAuth 2 is in scope and being worked on, but v1 ships first with password and app-specific-password authentication. If your provider requires OAuth, wait for that support to land, or use an app-specific password if your provider offers one (Gmail and iCloud both do).

Why no AI features yet?

Deliberately, not accidentally. Postio’s founding bet is that a mail client has to be excellent at the fundamentals — speed, search, and keyboard control — before AI has anything worth being layered onto. Shipping AI over a mediocre mail client would just produce a mediocre mail client with AI in it. AI is planned for after v1, with two constraints already fixed before a line of it is built: it must never silently modify or send mail, and every design has to treat mail as attacker-controlled text an AI agent could be tricked by.

How does search work?

Locally and fast — a full-text index built on your own machine, never a server-side search. One query language works everywhere it shows up: typed in the search bar, saved to the sidebar as a named search, or pinned as a virtual folder. from:ada after:2026-01-01 has:attach is the kind of query you can type, and results begin appearing as you type it.

What happens if I lose access to my keyring?

Postio stores your mail credentials in your OS keyring and encrypts your local mail store with a key that also lives there — never in a plain config file. If the keyring entry is lost, you lose the local copy and need to re-sync from the server: annoying, but you don’t lose any mail, since the server is still the source of truth.

Is Postio really written by AI?

Yes — Postio is written by AI coding agents under a human maintainer who sets scope, reviews the results, and makes the product calls. It isn’t a disclaimer so much as the actual experiment behind the project: not whether an agent can write code, but whether a process — test-driven development, machine-checked invariants, and a public issue tracker as the paper trail — can make agent-written software trustworthy. Read the code with the same skepticism you’d give any project, and if you find something wrong, the issue tracker is exactly where that gets fixed.

Where do I report a bug or request a feature?

The project’s GitHub issue tracker. See the repository’s CONTRIBUTING.md for how to file an issue that’s actionable.

Is my data ever sent anywhere Postio doesn’t tell me about?

No. See Privacy and security for the specifics — remote images, read receipts, unsubscribe links, telemetry, and this documentation site itself are all covered.