Replace JmapEngine's canned session/mailbox/message data with real
RFC 8620/8621 HTTP session discovery and Mailbox/get + Email/query +
Email/get JSON-RPC calls over reqwest, gated behind with_credentials
so uncredentialed callers keep the deterministic no-network fallback.
Fix the camelCase wire bug on JmapEmail (receivedAt/isUnread/
bodySnippet were silently defaulting) and add a regression test.
Wire nunciod::sync::build_mail_backend to construct the JMAP engine
with the account's real host/address/keyring password, mirroring the
IMAP arm, and rewrite the sync.rs tests that previously relied on
JmapEngine's canned data to run against wiremock servers instead.
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Publish the M2-S1 contacts engine over the daemon's authenticated gRPC
API, mirroring the Calendar vertical: add nuncio.v1.Contacts (Sync,
ListContacts, GetContact, CreateContact) plus Contact/ContactEmail/
ContactPhone messages to the proto contract and regenerate the golden
descriptor.
Wire ContactsEngineOverrides + ContactsGrpcService into nunciod,
mounted behind its own BearerAuthInterceptor exactly like every other
service. ListContacts/GetContact always read genuinely persisted data
from DatabaseEngine; Sync fetches from an injected ContactsBackend
(MockContactsBackend in tests) and persists via
contacts_sync::sync_with_backend, mirroring calendar_sync.rs; with no
backend injected, production Sync returns an honest error since
AccountConfig carries no per-account CardDAV configuration yet.
Add connect_contacts/AuthenticatedContactsClient to nuncio-proto's
shared client helpers. Replace the CLI's Commands::Contact handlers
(previously a disposable in-memory ContactsDatabase.in_memory() per
invocation) with real gRPC calls against the daemon, so a contact
added via `contact add` persists and is visible to a later, separate
CLI invocation.
Deviations from the issue's literal proto sketch, flagged for review:
- Added CreateContact (not in the issue's message list): the
acceptance criteria requires `contact add` to persist a locally-
authored contact across CLI invocations, but the daemon has no
per-account CardDAV credential to write back to a real server. This
RPC only ever writes to the daemon's own local store -- never a
fabricated CardDAV write-back -- which is the same honest-surface
spirit as Calendar's "no create/delete" note (that note is about not
pretending to write to the remote server, not about never
persisting locally-authored data at all).
- ListContacts/CreateContact take a required account_id (the issue
sketched a bare ListContactsRequest{}), because the real
DatabaseEngine::list_contacts/save_contact are account-scoped;
ContactSubcommand::List/Search/Add/Sync now all take --account to
match.
- Contact search has no server-side RPC; `contact search` fetches the
full ListContacts result and filters client-side by display name,
organization, and email.
- Contact's created_at/updated_at/last_interacted_at are omitted from
the proto message -- no client use case identified for them yet.
Full-daemon offline E2E (contacts_e2e_test.rs) proves Sync -> List/Get
over the authenticated API, CreateContact's persistence surviving a
second, independent client connection, and an unauthenticated call
rejected with Code::Unauthenticated.
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Replace CardDavClient::fetch_remote_vcards's hardcoded "Google CardDAV
Sync Contact" return with a real reqwest REPORT/addressbook-query
client that parses a genuine multistatus response. Add a real vCard
(RFC 6350) parser -- the crate previously only had a generator -- and
prove round-trip fidelity through Contact::to_vcard(). Add the
ContactsBackend trait and MockContactsBackend, mirroring
nuncio_cal::CalendarBackend/MockCalendarBackend. Distinguish transport
failures from parse failures via CardDavError::TransportFailed/
ParseFailed rather than ever silently succeeding with fabricated data.
Give nuncio-store real, tested save_contact/list_contacts/get_contact
CRUD backed by a genuine SQLite table + trigram FTS mirror, so contacts
persist through the daemon's own store instead of nuncio-contacts's
disconnected ContactsDatabase pool. This takes nuncio-store as a
narrow, deliberate dependency on nuncio-contacts::Contact.
Opt nuncio-contacts into the workspace lint gate ([lints] workspace,
#![forbid(unsafe_code)]) for parity with every other crate.
Wiremock-only, offline: no live network calls in tests.
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Several gRPC test helpers created an ephemeral sqlite-backed
DatabaseEngine via connect_ephemeral() and then discarded the
returned TempDir guard before returning the still-live db/server to
the caller. Dropping the guard unlinks the directory backing the
sqlite file; on Windows the still-open file handle keeps the data
reachable (masking the bug locally), but on Linux the directory entry
disappears immediately, so any pool connection opened afterward fails
with "unable to open database file" -- exactly the flake CI hit in
nunciod::grpc::tests::preview_rule_dry_runs_against_a_stored_message_without_persisting.
Fixes the same pattern in nunciod::grpc's spawn_test_server and
spawn_filters_test_server helpers, nuncio-cli's HeadlessRunner
(ephemeral_with), and grpc_auth_test's start_server: each now returns
(or stores) the TempDir so it stays alive for as long as its caller
keeps using the database.
Earlier feature commits staged only crates/, leaving the root Cargo.lock behind
when nunciod gained bytes + a sqlx dev-dependency. Re-sync so a --locked build
matches the committed manifests.
Add System.Subscribe server-streaming RPC on the authenticated System service,
mapping every CoreEvent variant to a nuncio.v1.Event oneof. The daemon streams
live EventBus broadcasts (skip-on-lag with a warning; clean close when the bus
drops). Add a client subscribe helper in nuncio-proto. Regression test proves
20 concurrent unary GetStatus calls and 20 streamed events arrive uncorrupted
and in order -- the property the old hand-rolled JSON-RPC IPC demux lacked.
Refs #151
Move the gRPC address resolver and add an authenticated client helper
(connect_system + BearerTokenInterceptor) into the shared nuncio-proto crate so
client shells never depend on the nunciod binary. Wire nuncio-cli's system
status to query the live daemon over gRPC (token from an injectable OS keyring),
printing real status or an honest error when unreachable. Refactor nunciod to
reuse the shared addr items. Reconcile tests to assert the real gRPC round-trip
instead of local ephemeral state.
Refs #150
nunciod now serves the nuncio.v1.System gRPC service on loopback (default
127.0.0.1:9420, env NUNCIO_GRPC_ADDR), authenticated by a bearer token minted
into the OS keyring (SecretManager, CSPRNG, fail-closed). A tonic interceptor
requires 'authorization: Bearer <token>' and compares in constant time
(subtle::ConstantTimeEq); every missing/malformed/wrong-token path returns
Unauthenticated. GetStatus returns live engine status + version. Existing
JSON-RPC IPC (9422) unchanged. Security-reviewed, APPROVED.
Refs #149
New crate crates/nuncio-proto houses the versioned protobuf contract
(proto/nuncio/v1) and tonic-generated client+server stubs, starting with a
minimal System.GetStatus RPC. protoc is vendored via protoc-bin-vendored
(prebuilt binaries; no system protoc, cmake, or C++ toolchain) for reproducible
CI on Linux/macOS/Windows. Uses the tonic 0.14 split (tonic + tonic-prost
runtime, tonic-prost-build codegen). Generated code exempted from workspace
lints; hand-written code keeps them.
Refs #148
Move nuncio-tui, nuncio-gui (+src-tauri, ui), and nuncio-mcp out of the Cargo
workspace into _reference/ (git renames; history preserved), retained for the
Phase 5 native client-repo extraction. Remove the unused nuncio-mcp dev-dep
from nunciod and rework daemon_e2e_test to drive the real nuncio-cli
HeadlessRunner (more coverage, no archived-shell dependency). Workspace is now
engine + daemon + reference CLI (8 members); build/clippy/tests green.
Refs #145