Inboxly — an Inbox by Google recreation in Rust
Find a file
Alan 99f2b83c84
Some checks are pending
CI / test (push) Waiting to run
docs: daily-driver completion roadmap M62-M66 + true-status audit
Root ROADMAP.md: 5-milestone arc against the verified v1.0.0-rc2
baseline — GA-smoke + dogfood entry (M62), Done + folder views (M63),
arrival pipeline completion (M64), compose completeness (M65),
dogfood remediation + carryforwards + arc close (M66). Pre-1.0
versions 0.62.0-0.66.0 (owner decision); GA cut stays a post-arc
owner call. Written for /milestone-cycle, executable by lesser models.

Also: archive the M61 ship SESSION_NOTES and roll forward (section 8
seeds M62); rewrite TODOS.md to truth (M37-1/2/3/5 + bundler-traits
resolved by later milestones — audit table added; F4 -> M66,
M37-4 -> M65); pointer banner + M44-M47 checkmarks on the historical
roadmap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gz5s2C9dv2yjVYpR2j5Dby
2026-07-06 00:30:01 -04:00
.forgejo/workflows ci: mirror GitHub Actions workflow to Codeberg Forgejo 2026-05-25 18:36:42 -04:00
.github/workflows chore: post-M43 CI fixup — fmt drift + test command exclusion 2026-05-25 16:25:52 -04:00
docs docs: daily-driver completion roadmap M62-M66 + true-status audit 2026-07-06 00:30:01 -04:00
inboxly v1.0.0-rc2 — M61 In-App OAuth Activation (Gmail + Outlook) (#27) 2026-06-06 20:42:59 -04:00
inboxly-bundler v0.57.0 feat: settings bundle rule CRUD + rule application (M57) (#22) 2026-06-03 21:22:29 -04:00
inboxly-core v1.0.0-rc2 — M61 In-App OAuth Activation (Gmail + Outlook) (#27) 2026-06-06 20:42:59 -04:00
inboxly-extract v0.54.0 feat: PDF + OCR attachment extraction (M54) (#19) 2026-06-02 18:42:59 -04:00
inboxly-imap v1.0.0-rc2 — M61 In-App OAuth Activation (Gmail + Outlook) (#27) 2026-06-06 20:42:59 -04:00
inboxly-store release: v1.0.0-rc1 — design-parity arc terminus (M60) (#25) 2026-06-05 03:12:33 -04:00
inboxly-ui v1.0.0-rc2 — M61 In-App OAuth Activation (Gmail + Outlook) (#27) 2026-06-06 20:42:59 -04:00
.gitignore v0.53.0 feat: trip itineraries + HTML travel-email parsing (M53) (#18) 2026-06-02 04:44:43 -04:00
AGENTS.md v1.0.0-rc2 — M61 In-App OAuth Activation (Gmail + Outlook) (#27) 2026-06-06 20:42:59 -04:00
Cargo.lock v1.0.0-rc2 — M61 In-App OAuth Activation (Gmail + Outlook) (#27) 2026-06-06 20:42:59 -04:00
Cargo.toml v1.0.0-rc2 — M61 In-App OAuth Activation (Gmail + Outlook) (#27) 2026-06-06 20:42:59 -04:00
CHANGELOG.md v1.0.0-rc2 — M61 In-App OAuth Activation (Gmail + Outlook) (#27) 2026-06-06 20:42:59 -04:00
CLAUDE.md v0.54.0 feat: PDF + OCR attachment extraction (M54) (#19) 2026-06-02 18:42:59 -04:00
GEMINI.md v0.54.0 feat: PDF + OCR attachment extraction (M54) (#19) 2026-06-02 18:42:59 -04:00
README.md v1.0.0-rc2 — M61 In-App OAuth Activation (Gmail + Outlook) (#27) 2026-06-06 20:42:59 -04:00
ROADMAP.md docs: daily-driver completion roadmap M62-M66 + true-status audit 2026-07-06 00:30:01 -04:00
TODOS.md docs: daily-driver completion roadmap M62-M66 + true-status audit 2026-07-06 00:30:01 -04:00

📬 Inboxly

A recreation of Inbox by Google as a native desktop email client, built in Rust with Dioxus.

Google killed Inbox in April 2019. Nothing has replaced its unique approach to email — bundles, highlights, snooze, sweep, and smart extraction. Inboxly brings it back as a fully local, privacy-respecting desktop application.

Features

📥 Inbox Feed

  • Date-grouped sections — Pinned, Today, Yesterday, This Week, This Month, Earlier
  • Email rows with sender avatar, subject, snippet, timestamp, attachment/unread indicators
  • Bundle rows — collapsible groups (Social, Promos, Updates, Finance, etc.)
  • Inbox Zero 🌞 — celebratory view when you've handled everything

Triage Actions

  • Done — archive threads with one click (keyboard: e)
  • Pin 📌 — keep important threads at the top (keyboard: =)
  • Sweep — bulk-archive all unpinned threads
  • 7-second undo — timed snackbar with one-click revert

🕐 Snooze

  • 5 preset options — Later Today, Tomorrow, This Weekend, Next Week, Someday
  • Computed UTC times — respects time zones and weekend preferences
  • Snoozed view — dedicated view for all snoozed threads

✉️ Compose

  • Full compose view — To, Cc, Subject, Body fields (920dp max-width)
  • Reply modes — New, Reply, Reply All, Forward
  • Send / Discard — with confirmation
  • Query parserfrom:, to:, subject:, has:attachment, is:unread operators
  • Result view — empty state, no-results state, result list

📋 Menus & Context Actions

  • PopupMenu widget — reusable dropdown/context menu overlay (custom Iced Widget + Overlay)
  • Overflow menu (⋮) — appears on hover with Move to, Mark read/unread, Mute, Reply, Forward, Block sender, Report spam
  • Right-click context menu — Done/Pin/Snooze quick actions + full overflow menu at cursor position
  • RightClickArea widget — custom Iced widget for right-click event interception

👤 Account Switcher

  • Inline expansion in nav drawer — 44px avatar, display name, email, chevron
  • Multi-account support — switch between configured accounts with one click
  • Active account indicator — blue highlight + checkmark
  • Add account — navigates to Settings → Accounts

⚙️ Settings (6 tabs)

Tab Controls
🎨 General Theme (System/Light/Dark chips), Default View, Snooze Presets, Undo Timeout
👤 Accounts Account cards with avatar, add/edit/remove forms, active-deletion prevention
📦 Bundles Reorder (↑↓ arrows), throttle badge (Immediate/Daily/Weekly), visibility toggle
🔔 Notifications Desktop notification toggle, sound toggle, per-bundle selection
⌨️ Keyboard Shortcuts 18 remappable actions, click-to-capture UI, delta-only JSON persistence
💾 Data & Storage Clear cache, rebuild search index, export (stub), storage sizes, last sync

🎨 Theming

  • Light & dark themes — BigTop design tokens from original Google Inbox APK
  • System detection — D-Bus org.freedesktop.portal.Settings for automatic theme
  • Live switching — theme changes apply instantly from Settings
  • Themed toolbar — blue (Inbox), orange (Snoozed), green (Done), grey (Settings)

⌨️ Keyboard Shortcuts

Key Action Key Action
e Done (archive) c Compose
= Pin / unpin r Refresh
b Snooze / Search
j / k Next / previous o Open thread
Ctrl+Z Undo ? Help
g i / g s / g d Go to Inbox / Snoozed / Done Esc Close / back
Shift+R Reply Shift+A Reply All
f Forward

All shortcuts are runtime-remappable via Settings → Keyboard Shortcuts.

🏗️ Architecture

7-crate Cargo workspace:

Crate Purpose
inboxly-core 🧱 Core types, traits, and error definitions
inboxly-imap 📡 IMAP sync engine and OAuth2 authentication
inboxly-store 💾 Maildir, SQLite, and Tantivy storage layer
inboxly-bundler 📦 Email categorisation engine
inboxly-extract 🔍 Smart extraction and highlight detection
inboxly-ui 🖥️ Iced-based desktop UI
inboxly 🚀 Binary entry point

Dependency Graph

inboxly-core (foundation — zero internal deps)
  ├── inboxly-imap
  ├── inboxly-store
  ├── inboxly-extract
  ├── inboxly-bundler (+ inboxly-store)
  └── inboxly-ui (all subcrates)
        └── inboxly (binary)

Key Design Patterns

  • Custom Iced widgetsPopupMenu (Widget + Overlay), RightClickArea (event interception)
  • View-local message enumsInboxViewMessage decouples views from app-level Message
  • Auto-save settings — every control change persists immediately, no dirty-state tracking
  • Delta persistenceShortcutMap stores only non-default overrides as JSON
  • Store adapter patternStoreSettingsAdapter bridges crate boundaries without circular deps

🚀 Building

# Build
cargo build --workspace

# Test (1575 tests, full workspace)
cargo test --workspace

# Lint — full workspace, mirrors CI
cargo clippy --workspace --all-targets -- -D warnings

# Run
cargo run -p inboxly

Requires Rust edition 2024 (rustc 1.85+).

🔑 OAuth2 sign-in setup (Gmail / Outlook)

The "Sign in with Google" / "Sign in with Outlook" buttons need an OAuth client ID from your own provider app registration. Inboxly never ships a shared client ID — every install brings its own, so no one can impersonate the app. There are two ways to supply it; the app resolves env-var first, falling back to the in-app value.

In-app (recommended for end users). Register an app, then paste the client ID — no terminal, env var, or restart needed:

  1. Gmail → create an OAuth client ID (application type Desktop app) in the Google Cloud Console. Outlook → register an app in the Microsoft Entra admin center and copy its Application (client) ID.
  2. In Inboxly, open the first-run wizard or Settings → Accounts and pick the provider; the Sign-in setup form appears.
  3. Paste the client ID (leave Client secret blank for PKCE desktop apps), click Save, and the sign-in button lights up immediately.

The client ID is saved to config.toml ([oauth2.gmail] / [oauth2.outlook]) so it persists across restarts. An optional client secret (confidential apps only) goes to your OS keyring, never to disk. Remove either with the form's Clear button.

Environment variables (operators / CI). Export the provider's variable(s) in the shell that launches Inboxly; these take precedence over a stored value:

# Gmail
export INBOXLY_OAUTH2_CLIENT_ID=...             # required
export INBOXLY_OAUTH2_CLIENT_SECRET=...         # optional (Web-type apps only)
# Outlook
export INBOXLY_OUTLOOK_OAUTH2_CLIENT_ID=...     # required
export INBOXLY_OUTLOOK_OAUTH2_CLIENT_SECRET=... # optional (confidential apps)
cargo run -p inboxly

If neither source provides a client ID, the sign-in button stays disabled and an inline hint points you at the Sign-in setup form.

📊 Project Status

Milestone Version Tests Description
M1-M25 v0.25.0 715 🏁 Full v1 UI scaffold
M26 v0.26.0 756 📋 PopupMenu widget
M27 v0.27.0 774 📋 Overflow + context menus
M28 v0.28.0 790 👤 Account switcher
M29 v0.29.0 808 ⚙️ Settings framework (3 tabs)
M30 v0.30.0 841 ⚙️ Settings complete (6 tabs)
M31 v0.31.0 🧱 Store traits + action execution
M32 v0.32.0 225 🔄 Iced 0.14 → Dioxus 0.7 framework conversion
M33 v0.33.0 242 📥 Inbox feed + widgets restored on Dioxus
M33.1 v0.33.1 870 🔧 Post-M33 polish (window title, dark mode)
M34 v0.34.0 882 🧵 Thread detail view + HTML email rendering
M34.1 v0.34.1 884 🔧 Post-M34 polish (test side effect, validate_external_url, Settings re-entry)
M35 v0.35.0 961 ✉️ SMTP engine + compose view + drafts (M35a refactor + M35b feature work)
M35.1 v0.35.1 961 🔧 Post-M35 dogfooding polish (inline CSS, 1280×800 default, FAB hide, blur-to-chip)
M36 v0.36.0 1072 ✉️ Reply/ReplyAll/Forward + M35 cleanup (keyring, OAuth2 persistence, Sent write, explicit save)
M36.1 v0.36.1 1076 🔧 Data-layer startup wiring — binary now instantiates Store / MaildirStore / ThreadReader
M37 v0.37.0 1180 📎 Full Attachment Support — download, inline images, PDF preview, DnD, size guardrails, forward streaming
M38 v0.38.0 1222 🔍 Advanced Search — operator parser, two-phase Tantivy+SQLite search, thread grouping, state filters, search history, SearchView
M39 v0.39.0 1233 📦 End-to-End Bundling — auto-categorise on sync, move-to-bundle with affinity learning, block sender, rule dialog, throttle filtering
M40M47 v0.40.0v0.47.0 🔧 Snooze system, smart highlights & extraction, reminders UI, settings-UI rendering, and the M44M47 gap-remediation arc (first-run enhancement)
M48M59 v0.48.0v0.59.0 1526 🎯 Design-parity arc — real auth, OAuth2 (Gmail+Outlook), on-demand body fetch + thread reader, trip itineraries, PDF/OCR extraction, persistent snooze, compose-signal refactor, bundle rules, shortcut editing, JSON data export
M60 v1.0.0-rc1 1526 🏁 Design-parity arc terminus — carryforwards cleanup + release candidate
M61 v1.0.0-rc2 1575 🔐 In-app OAuth activation — paste your own OAuth client_id in Settings / first-run wizard (Gmail + Outlook); no env var or restart. Review fixes F1F3.

Current: v1.0.0-rc2 (1575 tests, 0 clippy warnings, 7-crate workspace)

M36 highlights

M36 closes every M35.1 follow-up item and ships the Reply / ReplyAll / Forward feature surface on top. Fourteen phases across two sub-milestones under a /plan-eng-review pass (findings A1-A9) and a Gemini outside-voice pass (findings G1-G8):

  • M36a (Phases 0-5) — M35 cleanup: a real keyring 3 secrets backend (replaces the INBOXLY_SMTP_PASSWORD env var), OAuth2 refresh token persistence with a rotation callback (A3), a local .Sent/ write on every successful SMTP send + real IMAP APPEND via WellKnownFolders (A6), and an explicit Save Draft bridge with a three-layer persistence model (in-memory → SQLite → offline queue) plus a Navigate-with-compose auto-save guard (A8). New inboxly CLI subcommands: oauth2-authorize, set-password, delete-credentials.
  • M36b (Phases 6-13) — Reply feature surface: pure helpers for subject normalization + References chain + Gmail-compatible quote formatting, a compose_state_from_original DRY helper + ComposeMode dispatch, a real OpenComposeReply handler with a body-fetch fallback for header-only local copies (G3) via new ThreadReader::load_email, forward attachment passthrough, Reply / ReplyAll / Forward buttons in the thread message footer, an inline compose panel with a layout toggle (Inline ↔ FullScreen), a quoted-original placeholder, and 11 end-to-end state-machine integration tests.

M36.1 patch — data-layer startup wiring (v0.36.1). The runtime gap M36 phase 14 flagged (the binary never instantiating Store / MaildirStore / ThreadReader) is closed as of v0.36.1. main() now resolves XDG paths, opens SQLite, initialises per-account Maildir stores, and publishes both through new inboxly-ui::startup::STORE and MAILDIR_STORES singletons (gated behind a MainThreadOnly<T> Sync wrapper because rusqlite's Connection is !Sync). App() reads them on first render and builds a ThreadReader for the active account; SwitchAccount reconstructs it on every account change. Fail-soft at every step — paths-unresolved, ensure_dirs, Store::open, and MaildirStore::init errors all log a warning and leave the binary in the pre-patch None state instead of crashing. CLI subcommands (--help, oauth2-authorize, set-password, delete-credentials) still short-circuit before any data-layer call, preserving the Gemini G6 keyring-free-help invariant. See CHANGELOG.md for the full M36 phase-by-phase breakdown and the four documented scope reductions (Phase 9 forward streaming, Phase 11 signal split, Phase 12 quoted original expansion, Phase 5 IMAP draft replay).

M35 highlights

M35 closes the read/write loop. After M34 users could read email via the thread detail view; after M35 they can write it too. The milestone shipped in two sub-milestones:

  • M35a — behaviour-preserving god-object refactor (31 flat fields on Inboxly extracted into SettingsState, MenuState, SnoozeState)
  • M35b — 14 phases covering lettre 0.11 API verification, core data types, SQLite drafts table, SMTP transport with dual message builders (Gemini G1 Bcc-not-in-headers invariant), retry logic with PII-redacted logging, IMAP APPEND helpers, sync-side Message-ID dedup (G8), offline replay via a DraftSender trait, ComposeState + 23 Message variants, CSS, the ComposeView Dioxus component, FAB wiring, 30 s auto-save bridge, rfd attachment picker, and the send bridge with two-phase commit dismiss overlay (G9) + AppendSent fallback (G6).

All six M35b limitations scheduled for M36 were closed in M36a (Phases 0-5): keyring-backed password secrets replace the INBOXLY_SMTP_PASSWORD env var, OAuth2 refresh token persistence with rotation callback is live end-to-end, SmtpClient::send() writes to the local Maildir .Sent/ atomically on every success, the real AppendSent replay handler resolves the Sent folder via WellKnownFolders, the explicit Save Draft bridge landed with a three-layer persistence model and a Navigate-with-compose auto-save guard, and the toolbar Draft chip shows unsaved / saving / saved state. Remaining deferrals after M36 are four smaller scope reductions (all TODO(post-M36) in-tree): forward attachment streaming, compose signal split, quoted-original expanded preview, and IMAP APPEND for drafts. See CHANGELOG.md for the full phase-by-phase breakdown.

📄 Licence

GPL-3.0-only