Documentation
CardDAV behavior and storage reference
backend-contacts-http is a multi-instance CardDAV backend for Everlock.
Current state
- multi-instance and multi-vhost
- per-instance
versioned-storage - Everlock-user HTTP authentication and
http/contacts/<instance>access control - multiple address books per instance
- plain persisted
.vcfcontacts, stored in vCard 4.0 and served in 3.0 or 4.0, with server-side create, update, and delete - CardDAV discovery,
PROPFIND,PROPPATCH,GET,PUT,DELETE,MKCOL - conditional requests with ETags, checked again as the write is committed
REPORT sync-collectionusing real store commit ids as sync tokens
Shape
Each contacts instance has:
- one instance name
- one store
- one or more vhosts
- one CardDAV namespace served through
frontend-http
Example config:
enabled = true
[instances.default]
store = "everlock-contacts"
vhosts = ["contacts.example.com"]
Stores and vhosts
store=<name> picks the store explicitly and is used as given — an existing
store, a hosted git repository or a store another instance uses included —
as long as it is a valid name: lowercase letters, digits and -, not first or
last. Without store=, the store is the instance name, and /contacts create
refuses it when a store or repository of that name already exists, another
instance uses it, or Everlock reserves it (everlock-system, everlock-mail
and every backend's bootstrap store such as calendar and
everlock-contacts). The refusal says what the name collides with; pass
store=<name> to use it on purpose, or pick another name.
Instances that share a store serve the same data: deleting an address book through one deletes it for all.
A vhost belongs to one contacts instance; /contacts create and /contacts set
refuse a vhost another instance maps, comparing without regard to case. The
creating user becomes Owner of the new instance. When two configured
instances list one vhost, the first by name serves it and a warning is logged.
A config file that is not TOML stops the backend from starting, while a single
malformed instance entry is skipped with a warning.
CardDAV lives under /carddav, with discovery at /.well-known/carddav.
An instance holds those two paths on each of its vhosts and nothing else, so
a site, a file share or the calendar backend serves the rest of the same
host; /contacts create and /contacts set refuse a vhost where another backend
already holds one of the two paths. Virtual hosts explains how
backends share a host.
.local vhosts are announced over mDNS as CardDAV services with
path=/carddav as soon as an instance takes them on, and withdrawn when it
gives them up.
A vhost inside a zone the DNS backend serves carries
the RFC 6764 records clients look up when given just the vhost name:
_carddavs._tcp.<vhost> SRV 0 1 <HTTPS port> <vhost>. with a TXT record
path=/carddav beside it, and _carddav._tcp.<vhost> SRV 0 0 0 ., as
CardDAV runs over HTTPS. The port is the one the HTTP frontend serves HTTPS
on, and the records follow a vhost from the moment /contacts create or
/contacts set saves it.
Storage
Contacts are stored as plain .vcf files in versioned-storage.
The current store layout is:
default/
config.toml
alice-smith.vcf
alice-smith-2.vcf
work/
config.toml
vendor-x.vcf
Each contact is one .vcf file in vCard 4.0, named by the URL the client
writes it to. A 4.0 card holds what the client sent, with CRLF line breaks
and a UID added where one is missing. A 3.0 card — what Apple Contacts
writes — is converted to 4.0 before it is stored. When the stored card
differs from what was sent, the answer to the write leaves out the ETag, and
the client fetches the stored version.
A UID is unique within an address book, and a contact keeps its UID: a
write that gives an existing contact another one is refused with 403
no-uid-conflict. The check looks the UID up in a set of hashes Everlock
keeps in memory per address book, so bulk uploads stay fast.
vCard 3.0 and 4.0
Clients read each contact in the version they ask for:
- Apple Contacts asks for no version and gets vCard 3.0, which is the
CardDAV default; so does a
GETwithout anAcceptheader - DAVx⁵ asks for 4.0 and gets the stored card as it is
- a
GETwithAccept: text/vcard; version=4.0gets 4.0
Every answer names the version it carries in its content type, such as
text/vcard; charset=utf-8; version=3.0. An address book listing gives each
contact the size and type of the 3.0 card a plain GET returns.
The conversion maps what both versions have — preferred types, inline
photos, Apple's groups, birthdays without a year, address labels, sort
strings — and carries what only one of them has in X-EVERLOCK- properties
and parameters, so a card goes from one version to the other and back
with everything it held. Apple's own X- properties, such as custom labels,
pass as they are. Every version of a contact carries the same ETag, so a
client's next write finds the card it read. Address book queries filter the
stored 4.0 card.
Address book properties
An address book's name and description live in its config.toml, and every
other property a client sets on it is kept there as sent and returned when
asked for; PROPPATCH with remove deletes one. Properties the server works
out itself are refused with 403.
A PROPFIND gets the properties it names, and a 404 for each one the
address book or contact lacks; a request for everything gets the full set.
Access model
The access path is:
http/contacts/<instance>
Role mapping:
Readerfor discovery, listing, fetch,PROPFIND, andREPORTWriterforPUT,DELETE,MKCOL, andPROPPATCH
The current model is instance-wide. There are no per-address-book ACLs yet.
CardDAV surface
Current URL shape:
/.well-known/carddav
/.well-known/carddav/
/carddav/
/carddav/principals/me/
/carddav/addressbooks/
/carddav/addressbooks/<book>/
/carddav/addressbooks/<book>/<contact>.vcf
Supported today:
- discovery and
PROPFIND - contact
GET/HEAD REPORT addressbook-multigetREPORT addressbook-query, with property and parameter filters, match types and negationREPORT sync-collection- writable
PUT,DELETE, andMKCOL, an extendedMKCOLsetting the new address book's properties PROPPATCHsetting and removing address book properties
Sync
Address-book sync tokens use the real versioned-storage commit id.
That means incremental sync is derived directly from store history instead of a second sync database. The token a sync hands out stands for exactly the contacts it reports, and a token the address book cannot sync from makes the client start over with a full sync.