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>
* feat(calendar): expand RRULE recurrence in ListEvents and filter by calendar_id
Wire the previously-dead nuncio_cal::RecurrenceEngine into
CalendarGrpcService::list_events: non-recurring events pass through
unchanged, while recurring masters are expanded into real occurrences
for the requested window via a new store method
(list_recurring_calendar_events, no time filter since a master's own
timestamps can precede the window while its occurrences fall inside
it) and a pure expand_events_for_window merge helper that keeps
recurring rows sourced only from expansion, never double-counting a
master that also overlaps the window in the raw query.
Fix expand_occurrences to stop trusting an empty match as "return the
raw master": that fabricated an out-of-window result whenever a
window fell entirely outside a bounded (UNTIL/COUNT) series, and could
hang scanning an unbounded series for a match that would never come.
skip_while/take_while over the RRuleSet's monotonic iterator bounds
the walk and returns a genuinely empty result instead.
Also apply the calendar_id filter list_events accepted but ignored,
closing the gap between it and the mock backend's own filtering.
Refs #186
* docs(nunciod): reword stale comment referencing removed rrule fallback
---------
Co-authored-by: James Maes <jmaes@dmdbrands.com>
Wire the orphaned nuncio-cal engine into nunciod as a real, authenticated
gRPC vertical mirroring Mail: add service Calendar (Sync/ListEvents/GetEvent)
to nuncio.proto with a regenerated descriptor golden, connect_calendar in
nuncio-proto's client, calendar_sync::sync_with_backend plus
CalendarEngineOverrides/CalendarGrpcService mounted behind its own
BearerAuthInterceptor in nunciod::grpc, and real `cal sync`/`cal list` gRPC
clients in nuncio-cli replacing the fabricated CLI output. ListEvents/GetEvent
always read genuinely persisted data; production Sync returns an honest error
since there is no persisted per-account CalDAV configuration yet, and only an
injected MockCalendarBackend exercises a working Sync. Adds unit, daemon
integration, and offline full-daemon E2E tests, including an
unauthenticated-call-rejected assertion for the new service.
Refs #185
Co-authored-by: James Maes <jmaes@dmdbrands.com>