A spiffy ratatui-based TUI email client with an app-owned mail layer, native maildir support, and native IMAP/SMTP transport for DB-backed accounts.
Every screenshot below is the running application: the images are rendered from the app's own cell grid, with the colours it chose.
# Run from source with a specific account
cargo run -- --account test
cargo run -- --account icloud
# Set up accounts
cargo run -- --setup- Non-blocking I/O - Background workers for all mail operations
- Relative timestamps - "2h ago", "Yesterday", "Mon"
- Threading support - Press
tto toggle threaded view; replies nest under their parent and server-sideTHREADis used when available - Auto-refresh - New mail check every 60 seconds
- Folder unread counts - Shows (3) badge on folders
- Mouse support - Click to select, scroll wheel works
- Multi-account - Switch with Ctrl+a; an "All Inboxes" folder merges every account's inbox when more than one account is configured
- Account-scoped mail triage - In an inbox,
vcycles Screening, Inbox, Reading, Receipts, Blocked, then the unfiltered server view.1-5routes the selected sender for the receiving account; see mail workflow. - Follow-up queues -
ymarks a message Reply later,Ysaves it for reference;L/Dopen the respective account-scoped queues, including from All Inboxes - Quiet conversations and resurfacing -
Mquiets (or restores) a conversation so new replies stop demanding attention;bsets a delay (30m, 2h, 1d) after which the conversation floats back to the top of the list - Local annotations -
iwrites a private note shown above the message;%renames a subject for your eyes only, andxplaces a single message in another lane - Attachment library -
Ctrl+llists attachments from cached mail across accounts; Enter opens the source message - Read together -
Treads the selected messages, or the cursor row, in one numbered scroll - Bypass token and cover - Preferences holds a per-account token that lifts screening for a trusted stranger's subject; a cover hides previously seen Inbox mail until
V - Focus & reply -
Fworks the Reply later queue one message at a time:n/pmove,r/Rreply,ddone - Snippets -
Ctrl+nin compose inserts reusable text;ssaves the current body as a snippet - Text clips -
*in the reader saves an excerpt;Ctrl+klists clips, Enter copies one to the clipboard - Quiet or loud threads -
Mquiets a conversation,+makes it always notify; both key on the conversation's message anchors - Sender bundles -
Bcollapses a high-volume sender to one counted row, expands it, or unbundles it - Workflow stages -
Ctrl+gassigns a conversation to an account-scoped stage and can filter the list by stage - Bulk reply and reading stream -
Rreplies to every selected sender in one message;Tin the Reading lane streams the loaded newsletters - Collections -
Ctrl+ofiles a conversation into named collections and filters the list by one - Merged conversations -
Ctrl+tmerges two local conversations into one grouping without touching the mail - Fast keyboard navigation - j/k and g/G in list/message views, plus direct multiline editing in compose
- Smart error handling - Typed mail diagnostics and clean user-facing errors
- Structured message reader - MIME-aware message content with one canonical HTML-first render path
- Address book - Contacts with name, email, phone, org, notes, tags
- Contact import - vCard (.vcf) and Google CSV import
- Auto-harvest contacts - Captured from sent/received mail
- Sender identities - Multiple From addresses per account with default
- Local SQLite database - Contacts and identities stored in
~/.local/share/franking/mail.db - Safe database upgrades - Versioned migrations preserve accounts, contacts, identities, messages, and local decisions; unsupported versions fail without replacing the database
- App-owned account store - Accounts, endpoints, auth bindings, and secret references live in SQLite
- Keyring-backed secrets - Password and app-password flows store secret IDs in the app and raw secrets in the OS keyring
- Account discovery - Add an account by email: Google/iCloud/Outlook presets, Mozilla autoconfig, Microsoft Autodiscover, then RFC 6186 SRV
- Attachments - Attach files on send; download attachments from received mail
- Drafts - Save a draft and resume it from the Drafts folder; the draft is removed after sending
- Security indicators - SPF/DKIM/DMARC verdicts from
Authentication-Results, PGP/MIME and S/MIME structure detection, and OpenPGP/S/MIME verification and decryption - Crypto keyring - public/secret keys under
~/.local/share/franking/keysverify and decrypt OpenPGP (inline and PGP/MIME) and S/MIME (PKCS#7 signed/enveloped) - Calendar invitations -
text/calendarevents show their summary and time with its timezone, cancelled events are marked, andchands the invitation to Planner123 (planner123-cli ical import) - Desktop notifications - Quiet by default:
notify-sendfor accepted Inbox senders when the IDLE watcher fires, with all/contacts/off preferences - Offline cache - Listings and search fall back to the local store when the server is unreachable
- Mark read/unread - Press
Nto toggle the Seen flag
Ctrl+c/Ctrl+q- QuitCtrl+a- Switch accountCtrl+r- Refresh?- HelpF1- Help from any view (including compose);g/Gjump to the start/end
j/k- Navigate up/downEnter- Read messagec- Compose newd- Deletem- Move to folder!- Toggle flagN- Toggle read/unreadA- Mark all readt- Toggle threaded view/- SearchTab- Focus foldersCtrl+b- Open contactsI- Open identities
j/k- Scrollq/Esc- Back to listr- ReplyR- Reply allf- Forwardd- Deletea- Download attachmentsN- Toggle read/unread
Tab/Shift+Tab- Next/previous compose fieldUp/Downin headers or action bar - Previous/next compose field- Typing in header text fields edits them directly
Enteron theFromfield cycles identitiesEnteron header text fields advances to the next compose fieldEnteron action buttons activates the focused actionEscon the action bar returns focus to the bodyCtrl+c/Ctrl+q- Discard compose- In Body focus: type directly in the multiline editor
Ctrl+fin the body opens in-body search;Enter/F3repeats forward andShift+F3repeats backward- Discard confirmation modal:
yconfirms,n/Esccancels
- Scroll wheel - Navigate/scroll
- Left click - Select folder/envelope
- Right click - Go back (in message view)
Compose input is resolved in two layers:
- Context builder (
App::compose_key_context) maps runtime compose state into a compact context:- Focus bucket:
From/Header/Body/ActionBar - Popup flags: autocomplete visible, discard-confirm visible
- Focus bucket:
- Contextual resolver (
resolve_compose_with_context) applies deterministic priority rules:- Discard-confirm modal interception (
y,n,Esc) - Global compose shortcuts (
Ctrl+c,Ctrl+q) - Autocomplete navigation/accept interception
- Compose shell controls (
Tab,Shift+Tab, non-bodyUp/Down, action-bar activation) - Passthrough to the focused compose field
- Discard-confirm modal interception (
- Keeps compose ownership explicit while the body editor stays focused on text editing.
- Keeps compose behavior explicit and testable with a single resolver function.
- Makes modal interactions predictable by using a clear precedence order.
- Tracks
dirtyfrom actual text mutations instead of inferring it from raw body keys.
For this use case, the ideal architecture is:
- A single authoritative input router per view (Compose already follows this pattern).
- State modeled as explicit focus buckets + overlays.
- Pure key-resolution functions with unit tests for each mode interaction.
- Minimal side effects in key resolver; side effects happen in
Appaction handlers.
To improve safely in one pass:
- Keep behavior changes isolated to the compose resolver (
resolve_compose_with_context). - Encode precedence explicitly (modal > shortcut > popup > compose shell > focused-field passthrough).
- Add regression tests for each precedence boundary.
- Avoid moving side-effectful logic into resolver code.
The app supports multiple account types:
Already configured with sample emails:
cargo run -- --account testRun the setup wizard:
cargo run -- --setupSupported setup flows inside the wizard:
- Generic IMAP/SMTP: Native app-owned IMAP read + SMTP send using SQLite account metadata and OS-keyring secrets
- iCloud: Native app-owned endpoint presets with app-password storage in the OS keyring
- Gmail OAuth: Native browser-based OAuth bootstrap with app-owned token refresh and SQLite-backed metadata
- Outlook OAuth: Native browser-based OAuth bootstrap with app-owned token refresh and SQLite-backed metadata
- Auth source of truth: SQLite stores account definitions, endpoints, auth bindings, OAuth state, and secret references; raw secrets stay in the OS keyring
- TEA pattern - The Elm Architecture (Model, Update, View)
- Async worker pool - Background threads for all I/O
- Channel-based IPC - mpsc for result passing
- Mail service boundary -
MailServicetrait isolates UI/worker code from transport details - Shared MIME parser - Raw messages are parsed once and rendered as structured content
- Theme support - Reads SolverForge colors.toml
- Zero dependencies on async runtime (no tokio)
Check the local maildir test account first. If this fails, the backend/runtime is broken rather than remote auth:
cargo run -- --account testFranking expects:
- no external dependencies for the local
testmaildir account - the system keyring/
secret-toolfor password, app-password, and OAuth token storage - network reachability to the configured IMAP and SMTP endpoints for remote accounts
- Generic IMAP/SMTP: Re-open
--setup, confirm the endpoint/port pair, and verify the stored keyring secret matches the server login. - iCloud: Use an app-specific password, not your Apple ID password. If you keep
~/.authinfo.gpgfor other tooling, verify it still decrypts in this session. - Gmail/Outlook OAuth: Re-open
--setup, run the provider flow again, and verify the stored client credentials match the OAuth app you registered with the provider. - Local
testaccount failing: This is not an auth issue. Fix backend discovery, config loading, or local maildir paths first.
# Check if keyring is accessible
secret-tool store --label="test" service test user test
# (enter any password)
secret-tool lookup service test user test# Check that the iCloud auth file decrypts in this session
gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | headSchema changes require an ordered migration in src/db/schema_migrations.rs
and an increment of SCHEMA_VERSION in src/db/connection.rs. Update the
fresh-install DDL in src/db/schema.rs too. Startup applies migrations in one
transaction before loading mail, and a newer, malformed, or unversioned
existing database is left intact with an error instead of being reset.
# Build
cargo build --release
# Test
cargo test
# Live integration tests: Dovecot (IMAP + ManageSieve) and Mailpit (SMTP) in
# throwaway containers, removed again on exit (KEEP=1 leaves them running)
make live-test
# Local CI-style validation
make ci
# Interactive setup wizard
cargo run -- --setup
# Run with specific account
cargo run -- --account testGitHub Actions now runs the same core validation as local development:
make cifranking/
├── setup-accounts.sh # Interactive account setup wizard
├── src/
│ ├── main.rs # Entry point, terminal setup, CLI modes
│ ├── app.rs # TEA state machine
│ ├── setup.rs # Interactive account setup wizard
│ ├── worker.rs # Background thread pool
│ ├── event.rs # Terminal event handling
│ ├── keys.rs # Keybinding definitions
│ ├── theme.rs # Color theme loader
│ ├── mail/
│ │ ├── service.rs # App-facing mail service boundary
│ │ ├── remote/ # Native IMAP/SMTP transport
│ │ │ ├── next/ # Commands on the app-owned IMAP client
│ │ │ └── session/ # Transport, codec reading policy, pool, IDLE
│ │ ├── message.rs # Structured message content + display modes
│ │ ├── mime.rs # Shared raw-message MIME parser
│ │ ├── pgp.rs # PGP verify/decrypt/key generation
│ │ ├── planner123.rs # Handing invitations to Planner123
│ │ ├── smime.rs # S/MIME verify/decrypt
│ │ ├── sieve.rs # ManageSieve script management
│ │ ├── maildir.rs # Native local maildir backend
│ │ └── account_store.rs # App-owned account metadata store
│ └── ui/
│ ├── envelope_list.rs # Email list with relative dates
│ ├── folder_list.rs # Sidebar with unread counts
│ ├── message_view.rs # Structured message reader
│ ├── account_list.rs # Account switcher
│ └── ... # Other UI components
└── tests/ # 54 comprehensive tests








