Import a single contact from a connected CardDAV address book #98

Open
opened 2026-06-18 02:33:29 +00:00 by james · 0 comments
Owner

Bulk-importing a whole address book is the wrong shape for Carol — most contacts in a CardDAV book aren't relevant to a user's career-network model. The right shape is selective: when adding a contact, the user should be able to search their address book for one specific person and pull them in, instead of retyping name / email / phone / org by hand.

User-facing shape

Next to the existing "New contact" action, add a split / dropdown control:

[+ New contact ▼]
        ├─ Enter manually
        └─ Import from Address Book…

Selecting "Import from Address Book…" opens a modal (or inline panel) with:

  1. A search box that queries the connected CardDAV address book(s) as the user types. Debounced — query fires on a settle delay, not per keystroke.
  2. A result list showing each match with the fields a user needs to disambiguate: name, primary email, primary phone, organization, source address book (when more than one is connected).
  3. An Import button per result. Clicking it creates the matching People row in Carol, scoped to the calling user, and closes the modal back into wherever the user invoked it from (the People list, an Organization page, etc.).

The dropdown's import option is disabled (with a tooltip / link to settings) when no CardDAV connection exists for the user.

Prereq: a CardDAV connection foundation ticket

This ticket assumes the user has already configured a CardDAV server — URL, credentials, which address books to expose. That setup deserves its own ticket because:

  • It needs encrypted-at-rest credential storage (CardDAV typically uses basic auth, which is a password to handle carefully).
  • It's the right shape to power future related features (CalDAV calendar imports, periodic sync, "show me my recent contacts" widgets).
  • It's per-user — credentials never cross user boundaries.

Suggested split: a sibling "CardDAV connection setup" ticket that adds Settings → Integrations → CardDAV (URL + username + password + discovered address books), and this ticket consuming that connection. Happy to file that separately when this ticket is picked up — flag in PR or comment.

Scope (this ticket)

Assuming the connection exists:

  • A new dropdown control on the "New contact" call-to-action, wherever that lives today.
  • A modal / panel with a debounced search box and result list.
  • The CardDAV query itself — preferring server-side filtering via addressbook-query REPORT (RFC 6352 §8.6) over client-side filtering of a full-book fetch. Servers that don't honour the REPORT filter degrade to client-side filtering with a clear log.
  • vCard → People field mapping (see below).
  • Idempotent import by vCard UID — re-importing the same contact updates the existing People row rather than duplicating.
  • Caching: a short-lived in-memory result cache (per-user, per-query) to avoid re-hitting CardDAV on every keystroke during a refine. No persistent address-book mirror in this ticket.

vCard → People field mapping

Map the standard vCard properties Carol's People shape can hold; surface unmapped properties in a single notes field rather than dropping them silently:

vCard People field Notes
FN name Required; if missing, skip with a warning in the result list.
EMAIL email First TYPE=PREF or first listed. Carry the rest into notes.
TEL phone Same selection rule as email.
ORG organization Match an existing Organizations row for the user by normalized name, link by FK; create one if not found (same scoping rules apply).
TITLE / ROLE role Free text.
BDAY birthday If People carries this field today; otherwise drop to notes.
URL notes One line per URL, prefixed with the optional TYPE.
NOTE notes Appended.
UID external_id Stable per vCard; used for idempotency on re-import.
REV external_updated Last-modified timestamp; informational.

Anything else (photo, geo, X-extensions): out of scope for v1. Photo support is interesting but it's an image-pipeline question, not a contact-shape question — file as a follow-up if/when it matters.

Acceptance criteria

  • On a user with a configured CardDAV connection, the "New contact" control shows a dropdown that includes "Import from Address Book…".
  • On a user with no configured CardDAV connection, the dropdown item is disabled with a tooltip pointing at the settings page (or simply not shown — UX decision).
  • Typing a query in the import modal returns matches from the user's address book(s), debounced; results show name + the disambiguators above.
  • Selecting a result and clicking Import creates a People row scoped to the calling user, with the vCard mapping above, and the modal closes back to where the user invoked it.
  • Re-importing the same contact (same vCard UID) updates the existing People row in place rather than creating a duplicate. Field-level changes from CardDAV overwrite Carol's values — see "Conflict policy" question below.
  • An Organization referenced by ORG is matched against the user's existing organizations by normalized name; a new one is created if no match. The created Organizations row is also scoped to the calling user.
  • CardDAV failures (auth expired, server unreachable, malformed vCard) produce a clear error in the modal that does NOT leak the underlying CardDAV password into the UI or logs.
  • The CardDAV password is encrypted at rest (per the connection-foundation ticket); this ticket's UI never reflects the password value back to the user or includes it in any client-side payload.

Design questions to settle before implementation

  • Library vs hand-rolled. tsdav is the actively-maintained TS CardDAV/CalDAV client and would cut a real amount of XML wrangling, at the cost of one more devDep (with lavamoat.allowScripts implications per ADR-0010). Hand-rolling against fetch + an XML parser is feasible but every server's quirk becomes our problem. tsdav is the recommended starting point; revisit if it pulls in too much.
  • Search strategy when addressbook-query is unavailable. Some CardDAV servers (or specific address books) don't honor the property-filter REPORT. Fall back to a full-book fetch + client-side filter? Or refuse, log, and tell the user "this address book doesn't support server-side search"? Lean toward fallback for usability but with a server-side log.
  • Conflict policy on re-import. Default proposed above is "CardDAV overwrites Carol values for mapped fields". Alternative: "CardDAV adds new fields but does not overwrite ones the user has manually edited". The latter needs a "modified locally" bit per field, which is more state. Open question.
  • Multiple address books per connection. A CardDAV server typically advertises several. Search all by default? Let the user pick which to search? Default-all with a filter chip is the path of least friction.
  • Where to invoke the import from. "New contact" button on a People list page is obvious. Less obvious: should there also be an "import this person's manager / direct reports" entry point from an existing People page? Out of scope for v1; flag as a follow-up.

Out of scope today

  • Full-address-book bulk import.
  • Bidirectional sync (Carol → CardDAV or periodic CardDAV → Carol).
  • vCard photo import.
  • Group / category support from vCard.
  • CalDAV (calendar / events) — sibling concern that would share the connection-foundation work.

Part of epic #2. Depends on a "CardDAV connection foundation" ticket (file separately).

Bulk-importing a whole address book is the wrong shape for Carol — most contacts in a CardDAV book aren't relevant to a user's career-network model. The right shape is selective: when adding a contact, the user should be able to *search* their address book for one specific person and pull them in, instead of retyping name / email / phone / org by hand. ## User-facing shape Next to the existing "New contact" action, add a split / dropdown control: ``` [+ New contact ▼] ├─ Enter manually └─ Import from Address Book… ``` Selecting **"Import from Address Book…"** opens a modal (or inline panel) with: 1. A search box that queries the connected CardDAV address book(s) as the user types. Debounced — query fires on a settle delay, not per keystroke. 2. A result list showing each match with the fields a user needs to disambiguate: name, primary email, primary phone, organization, source address book (when more than one is connected). 3. An **Import** button per result. Clicking it creates the matching `People` row in Carol, scoped to the calling user, and closes the modal back into wherever the user invoked it from (the People list, an Organization page, etc.). The dropdown's import option is disabled (with a tooltip / link to settings) when no CardDAV connection exists for the user. ## Prereq: a CardDAV connection foundation ticket This ticket assumes the user has *already* configured a CardDAV server — URL, credentials, which address books to expose. That setup deserves its own ticket because: - It needs encrypted-at-rest credential storage (CardDAV typically uses basic auth, which is a password to handle carefully). - It's the right shape to power future related features (CalDAV calendar imports, periodic sync, "show me my recent contacts" widgets). - It's per-user — credentials never cross user boundaries. Suggested split: a sibling **"CardDAV connection setup"** ticket that adds Settings → Integrations → CardDAV (URL + username + password + discovered address books), and **this** ticket consuming that connection. Happy to file that separately when this ticket is picked up — flag in PR or comment. ## Scope (this ticket) Assuming the connection exists: - A new dropdown control on the "New contact" call-to-action, wherever that lives today. - A modal / panel with a debounced search box and result list. - The CardDAV query itself — preferring server-side filtering via `addressbook-query` REPORT (RFC 6352 §8.6) over client-side filtering of a full-book fetch. Servers that don't honour the REPORT filter degrade to client-side filtering with a clear log. - vCard → `People` field mapping (see below). - Idempotent import by vCard `UID` — re-importing the same contact updates the existing `People` row rather than duplicating. - Caching: a short-lived in-memory result cache (per-user, per-query) to avoid re-hitting CardDAV on every keystroke during a refine. No persistent address-book mirror in this ticket. ## vCard → `People` field mapping Map the standard vCard properties Carol's `People` shape can hold; surface unmapped properties in a single `notes` field rather than dropping them silently: | vCard | `People` field | Notes | |---|---|---| | `FN` | `name` | Required; if missing, skip with a warning in the result list. | | `EMAIL` | `email` | First `TYPE=PREF` or first listed. Carry the rest into `notes`. | | `TEL` | `phone` | Same selection rule as email. | | `ORG` | `organization` | Match an existing `Organizations` row for the user by normalized name, link by FK; create one if not found (same scoping rules apply). | | `TITLE` / `ROLE` | `role` | Free text. | | `BDAY` | `birthday` | If `People` carries this field today; otherwise drop to `notes`. | | `URL` | `notes` | One line per URL, prefixed with the optional `TYPE`. | | `NOTE` | `notes` | Appended. | | `UID` | `external_id` | Stable per vCard; used for idempotency on re-import. | | `REV` | `external_updated`| Last-modified timestamp; informational. | Anything else (photo, geo, X-extensions): out of scope for v1. Photo support is interesting but it's an image-pipeline question, not a contact-shape question — file as a follow-up if/when it matters. ## Acceptance criteria - [ ] On a user with a configured CardDAV connection, the "New contact" control shows a dropdown that includes "Import from Address Book…". - [ ] On a user with **no** configured CardDAV connection, the dropdown item is disabled with a tooltip pointing at the settings page (or simply not shown — UX decision). - [ ] Typing a query in the import modal returns matches from the user's address book(s), debounced; results show name + the disambiguators above. - [ ] Selecting a result and clicking Import creates a `People` row scoped to the calling user, with the vCard mapping above, and the modal closes back to where the user invoked it. - [ ] Re-importing the same contact (same vCard `UID`) updates the existing `People` row in place rather than creating a duplicate. Field-level changes from CardDAV overwrite Carol's values — see "Conflict policy" question below. - [ ] An Organization referenced by `ORG` is matched against the user's existing organizations by normalized name; a new one is created if no match. The created `Organizations` row is also scoped to the calling user. - [ ] CardDAV failures (auth expired, server unreachable, malformed vCard) produce a clear error in the modal that does NOT leak the underlying CardDAV password into the UI or logs. - [ ] The CardDAV password is encrypted at rest (per the connection-foundation ticket); this ticket's UI never reflects the password value back to the user or includes it in any client-side payload. ## Design questions to settle before implementation - **Library vs hand-rolled.** [`tsdav`](https://github.com/natelindev/tsdav) is the actively-maintained TS CardDAV/CalDAV client and would cut a real amount of XML wrangling, at the cost of one more devDep (with `lavamoat.allowScripts` implications per ADR-0010). Hand-rolling against `fetch` + an XML parser is feasible but every server's quirk becomes our problem. tsdav is the recommended starting point; revisit if it pulls in too much. - **Search strategy when `addressbook-query` is unavailable.** Some CardDAV servers (or specific address books) don't honor the property-filter REPORT. Fall back to a full-book fetch + client-side filter? Or refuse, log, and tell the user "this address book doesn't support server-side search"? Lean toward fallback for usability but with a server-side log. - **Conflict policy on re-import.** Default proposed above is "CardDAV overwrites Carol values for mapped fields". Alternative: "CardDAV adds new fields but does not overwrite ones the user has manually edited". The latter needs a "modified locally" bit per field, which is more state. Open question. - **Multiple address books per connection.** A CardDAV server typically advertises several. Search all by default? Let the user pick which to search? Default-all with a filter chip is the path of least friction. - **Where to invoke the import from.** "New contact" button on a People list page is obvious. Less obvious: should there also be an "import this person's manager / direct reports" entry point from an existing People page? Out of scope for v1; flag as a follow-up. ## Out of scope today - Full-address-book bulk import. - Bidirectional sync (Carol → CardDAV or periodic CardDAV → Carol). - vCard photo import. - Group / category support from vCard. - CalDAV (calendar / events) — sibling concern that would share the connection-foundation work. Part of epic #2. Depends on a "CardDAV connection foundation" ticket (file separately).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#98
No description provided.