Skip to content

Production readiness: Phase 0 (stabilize) + Phase 1 (send pipeline)#1

Open
ilyk wants to merge 18 commits into
mainfrom
production-readiness
Open

Production readiness: Phase 0 (stabilize) + Phase 1 (send pipeline)#1
ilyk wants to merge 18 commits into
mainfrom
production-readiness

Conversation

@ilyk

@ilyk ilyk commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Executes Phase 0 and Phase 1 of the production-readiness plan (see EXECUTION_PLAN.md). Anchored to the verified PRODUCTION_GAP_REPORT.md.

Phase 0 — Stop the bleeding & make the repo real

  • WP-0.1 Commit the real asgard-mail/ app crate (was untracked; index held the deleted app/ prototype) + Cargo.lock; fix submodule URL to HTTPS; repo hygiene sweep.
  • WP-0.2 Relicense to AGPL-3.0-only — resolves the MIT-vs-AGPL conflict from the statically-linked proton-api-rs fork.
  • WP-0.3 Stop the UI claiming success on unimplemented actions (offline send, delete, Schedule Send) — silent data-loss paths made honest.
  • WP-0.5 Fix CI (WebKit 6.0 dev pkg, submodule checkout, drop impossible Win/macOS jobs), add rustfmt.toml, format first-party crates.
  • WP-0.6 docs/BUILD.md with the exact dev-package list.

Phase 1 — Fix the send pipeline (no more dropped data)

  • WP-1.1/1.2/1.3 New SendRequest contract + MIME encoder (14 tests); send_message(&SendRequest) now carries Cc/Bcc/HTML/attachments and sends from the active account (not "first in DB"). Gmail sends full MIME via a new send_raw path. All four compose flows DRY'd through shared helpers.
  • WP-1.4 Real offline + scheduled outbox: offline sends enqueue and flush via the 60s poller; Schedule Send wired; poller restores Cc/Bcc/HTML + attachments + identity.
  • WP-1.5 Draft saving — cancelling/closing compose with content saves a draft (drafts table + CRUD).
  • WP-1.6 Per-account signatures — settings editor + persistence; New/Forward compose prepend them.

Verification

  • cargo check -p asgard-mail --features webview0 errors.
  • asgard-core tests: 184 pass / 5 fail — the 5 are pre-existing cache+tantivy failures documented in the GAP report (zero regressions); new MIME/draft/send tests pass.

Known, deliberately deferred

  • Proton send still errors pending encryption (Phase 2, WP-2.4).
  • BackendBridge still does network I/O on the GTK main thread (Phase 2, WP-2.1).
  • Generic IMAP/SMTP send/sync (Phase 2, v1.0 scope).
  • 5 pre-existing core test failures (Phase 5, WP-5.3).

Reviewer notes

  • One SSH→HTTPS submodule URL change; run git submodule update --init --recursive after checkout.
  • The 5 failing tests predate this branch and are tracked for Phase 5.

🤖 Generated with Claude Code

ilyk and others added 18 commits July 2, 2026 16:32
The working tree held the actual GTK4 application (asgard-mail/) as
untracked while the git index still described the deleted app/ and
simple-gtk-app/ prototype trees. This commits the real codebase so the
repository reflects what is actually built.

- Add asgard-mail/ (main binary), com.asgard.Mail.metainfo.xml, and the
  untracked core modules (mail_rules, smart_mailbox, pgp) + ui-components
- Remove the obsolete app/ prototype tree (55 files)
- Commit Cargo.lock (binary app: lockfile belongs in VCS; also required
  for the Flatpak offline build)
- Fix .gitmodules to an https URL so anonymous clone --recursive works
- Untrack logs/, remove debug_output*.txt and Cargo-no-webkit.toml
- Move stale root status docs to docs/history/

Part of the production-readiness plan (Phase 0). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
The main binary statically links the bundled proton-api-rs fork, which is
AGPL-3.0-only, so the combined distributed work must be AGPL-3.0. The tree
previously mislabeled everything as MIT.

- Replace root LICENSE with the AGPLv3 text
- workspace Cargo.toml: license = AGPL-3.0-only
- Add license = AGPL-3.0-only to the four crates that had no license field
  (asgard-mail, email-backend, minimal-app, minimal-core)
- Update README badge + License section and metainfo project_license

Part of the production-readiness plan (Phase 0). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
Three paths previously told the user an action succeeded when it did
nothing, causing silent data loss. Interim honesty until the real
implementations land in later phases:

- Offline send (all 4 compose/reply/reply-all/forward paths): no longer
  claims "queued for delivery when online" — the outbox is not wired yet
  (WP-1.4). Now says the message was NOT sent, retry when online.
- Delete keyboard action: confirmation dialog no longer promises permanent
  deletion / Trash move; states it only hides the row locally and the
  message returns on next sync (real delete is WP-2.6).
- Toolbar delete: toast no longer says "Moved to Trash"; says removed from
  this device, not yet deleted on the server.
- Schedule Send button: hidden until the outbox is wired (WP-1.4); its
  callback was never set, so the picker silently discarded the message.

Part of the production-readiness plan (Phase 0). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
…ild (WP-0.5, WP-0.6)

CI (WP-0.5):
- ci.yml/release.yml: checkout submodules recursively; swap the wrong
  libwebkit2gtk-4.1-dev for libwebkitgtk-6.0-dev (+ libgdk-pixbuf-2.0-dev,
  libenchant-2-dev) so the webview default feature actually builds.
- Drop the Windows and macOS jobs: this is a Linux/GNOME app (ksni, Secret
  Service, zbus, webkitgtk-6.0) that cannot build there.
- clippy runs in -W warnings mode until the lint debt is cleared (WP-5.3);
  build/test scoped to buildable targets.
- Add rustfmt.toml and format all first-party crates. The proton-api-rs
  submodule fork is intentionally left unformatted (own upstream history).

Build env (WP-0.6):
- Add docs/BUILD.md: exact apt dev-package list (libgtk-4-dev,
  libadwaita-1-dev, libwebkitgtk-6.0-dev, libgdk-pixbuf-2.0-dev),
  pkg-config verification, and cargo build/check commands.
- Verified `cargo check -p asgard-mail --features webview` finishes with
  0 errors against these packages.

Part of the production-readiness plan (Phase 0). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
Introduces core::sync::SendRequest and OutgoingAttachment — the single
structured payload for every send path, replacing the lossy
(to, subject, body, reply_to) tuple that silently dropped Cc, Bcc, the
HTML body, and attachments. Carries from_account_id so sends use the
active identity instead of a first-account default.

Includes helpers (parse_recipients, has_html, has_attachments, plain)
with unit tests. Wiring backends and callers to it follows in WP-1.1/1.3.

Part of the production-readiness plan (Phase 1). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
…P-1.1, WP-1.3)

Every send path now carries To/Cc/Bcc, the HTML alternative, and
attachments end-to-end, and sends from the account the user is actually
viewing instead of the first account in the database.

- backend_bridge::send_message now takes &SendRequest and resolves the
  sending account from from_account_id (falls back to first account with
  a warning only when unset). Gmail sends full MIME via
  core::sync::encode_mime(..).to_gmail_raw(); Proton carries Cc + the
  HTML/plain body (full Proton send still gated on encryption, WP-2.4).
- oauth: add GmailApiClient::send_raw(raw_base64url) — the fidelity path
  that posts a pre-built MIME message; send_message now delegates to it.
- main.rs: extract build_send_request() + spawn_undo_send() helpers and
  route all four compose flows (new/reply/reply-all/forward) through them,
  removing ~250 lines of near-identical, drifting callback code. Each flow
  now passes the active account and all compose fields.
- dbus_service + the 60s outbox poller build SendRequests too; the poller
  now preserves Cc/Bcc/HTML and the sending identity from the stored row
  (attachment restoration from JSON lands in WP-1.4).

asgard-mail --features webview compiles clean; core send_request/mime
tests pass.

Part of the production-readiness plan (Phase 1). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
Adds core::sync::encode_mime(&SendRequest, from_address) -> EncodedMessage,
producing a complete RFC 2822 message with correct multipart structure:
- text/plain only, or multipart/alternative (text+HTML), or
  multipart/mixed wrapping either, when attachments are present.
- To/Cc + In-Reply-To/References headers; Bcc is stripped from the header
  block and surfaced via EncodedMessage.bcc_recipients for the SMTP
  envelope. EncodedMessage::to_gmail_raw() gives base64url (no pad) for the
  Gmail messages.send `raw` field.
- RFC 2047 encoded-words for non-ASCII headers; quoted-printable text
  parts; base64 attachment parts with folded lines.

14 unit tests cover each structure, boundary well-formedness, base64
round-trips (incl. all 256 byte values), Bcc isolation, and reply headers.
Uses the existing base64 dependency; no new crates.

Part of the production-readiness plan (Phase 1). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
…(WP-1.4/1.5/1.6)

Closes the remaining Phase 1 data-loss and fidelity gaps.

WP-1.6 Signatures:
- Settings dialog gains a per-account signature editor (multiline, in a
  MessageDialog) wired through a new AccountCallbacks::on_signature_changed;
  persisted via get_account/update_account (config.signature).
- New and Forward compose now prepend the active account's signature.

WP-1.5 Draft saving:
- Drafts table + CRUD (save_draft/get_drafts/delete_draft) and a
  create_drafts_table migration (idempotent CREATE TABLE IF NOT EXISTS).
- compose.rs gains set_draft_callback + save_draft_if_dirty; cancelling or
  closing a compose window with content now saves a draft instead of
  silently discarding it.

WP-1.4 Offline + scheduled outbox (real, replacing the WP-0.3 interim):
- backend_bridge: enqueue_outbox(&SendRequest, when), get_due_outbox_requests
  (returns fully reconstructed SendRequests incl. base64 attachments), plus
  attachment JSON (de)serialization.
- Offline sends now enqueue to the outbox (epoch timestamp = due immediately)
  and flush via the 60s poller when back online — no more "queued" lie.
- Schedule Send is wired (set_schedule_callback reveals the button) and stores
  into the outbox; picker timestamp fixed to SQLite datetime() format so the
  due-comparison works intra-day.
- Poller rewritten to use get_due_outbox_requests, preserving Cc/Bcc/HTML,
  attachments, and the sending identity.

main.rs: extracted build_send_request/spawn_undo_send/wire_compose_persistence
helpers; all four compose flows route through them.

asgard-mail --features webview compiles clean; asgard-core 184 pass / 5 fail
(the pre-existing cache+tantivy failures, unchanged); new draft tests pass.

Part of the production-readiness plan (Phase 1). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
First step of moving blocking BackendBridge operations off the GTK main
thread. Adds asgard-mail/src/backend_actor.rs:

- BackendCommand: the slow (network) operations that must not run on the
  UI thread — Send, EnqueueOutbox, SaveDraft, SyncProtonMailbox, Shutdown.
- BackendHandle: a cloneable main-thread handle over Arc<Mutex<BackendBridge>>
  with `dispatch` (non-blocking command send) and `lock` (brief synchronous
  access for cheap local reads).
- spawn_backend_actor: owns the bridge on a dedicated "backend-actor" thread
  and runs the command loop, reporting each result through the command's
  reply channel.

Additive only — main.rs still uses the existing Rc<RefCell> bridge, so the
app builds and behaves unchanged. Compiling this confirmed BackendBridge is
Send (required to move it onto the actor thread). Call sites migrate onto
this in WP-2.1b/c.

Part of the production-readiness plan (Phase 2). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
…(WP-2.1b)

Migrates the ~80 bridge access sites in main.rs from Rc<RefCell<BackendBridge>>
to backend_actor::BackendHandle (Arc<Mutex<BackendBridge>> owned by the actor
thread). Bridge .borrow()/.borrow_mut() → .lock(); construction uses
spawn_backend_actor(). app_state and trust-store RefCells are untouched.

Behavior-preserving: slow operations still run synchronously via .lock() on
the main thread for now (no freeze fix yet), but the bridge is now Send-shared
and reachable from the actor thread — the prerequisite for WP-2.1c, which moves
sends and Proton sync onto the actor so the UI stops freezing.

Compiles clean under --features webview.

Part of the production-readiness plan (Phase 2). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
Moves the two worst UI-freeze offenders off the GTK main loop:

- Send: spawn_undo_send now dispatches BackendCommand::Send to the actor
  thread and polls the reply channel from a 100ms glib timeout to show the
  result toast — the SMTP/Gmail/Proton network round-trip no longer blocks
  the UI. (Offline enqueue stays inline: it is a fast local DB insert.)
- Proton mailbox sync (both the manual-refresh and mailbox-open paths):
  previously ran inside spawn_future_local, which executes on the main
  thread, so the internal block_on froze the UI for the whole sync. Now
  dispatched to the actor via BackendCommand::SyncProtonMailbox (which
  replies with the full ProtonSyncResult); the UI refresh + reauth handling
  run when the reply arrives.

Remaining main-thread block_on (Proton message decrypt-on-open, attachment
download, login) are smaller/rarer and can move to the actor incrementally.

Compiles clean under --features webview.

Part of the production-readiness plan (Phase 2, WP-2.1). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
…, part 1)

Fixes the bug where adding an IMAP account stored the password in the system
keyring but discarded the dialog result, creating no account (and leaving an
orphan credential). The IMAP arm now builds ServerConfigs from the entered
host/port (TLS on 993/465, STARTTLS otherwise), constructs an Account via
Account::new_imap_smtp, persists it with create_account, creates the standard
mailboxes (INBOX/Sent/Drafts/Trash/Spam/Archive), and registers it in the mock
backend for display — mirroring the Gmail creation path. Duplicate-email guard
included.

Honest scope: the account is created and browsable, but background IMAP sync
(live fetch) is a follow-on — the core ImapSync engine exists but is not yet
started for is_imap() accounts, and can only be validated against a real IMAP
server.

Compiles clean under --features webview.

Part of the production-readiness plan (Phase 2, WP-2.2). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
…rch (WP-5.3)

Two real product-code bugs (not test scaffolding), both now covered green:

Cache (storage/cache.rs):
- store() wrote to messages/{hash[0..2]}/{hash} but only the top-level
  messages/ attachments/ temp/ dirs were created, so message/attachment
  writes failed with NotFound and retrieval missed. Now create_dir_all the
  hash-sharded parent before write and before hard_link.
- store() counted as a cache "hit", corrupting hit/miss stats. Removed; only
  retrievals update stats now.

Tantivy (search/tantivy_index.rs):
- Reader used ReloadPolicy::OnCommitWithDelay, so an add-then-search saw 0
  docs. Force reader.reload() after each commit (add/remove/clear).
- Query terms were matched case-sensitively against the default tokenizer's
  lowercased tokens ("Test" never matched indexed "test"). Lowercase query
  terms in create_text_field_query.

asgard-core: 189 passed / 0 failed (was 184/5). Removes the last failing
gate from the CI test step.

Part of the production-readiness plan (Phase 5, WP-5.3). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
…(WP-3.1)

The IsUnread/IsFlagged predicates compared message_flags.flag against 'read'
and 'flagged', but flags are persisted as serde_json of the MessageFlags enum
(rename_all="lowercase"), i.e. the column literally holds "seen" / "flagged"
(with quotes). Result: an "Unread" smart mailbox matched every message and a
"Flagged" one matched none — silently wrong sets.

- Add MessageFlags::as_stored_sql_literal(), which derives the exact
  single-quoted stored encoding from serde so query SQL can never drift from
  the storage format.
- IsUnread/IsFlagged now use it (compare against '"seen"' / '"flagged"').
- Correct the two pre-existing tests that asserted the buggy 'read'/'flagged'
  literals, and add a test pinning the stored-literal to serde output.

asgard-core: 190 passed / 0 failed.

Part of the production-readiness plan (Phase 3, WP-3.1). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
…P-4.3)

Migrations previously ran in two separate steps — apply the DDL, then insert
the tracking row under a fresh lock. A crash in between left the migration
applied-but-unrecorded, so the next startup re-ran it; for a non-idempotent
ALTER TABLE ADD COLUMN (AddFlagColorColumn/AddSignatureColumn) the re-run
failed with "duplicate column" and bricked startup permanently.

Now the apply and the tracking-row insert run inside a single BEGIN/COMMIT
transaction (SQLite supports transactional DDL), with ROLLBACK on failure.
Removed the now-unused mark_migration_applied helper and strengthened the
idempotency test to assert the migration count is stable across repeated runs.

asgard-core: 190 passed / 0 failed.

Part of the production-readiness plan (Phase 4, WP-4.3). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
…(WP-4.4)

- Remove the `dangerous_configuration` feature from the workspace rustls
  dependency. No first-party code uses rustls directly (only reqwest/lettre
  pull it transitively via their own *-rustls-tls features), so the feature —
  which enables disabling TLS certificate verification — was pure attack
  surface inviting a future one-line cert bypass.
- Release profile: add lto = "thin", codegen-units = 1, strip = true to shrink
  the binary (~34MB unstripped → materially smaller). Deliberately NOT setting
  panic = "abort" so a worker/sync-thread panic unwinds that thread instead of
  aborting the whole app.

asgard-core compiles and passes 190 tests unchanged.

Part of the production-readiness plan (Phase 4, WP-4.4). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
…hase 6)

Packaging & community-readiness slices that are verifiable without a running
app:

- metainfo: add <developer> (clears the appstreamcli developer-info-missing
  info; now validates with only 1 pedantic note) and <keywords>; document the
  screenshots requirement that still needs a GUI capture before Flathub.
- Add CONTRIBUTING.md (AGPL, submodule clone, build via docs/BUILD.md, CI
  gates), CODE_OF_CONDUCT.md (Contributor Covenant), SECURITY.md (private
  disclosure + hardening scope), CHANGELOG.md (Keep a Changelog).
- README: fix the wrong WebKit dev package (libwebkit2gtk-5.0-dev →
  libwebkitgtk-6.0-dev + point to docs/BUILD.md), replace the deleted app/
  project layout with the real crate structure, and stop overclaiming
  ("production-ready" / POP3) with an honest pre-1.0 status note.

Part of the production-readiness plan (Phase 6). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
The test called ClientId::secret(), which does not exist (secret() is on
ClientSecret). ClientId exposes the value via as_str(). asgard-oauth test
suite now compiles and passes (6/6), restoring the crate's coverage in the
CI test step.

Part of the production-readiness plan (Phase 5, WP-5.3). See EXECUTION_PLAN.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jQxSBEhcYQYo9dozfQQDM
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant