# CardDAV endpoint

This document describes the CardDAV subset implemented by `contacts-server`. The endpoint is **read-only in v1**: writes go through D-Bus from native Liberux apps. EDS and KAddressBook see the address book as read-only.

## Bind address and discovery

| Item       | Value                                                                                      |
| ---------- | ------------------------------------------------------------------------------------------ |
| Listener   | `127.0.0.1:0` — kernel-assigned ephemeral port. The server binds, then reads the actual port back from the socket. |
| Scheme     | `http` (no TLS — loopback only).                                                          |
| Port file  | `$XDG_RUNTIME_DIR/liberux/contacts.port` — single line, ASCII decimal, mode 0600.         |
| Discovery  | EDS `.source` files written by `contacts-server` carry the port directly. The runtime port file exists for ad-hoc inspection and for KDE users who add the URL by hand. |

The server **rebinds on every startup** and may pick a different port. Consumers that cache the port (EDS does, in the `.source`) are tolerated by `contacts-server` overwriting the `.source` whenever the port changes; KDE-side cached URLs need to be refreshed by the user, which is acceptable v1 friction.

## URL layout

```
/                                       -- DAV root
/.well-known/carddav                    -- 301 → /
/principals/                            -- principal collection
/principals/liberux/                    -- the single principal
/addressbook/                           -- addressbook home set
/addressbook/main/                      -- the only addressbook collection
/addressbook/main/<resource-id>.vcf     -- one vCard per raw_contact
```

`<resource-id>` is the `raw_contact`'s stable CardDAV UUID, stored in the dedicated `raw_contacts.carddav_uid` column (`UNIQUE`, nullable, populated lazily on first request). The column is decoupled from `sourceid`, which is reserved for upstream sync engine ids — see `docs/schema.md` for the full schema and migration history.

## Authentication

HTTP Basic. Realm: `liberux-contacts`.

| Field         | Value                                                                                      |
| ------------- | ------------------------------------------------------------------------------------------ |
| Username      | `liberux` (fixed; the server ignores any other username and returns 401)                  |
| Password      | A random 32-byte URL-safe base64 token, generated at first launch and stored in Secret Service. |

Missing or incorrect credentials get:

```
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="liberux-contacts", charset="UTF-8"
DAV: 1, 3, addressbook
Content-Length: 0
```

`DAV:` header is included on the 401 so clients that probe before authenticating still learn we speak addressbook.

## Common response headers

Every successful response carries:

```
DAV: 1, 3, addressbook
Server: liberux-contacts/0.1
```

`Content-Type` follows the body (`application/xml; charset=utf-8` for DAV bodies, `text/vcard; charset=utf-8` for vCards).

## Methods

### `OPTIONS`

Applies to any path. Returns 200 with:

```
DAV: 1, 3, addressbook
Allow: OPTIONS, GET, HEAD, PROPFIND, REPORT
Accept-Ranges: none
```

`PUT` and `DELETE` are **not** in `Allow` because the endpoint is read-only.

### `PROPFIND`

Required at four depths/scopes:

| Path                       | Depth     | Behavior                                                                              |
| -------------------------- | --------- | ------------------------------------------------------------------------------------- |
| `/`                        | 0         | Root resource: advertises `current-user-principal`, `principal-URL`, `addressbook-home-set`. |
| `/principals/liberux/`     | 0         | Principal: advertises `addressbook-home-set` → `/addressbook/`.                        |
| `/addressbook/main/`       | 0         | Addressbook collection: `resourcetype` (`collection` + `addressbook`), `displayname`, `getctag`, `sync-token`, `supported-report-set`. |
| `/addressbook/main/`       | 1         | Returns the `propstat` for the collection plus one `<response>` per `.vcf` resource. |

Headers on the multistatus response:

```
HTTP/1.1 207 Multi-Status
Content-Type: application/xml; charset=utf-8
DAV: 1, 3, addressbook
```

Properties supported per resource type:

| Property (CARDDAV / DAV namespace)            | Where         | Source                                                                          |
| --------------------------------------------- | ------------- | ------------------------------------------------------------------------------- |
| `D:resourcetype`                              | all           | static per resource                                                             |
| `D:displayname`                               | collection    | `"Liberux Contacts"`                                                            |
| `D:current-user-principal`                    | root, principal | `/principals/liberux/`                                                        |
| `D:principal-URL`                             | principal     | `/principals/liberux/`                                                          |
| `CARD:addressbook-home-set`                   | principal     | `/addressbook/`                                                                 |
| `CARD:addressbook-description`                | collection    | `"Liberux unified contacts"`                                                    |
| `CARD:supported-address-data`                 | collection    | `<address-data-type content-type="text/vcard" version="3.0"/>`                  |
| `CARD:max-resource-size`                      | collection    | `1048576` (1 MiB; covers a contact with a generous photo)                       |
| `CS:getctag`                                  | collection    | `"<change_counter>"` (string form)                                              |
| `D:sync-token`                                | collection    | `data:,<change_counter>`                                                        |
| `D:supported-report-set`                      | collection    | `addressbook-query`, `addressbook-multiget`, `sync-collection`                  |
| `D:getetag`                                   | resource      | see "ETag"                                                                      |
| `D:getcontenttype`                            | resource      | `text/vcard; charset=utf-8`                                                     |
| `D:getcontentlength`                          | resource      | byte length of the serialized vCard                                             |
| `D:getlastmodified`                           | resource      | `raw_contacts.updated_at` formatted as RFC 1123                                 |
| `CARD:address-data`                           | resource      | the vCard body itself, served only if the request asked for it (otherwise omitted) |

`CS` namespace is `http://calendarserver.org/ns/` (Calendar Server). `getctag` is the de-facto compatibility property; modern clients prefer `sync-token`, but EDS and a few others still rely on `getctag`. Both reflect `change_counter` so they advance together.

Request properties not supported are returned with `404 Not Found` inside the `<propstat>` block, per RFC 4918.

### `REPORT`

Three reports must be supported.

#### `addressbook-query`

Used by clients to fetch a filtered subset (or all) of the resources, possibly with the vCard body included via `<address-data>` in the request.

v1 ignores filter elements and returns every non-deleted raw_contact (matching most clients' "fetch all" usage). A `TODO( carddav-filter )` comment in the implementation marks this for v2.

Response: 207 multistatus, one `<response>` per resource, including `getetag`, `getlastmodified`, and (if asked) `address-data`.

#### `addressbook-multiget`

Used by clients to fetch a specific list of resource hrefs in one round-trip, after a previous PROPFIND or sync.

The request body lists `<href>/addressbook/main/<uuid>.vcf</href>` elements. Response 207 multistatus, with each `<response>` either filled in (200) or `<status>HTTP/1.1 404 Not Found</status>` for unknown / deleted resources.

#### `sync-collection` (RFC 6578)

Incremental sync. Request body:

```xml
<sync-collection xmlns="DAV:">
    <sync-token>data:,42</sync-token>
    <sync-level>1</sync-level>
    <prop>
        <getetag/>
    </prop>
</sync-collection>
```

Behavior:

- If `sync-token` is missing or empty, the server returns a full enumeration plus the current `sync-token`. This is the "initial sync" path.
- If `sync-token` is present and **older** than the current `change_counter`, return one `<response>` per resource changed since (added/modified) plus one `<response>` with `<status>HTTP/1.1 404 Not Found</status>` for each resource deleted since, then the new `sync-token`.
- If `sync-token` is present and **equal to or newer than** the current counter, return an empty `<multistatus>` plus the current `sync-token`.
- If `sync-token` is present but unrecognized / too old (server lost the history needed to answer), return `403 Forbidden` with `valid-sync-token` precondition. v1 always retains enough history (it can synthesize "everything is new" from a counter mismatch by re-emitting the full set), so this branch is reserved for v2 if we add log truncation.

To answer "what changed since counter N" we maintain a `change_log` table:

```sql
CREATE TABLE change_log (
    counter        INTEGER PRIMARY KEY,
    raw_contact_id INTEGER NOT NULL,
    change_kind    TEXT    NOT NULL,    -- 'put' | 'delete'
    happened_at    INTEGER NOT NULL
);
```

Each mutation appends one row. Rows older than the last sync acknowledged by every consumer can be truncated; v1 keeps everything (small tables, low write rate).

Response headers always include the new sync token in the multistatus body, never as an HTTP header.

### `GET /addressbook/main/<uuid>.vcf`

Serves the on-the-fly serialized vCard 3.0 for the raw_contact whose UUID is `<uuid>`. Headers:

```
HTTP/1.1 200 OK
Content-Type: text/vcard; charset=utf-8
ETag: "<etag>"
Last-Modified: <RFC1123 of updated_at>
Content-Length: <bytes>
```

`HEAD` works the same way without the body. `If-None-Match` is honored and yields `304 Not Modified` when matching.

If the raw_contact is soft-deleted or unknown, returns `404 Not Found`.

### `PUT` and `DELETE`

```
HTTP/1.1 403 Forbidden
DAV: 1, 3, addressbook
Content-Type: text/plain; charset=utf-8
Content-Length: <n>

This address book is read-only via CardDAV. Use the org.liberux.Contacts1 D-Bus API to modify contacts.
```

The text body is informational; some clients show it to the user.

### Other methods

`MKCOL`, `MOVE`, `COPY`, `LOCK`, `UNLOCK`, `MKCALENDAR`, `PROPPATCH`, `POST` → `405 Method Not Allowed` with `Allow: OPTIONS, GET, HEAD, PROPFIND, REPORT`.

## ETag

`ETag` for a resource is computed deterministically from the serialized vCard bytes:

```
etag = first 16 hex chars of SHA-256( vcard_bytes )
ETag header: "liberux-1-<etag>"
```

The leading `liberux-1-` is a generation prefix: bumping it (to `liberux-2-`) invalidates every cached ETag at clients in case we change the serialization in a non-content-affecting way.

The ETag changes if and only if the serialized vCard changes. That is sufficient for client-side cache correctness — content-equal vCards across versions correctly hit the same ETag, and clients that compare ETags catch every meaningful change.

## Sync token

| Item         | Value                                       |
| ------------ | ------------------------------------------- |
| Format       | `data:,<change_counter>` (RFC 2397-shaped)  |
| Source       | `sync_state.change_counter`                 |
| Monotonicity | Strictly increasing across the lifetime of the database. Survives restarts. |
| `getctag`    | The decimal counter, quoted as a string. Advances together with `sync-token`. |

We use the `data:` URI form because RFC 6578 requires sync-tokens to be valid URIs. `data:,<n>` is short, opaque to clients, and trivially parseable on our side.

## Backpressure and limits

- Maximum request body: 1 MiB (covers any reasonable PROPFIND with a long `<href>` list). 413 Payload Too Large otherwise.
- Maximum concurrent connections: 16. Excess connections wait in the accept queue.
- No keep-alive timeout shorter than 30 s — EDS holds connections open between syncs.

## Headers we never set

- `Set-Cookie` — no sessions.
- `WWW-Authenticate: Digest` — Basic only.
- `Strict-Transport-Security`, `Content-Security-Policy` — irrelevant on loopback HTTP.

## Verification checklist

Before shipping, manually verify against:

1. `curl -u liberux:<token> http://127.0.0.1:<port>/` PROPFIND with Depth 0 returns the expected current-user-principal.
2. EDS `evolution-source-registry` discovers the address book within 30 s of startup.
3. GNOME Contacts shows the address book in its source list.
4. KAddressBook (manual URL entry) lists contacts.
