Skip to content

feat: bidirectional CardDAV contact sync#263

Open
salmonumbrella wants to merge 1 commit into
maathimself:mainfrom
salmonumbrella:feat/carddav-bidirectional-sync
Open

feat: bidirectional CardDAV contact sync#263
salmonumbrella wants to merge 1 commit into
maathimself:mainfrom
salmonumbrella:feat/carddav-bidirectional-sync

Conversation

@salmonumbrella

Copy link
Copy Markdown
Contributor

Summary

Makes CardDAV contact sync two-way: MailFlow already reads contacts from a CardDAV server (#209); this adds create, update, and delete back, with conflict handling.

Same posture as the read-only client: standards only, no vendor pinning. Nothing branches on a provider name or hostname, discovery is RFC 6764/6352, and every server capability is discovered and optional — a server that advertises nothing still works read-only. Verified against Nextcloud; the iCloud- and Fastmail-shaped fixtures serve as conformance cases, not provider profiles. Stalwart therefore works out of the box.

Changes

  • Remote-first mutations — local state changes only after the server confirms; an ambiguous write persists its intent and recovers read-only rather than replay a PUT/DELETE.
  • Lossless vCards — unmodeled properties (X-*, CATEGORIES, grouped item1.*, …) survive every edit path, including edits through MailFlow's own CardDAV server.
  • Conflicts — simultaneous edits (and edit-vs-delete) get a side-by-side keep-mine / keep-theirs resolution.
  • Protocol — RFC 6578 incremental sync with full-resync fallback, RFC 7232 conditional writes, honoured Retry-After.
  • UI — contact editor with photos and typed fields, per-book read-only gating, a conflict queue, sync-state markers, all seven locales.
  • Migrations 0034–0037 add the sync schema; pre-existing data changes only via guarded duplicate-book consolidation and removal of the obsolete dupMode key.

Testing

Backend 1,395 unit + 117 PostgreSQL-backed tests (real PostgreSQL 16, real HTTP CardDAV fixture server, wired into CI); frontend 1,418. Verified end-to-end in a browser against a disposable Nextcloud 34 and a live ~900-contact Nextcloud, where created contacts survive a full re-fetch with custom fields intact.

Notes

  1. MailFlow's CardDAV server. This PR prevents it from stripping retained properties on client PUTs — but should MailFlow run a CardDAV server at all, versus bridging to a real one through this client? Your call; the client stands alone either way.
  2. Multiple address books. The client still pulls every book and writes into the first writable one. A follow-up giving each book a role (write target / subscribed / lookup-only, after JMAP's isDefault/isSubscribed) is ready — kept out to hold this PR to one capability.

Contributor License Agreement

By submitting this pull request I confirm that:

  • I have read and agree to the Contributor License Agreement.
  • My contribution is my own original work (or I have identified any
    third-party material and confirmed it is compatible with the CLA).
  • I have the right to submit this contribution under the terms of the CLA.

Adds two-way contact synchronisation with any CardDAV server, building on
the existing read-only client.

- Remote-first create/update/delete: local state changes only after the
  server confirms, with committed intent and read-only recovery for
  ambiguous writes (no replayed PUT/DELETE).
- Retained lossless vCards: properties MailFlow does not model (X-*,
  CATEGORIES, KEY, TZ, grouped item properties) survive every edit path,
  including edits made through MailFlow's own CardDAV server and both
  conflict resolutions.
- Conflict detection and resolution: simultaneous local/remote edits raise
  an explicit conflict with a side-by-side comparison and keep-mine /
  keep-theirs resolution; deletions conflict too.
- RFC 6578 incremental sync with full-resync fallback, RFC 7232 conditional
  writes (If-Match required on mapped writes), and Retry-After throttling.
- Per-book capability discovery (create/update/delete allowed | denied |
  unknown) drives the UI; read-only books are surfaced as such.
- Contacts UI: photos (uploadable from the detail view too), typed
  additional fields with humanized vCard labels, per-operation controls,
  conflict queue, and sync-state markers, translated in all seven locales.
- Contact list pagination orders by a unique key so paging reaches every
  contact; the integration's contact count is derived from the sync ledger
  rather than accumulated, so it cannot drift.

Provider-neutral: no code branches on a provider name or hostname; discovery
is RFC 6764/6352 (current-user-principal -> addressbook-home-set), and every
server capability is optional. Verified against Nextcloud, plus iCloud- and
Fastmail-shaped protocol fixtures.

Migrations 0034-0037 add the sync schema; the only changes to pre-existing
data are a guarded consolidation of duplicate CardDAV address books and
removal of the obsolete dupMode config key.
@salmonumbrella

Copy link
Copy Markdown
Contributor Author

@maathimself patiently waiting for you to tackle this monster 👉 👈

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