Replaces CardDavClient::fetch_remote_vcards's hardcoded "Google CardDAV Sync Contact" return with a real reqwest-based CardDAV REPORT/addressbook-query client (RFC 6352) that parses a genuine multistatus response.
Adds a real vCard (RFC 6350) parser (VCardParserAdapter) — the crate previously only had a generator (Contact::to_vcard) — and proves lossless round-trip for FN/N/ORG/TITLE/primary EMAIL/TEL via vcard_round_trip_preserves_core_fields.
Expands CardDavError to ParseFailed/TransportFailed, mirroring CalendarError, so a transport failure and a malformed-vCard parse failure are distinguishable typed errors — never a fabricated empty/hardcoded success.
Adds real, tested save_contact/list_contacts/get_contact to nuncio_store::db::DatabaseEngine, backed by a genuine contacts SQLite table + trigram FTS mirror (contacts_fts), so contacts persist through the daemon's single owned store instead of nuncio-contacts's disconnected ContactsDatabase pool.
Opts nuncio-contacts into the workspace lint gate: [lints]\nworkspace = true in Cargo.toml, #![forbid(unsafe_code)] in lib.rs.
nuncio-contacts::db::ContactsDatabase is left as-is (not deleted); it is a known, intentional dead-code gap from the daemon's perspective until the CLI's Commands::Contact handlers are rewired in M2-S2 (out of scope here, same as runner.rs:453-464's existing comment style for the analogous mail-vertical gap).
Decision: dependency direction (flagged for PO review)
The issue's Cargo.toml bullet for crates/nuncio-contacts/Cargo.toml says to add nuncio-store as a dependency there, but the issue's store-CRUD bullet says nuncio-store gains a dependency on nuncio_contacts::Contact. Both together would be a dependency cycle. I implemented the direction that is unambiguous and repeated multiple times in the issue text (nuncio-store depends on nuncio-contacts, not the reverse) and did not add nuncio-store as a dependency of nuncio-contacts. Flagging this discrepancy explicitly for review.
No proto, no nunciod, no CLI changes — engine-only, per the issue's explicit scope (M2-S2 wires this into the daemon/gRPC/CLI).
Closes #187.
## Summary
- Replaces `CardDavClient::fetch_remote_vcards`'s hardcoded "Google CardDAV Sync Contact" return with a real `reqwest`-based CardDAV `REPORT`/`addressbook-query` client (RFC 6352) that parses a genuine `multistatus` response.
- Adds a real vCard (RFC 6350) parser (`VCardParserAdapter`) — the crate previously only had a generator (`Contact::to_vcard`) — and proves lossless round-trip for `FN`/`N`/`ORG`/`TITLE`/primary `EMAIL`/`TEL` via `vcard_round_trip_preserves_core_fields`.
- Adds `ContactsBackend` trait + `MockContactsBackend`, mirroring `nuncio_cal::CalendarBackend`/`MockCalendarBackend` exactly.
- Expands `CardDavError` to `ParseFailed`/`TransportFailed`, mirroring `CalendarError`, so a transport failure and a malformed-vCard parse failure are distinguishable typed errors — never a fabricated empty/hardcoded success.
- Adds real, tested `save_contact`/`list_contacts`/`get_contact` to `nuncio_store::db::DatabaseEngine`, backed by a genuine `contacts` SQLite table + trigram FTS mirror (`contacts_fts`), so contacts persist through the daemon's single owned store instead of `nuncio-contacts`'s disconnected `ContactsDatabase` pool.
- Opts `nuncio-contacts` into the workspace lint gate: `[lints]\nworkspace = true` in `Cargo.toml`, `#![forbid(unsafe_code)]` in `lib.rs`.
- `nuncio-contacts::db::ContactsDatabase` is left as-is (not deleted); it is a known, intentional dead-code gap from the daemon's perspective until the CLI's `Commands::Contact` handlers are rewired in **M2-S2** (out of scope here, same as `runner.rs:453-464`'s existing comment style for the analogous mail-vertical gap).
## Decision: dependency direction (flagged for PO review)
The issue's Cargo.toml bullet for `crates/nuncio-contacts/Cargo.toml` says to add `nuncio-store` as a dependency there, but the issue's store-CRUD bullet says `nuncio-store` gains a dependency on `nuncio_contacts::Contact`. Both together would be a dependency cycle. I implemented the direction that is unambiguous and repeated multiple times in the issue text (`nuncio-store` depends on `nuncio-contacts`, not the reverse) and did **not** add `nuncio-store` as a dependency of `nuncio-contacts`. Flagging this discrepancy explicitly for review.
No proto, no `nunciod`, no CLI changes — engine-only, per the issue's explicit scope (M2-S2 wires this into the daemon/gRPC/CLI).
## Gate evidence
```
cargo fmt --all -- --check # clean, no output
cargo check-all # clippy --all-targets --workspace -D warnings: clean, 0 warnings
cargo test-all # full workspace test-all: 0 failed, 0 ignored
nuncio-contacts: 12 unit tests + 3 wiremock tests = 15 passed
nuncio-store: 53 unit tests (incl. 3 new contact CRUD tests) = 53 passed
workspace total: all suites green (nuncio-cal 23+2, nuncio-cli 27+4, nuncio-contacts 12+3,
nuncio-core 52, nuncio-filter 24+2, nuncio-mail 36+1, nuncio-proto 19,
nuncio-store 53, nunciod 75+1+3+1) — 0 ignored throughout
```
Local pre-commit hook (fmt + clippy `-D warnings` + full test-all) ran and passed on the commit.
## Test plan
- [x] `cargo test -p nuncio-contacts` — 15/15 passed (unit + wiremock)
- [x] `cargo test -p nuncio-store` — 53/53 passed (incl. `save_get_list_and_search_contact`, `save_contact_upserts_existing_row_and_fts_entry`, `get_contact_reports_not_found_for_missing_contact`)
- [x] `cargo test-all` — full workspace green, 0 ignored
- [x] `cargo check-all` — 0 clippy warnings
- [x] `cargo fmt --all -- --check` — clean
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #187.
Summary
CardDavClient::fetch_remote_vcards's hardcoded "Google CardDAV Sync Contact" return with a realreqwest-based CardDAVREPORT/addressbook-queryclient (RFC 6352) that parses a genuinemultistatusresponse.VCardParserAdapter) — the crate previously only had a generator (Contact::to_vcard) — and proves lossless round-trip forFN/N/ORG/TITLE/primaryEMAIL/TELviavcard_round_trip_preserves_core_fields.ContactsBackendtrait +MockContactsBackend, mirroringnuncio_cal::CalendarBackend/MockCalendarBackendexactly.CardDavErrortoParseFailed/TransportFailed, mirroringCalendarError, so a transport failure and a malformed-vCard parse failure are distinguishable typed errors — never a fabricated empty/hardcoded success.save_contact/list_contacts/get_contacttonuncio_store::db::DatabaseEngine, backed by a genuinecontactsSQLite table + trigram FTS mirror (contacts_fts), so contacts persist through the daemon's single owned store instead ofnuncio-contacts's disconnectedContactsDatabasepool.nuncio-contactsinto the workspace lint gate:[lints]\nworkspace = trueinCargo.toml,#![forbid(unsafe_code)]inlib.rs.nuncio-contacts::db::ContactsDatabaseis left as-is (not deleted); it is a known, intentional dead-code gap from the daemon's perspective until the CLI'sCommands::Contacthandlers are rewired in M2-S2 (out of scope here, same asrunner.rs:453-464's existing comment style for the analogous mail-vertical gap).Decision: dependency direction (flagged for PO review)
The issue's Cargo.toml bullet for
crates/nuncio-contacts/Cargo.tomlsays to addnuncio-storeas a dependency there, but the issue's store-CRUD bullet saysnuncio-storegains a dependency onnuncio_contacts::Contact. Both together would be a dependency cycle. I implemented the direction that is unambiguous and repeated multiple times in the issue text (nuncio-storedepends onnuncio-contacts, not the reverse) and did not addnuncio-storeas a dependency ofnuncio-contacts. Flagging this discrepancy explicitly for review.No proto, no
nunciod, no CLI changes — engine-only, per the issue's explicit scope (M2-S2 wires this into the daemon/gRPC/CLI).Gate evidence
Local pre-commit hook (fmt + clippy
-D warnings+ full test-all) ran and passed on the commit.Test plan
cargo test -p nuncio-contacts— 15/15 passed (unit + wiremock)cargo test -p nuncio-store— 53/53 passed (incl.save_get_list_and_search_contact,save_contact_upserts_existing_row_and_fts_entry,get_contact_reports_not_found_for_missing_contact)cargo test-all— full workspace green, 0 ignoredcargo check-all— 0 clippy warningscargo fmt --all -- --check— clean