What is this document?
As server developers, we get a lot of questions on how to interact with aCardDAV server. This document explains how to integrate correctly with aCardDAV server.
This document (should) apply for any CardDAV server, not just SabreDAV.
Clients
Before you build your own client, there's a chance there's already a clientavailable for your programming language.
We've developed a PHP client that does some DAV-related stuff and makes ita tad easier. More information can be found on this wiki.
CalConnect DEVGUIDE maintains an updated list of CardDAV clients.
High-level protocol
CardDAV is defined by rfc6352. CardDAV is heavily inspired by itscounterpart CalDAV, and is mostly regarded as simpler.
CardDAV builds on WebDAV. WebDAV itself extends HTTP.
Some operations will be very familiar if you already have experience with HTTPservices (GET
, PUT
and DELETE
), but a number of new methods have beenadded to this list (PROPFIND
, PROPPATCH
, REPORT
, MKCOL
, MKCALENDAR
,ACL
).
Most HTTP clients should just support methods they don't know about. So it'ssmart to simply use a stock HTTP client for your platform, if your platformdoes not already have a specialized CardDAV client.
vCards
Every contact is submitted as a vCard. Every compliant CardDAV client orServer must support vCard 3.0 (rfc2425 and rfc2426).vCard 2.1 is way too old and should always be rejected.
vCard 4.0 (rfc6350) also exists though, and is in many respects amassive improvement over vCard 3.0. vCard 4 must now always be encoded asUTF-8, and many inconsistencies and problems have been fixed.
However, compliant servers must specifically advertise that they supportvCard 4.0, and clients must be willing to send vCard 3.0 if the server doesnot support it.
The current SabreDAV server supports vCard 4 and jCard, and will automaticallyconvert in between vCard 3, 4 and jCard on demand.
One thing we specifically want to warn people for, is that even though thevCard format seems easy to parse and generate, there are a lot of little rulesthat make it complicated.
A simple vCard may look like this:
BEGIN:VCARDVERSION:4.0FN:Evert PotN:Pot;Evert;;;END:VCARD
Don't fall into the trap of thinking every line is simply in the formatpropertyname colon propertyvalue
.
There's:
- Mixed character encoding, sometimes differing per line
- Different escaping mechanisms of values, which depends on the name of thevalue.
- Parameters, with different escaping mechanisms and a new (rfc6868) standardescaping mechanism that no-one supports yet.
- Line-folding. Sometimes single multi-byte UTF-8 characters are split up witha new-line.
- Two styles of new-lines, sometimes within the same document (
\n
and\r\n
). - Quoted-printable encoding and base64 encoding.
- Parameters that have their name omitted, because it's implied from theirvalues.
- Properties can be grouped together with a special syntax that alters theencoding of a property group.
Why did I write this list? Because if you're going to parse and generatevCards, you should either:
- Be fully aware of the scope of doing so, or:
- Use a parser that somebody already wrote for your programming language.
vCard parsers, per language
Language | Library |
---|---|
PHP | sabre/vobject |
Java | ez-vcard |
Ruby | vcard |
CalConnect DEVGUIDE maintains an updated list of CardDAV libraries with additional entries.
Know of any other good vCard parsers? Let me know so I can list them.
XML
CardDAV servers also use XML for various things:
- Getting a list of all vCards
- Getting information about an addressbook
- Finding out if vCards or addressbooks have changed.
Retain full vCards!
In most cases, when integrating with foreign API's, you will figure out theremote data model, and write code to map that to the data model in yourapplication. This tends to be some mapping code that is bidirectional andsimply converts one data model (such as json or xml) to something local (suchas an mvc model, database record or object property).
When integrating with CardDAV, it is not quite as simple.
The problem with simply mapping the vcard to your local data model, is thatthere is an potentially a lot of information to map. vCards can contain allsorts of information, and even allow applications to define new properties,and parameters on top of existing properties.
vCards can contain lot of different information, information about information, and application-specific information and you must support all of this.
If your data model is simpler than the vCard data model, this inherently meansthat data can get lost during conversion. E.g.: mapping back and forward, andreversing this again tends to be a 'lossy' process.
To illustrate, lets look at the protocol from a very high level. Simplisticlywe will be doing a GET
request (or equivalent) and later on a PUT
requestto update a vcard.
You must absolutely make sure that none of the information you received in aGET
is lost when you perform the PUT
.
Almost every client on the planet will even embed custom non-standard datain vCards. If you discard this data when performing PUT
, you are destroyingyour users data.
So a common trick that implementors use AND WE DON'T RECOMMEND is
- Go through all the properties of a vCard
- Map the properties you support to a local data model
- Store all the properties that are not supported by the local data model ina separate place.
Then when the vCard is uploaded again with PUT
, the 'unknown' propertiesare stitched back in.
We consider this to be a bad idea, because it ignores several vCard features:
- Parameters you may or may not support
- Property groups
- And a little bit less important: the order in which items appear can berelevant to the user.
Our recommendation
- Download the vCard
- Retain the entire vCard and store it locally, or at least in somelossless way
- Parse the vCard and populate your models with the information that isrelevant to you.
- Keep a reference to which vCard property maps to what information in themodel.
Now when something changes in a model (e.g.: a user changes an email addressin your UI.)
- Model receives change (email address updated)
- Find the property in the vCard that originally mapped to the information inthe model.
- Update the value in the vCard.
- Upload the vCard.
In an ideal world, your vCard is your model though.
Regardless of how this issue is solved (there may be better suggestions, wewould love to hear it), not ensuring that original vCard is kept as closeto the original as possible is guaranteed to trigger bugs and edge-cases forall sorts of CardDAV clients.
Typical urls
Note that the following url structure is typical for SabreDAV, but may bedifferent for other servers. All these urls should be discovered by a client,but listing these here helps with illustrating the examples that follow:
url | description |
---|---|
http://dav.example.org/ | Root |
http://dav.example.org/principals/johndoe/ | A principal url |
http://dav.example.org/addressbooks/johndoe/ | The addressbook home |
http://dav.example.org/addressbooks/johndoe/contacts/ | An addressbook |
http://dav.example.org/addressbooks/johndoe/contacts/foobarapp-2357-aeaat34.vcf | A vcard |
Authentication
Servers typically use HTTP Digest or HTTP Basic authentication. Your clientshould already support these. The Google CardDAV API uses OAuth2.
Operations
Retrieving addressbook information
To receive information about a URL, we use the PROPFIND
method.In this case we're going to ask for the addressbooks display name and aso-called 'ctag'.
PROPFIND /addressbooks/johndoe/contacts/ HTTP/1.1Depth: 0Content-Type: application/xml; charset=utf-8<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/"> <d:prop> <d:displayname /> <cs:getctag /> </d:prop></d:propfind>
The PROPFIND
request is a HTTP request, defined by WebDAV.PROPFIND
allows the client to fetch properties from an url.
CardDAV uses many properties like this, but in this case we just fetch the'displayname', which is the human-readable name the user gave the addressbook, andthe ctag. The ctag must be stored for subsequent requests.
The request will return something like:
HTTP/1.1 207 Multi-statusContent-Type: application/xml; charset=utf-8<d:multistatus xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/"> <d:response> <d:href>/addressbooks/johndoe/contacts/</d:href> <d:propstat> <d:prop> <d:displayname>My Address Book</d:displayname> <cs:getctag>3145</cs:getctag> </d:prop> <d:status>HTTP/1.1 200 OK</d:status> </d:propstat> </d:response></d:multistatus>
This multistatus response is very common for Cal and WebDAV. Many requestsreturn an xml document in this exact format, so it is worthwhile writing astandard parser.
The response gives us back the user, the values for the 2 properties and thestatus.
It is possible that a server does not support the ctag. In that case it willlikely return 404 Not Found
for the ctag, and 200 OK
for the displayname.
Example:
HTTP/1.1 207 Multi-statusContent-Type: application/xml; charset=utf-8<d:multistatus xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/"> <d:response> <d:href>/addressbooks/johndoe/contacts/</d:href> <d:propstat> <d:prop> <d:displayname>My Address Book</d:displayname> </d:prop> <d:status>HTTP/1.1 200 OK</d:status> </d:propstat> <d:propstat> <d:prop> <cs:getctag /> </d:prop> <d:status>HTTP/1.1 404 Not Found</d:status> </d:propstat> </d:response></d:multistatus>
So take note from this last response. Here we display that the status, suchas the 404
and the 200
are not related to the existence of the url(/addressbooks/johndoe/contacts
). The status codes are re-used to returninformation about the individual properties.
Downloading objects
Now we download every single object in this addressbook. To do this, we use aREPORT
method.
REPORT /addressbooks/johndoe/contacts/ HTTP/1.1Depth: 1Content-Type: application/xml; charset=utf-8<card:addressbook-query xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav"> <d:prop> <d:getetag /> <card:address-data /> </d:prop></card:addressbook-query>
This request will return a large xml object with all the vCards, and theiretags.
This report will return a multi-status object again:
HTTP/1.1 207 Multi-statusContent-Type: application/xml; charset=utf-8<d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav"> <d:response> <d:href>/addressbooks/johndoe/contacts/abc-def-fez-123454657.vcf</d:href> <d:propstat> <d:prop> <d:getetag>"2134-314"</d:getetag> <card:address-data>BEGIN:VCARD VERSION:3.0 FN:My Mother UID:abc-def-fez-1234546578 END:VCARD </card:address-data> </d:prop> <d:status>HTTP/1.1 200 OK</d:status> </d:propstat> </d:response> <d:response> <d:href>/addressbooks/johndoe/contacts/someapplication-12345678.vcf</d:href> <d:propstat> <d:prop> <d:getetag>"5467-323"</d:getetag> <card:address-data>BEGIN:VCARD VERSION:3.0 FN:Your Mother UID:foo-bar-zim-gir-1234567 END:VCARD </card:address-data> </d:prop> <d:status>HTTP/1.1 200 OK</d:status> </d:propstat> </d:response></d:multistatus>
This addressbook only contained 2 contacts.
So after you retrieved and processed these, for each object you must retain:
- The vCard data itself
- The url
- The etag
In this case all urls ended with .vcf
. This is often the case, but you mustnot rely on this. In this case the UID in the vCards was also identical toa part of the url. This too is often the case, but again not something you canrely on, so don't make any assumptions.
The url and the UID have no meaningful relationship, so treat both those itemsas separate unique identifiers.
Finding out if anything changed
To see if anything in an addressbook changed, we simply request the ctag againon the addressbook. If the ctag did not change, you still have the latest copy.
This is the purpose of the ctag. Every time anything in the address bookchanges, the ctag must also change.
If it did change, you should request all the etags in the entire addressbookagain:
REPORT /addressbooks/johndoe/contacts/ HTTP/1.1Depth: 1Content-Type: application/xml; charset=utf-8<card:addressbook-query xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav"> <d:prop> <d:getetag /> </d:prop></card:addressbook-query>
Note that this last request is extremely similar to a previous one, but we areonly asking for the etag, not the address-data.
The reason for this, is that addressbooks can be rather huge. It will save a TONof bandwidth to only check the etag first.
HTTP/1.1 207 Multi-statusContent-Type: application/xml; charset=utf-8<d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav"> <d:response> <d:href>/addressbooks/johndoe/contacts/abc-def-fez-123454657.vcf</d:href> <d:propstat> <d:prop> <d:getetag>"2134-888"</d:getetag> </d:prop> <d:status>HTTP/1.1 200 OK</d:status> </d:propstat> </d:response> <d:response> <d:href>/addressbooks/johndoe/contacts/acme-12345.vcf</d:href> <d:propstat> <d:prop> <d:getetag>"9999-2344""</d:getetag> </d:prop> <d:status>HTTP/1.1 200 OK</d:status> </d:propstat> </d:response></d:multistatus>
Judging from this last request, 3 things have changed:
- The etag for the first contact has changed, so it must have been updated.
- There's a new url, some other client must have created a new contact.
- The second contact we saw earlier is no longer in the list, so it must havebeen deleted.
So based on those 3 points, we know that we need to remove a contact from thelocal addressbook, and fetch the vCards for both the new item, and the updatedone.
To fetch the data for these, you can simply issue GET requests:
GET /addressbooks/johndoe/contacts/abc-def-fez-123454657.vcf
But that does not scale up well, in case a few hundred contacts have changed.It's better to batch the GET's together with multiget
.
REPORT /addressbooks/johndoe/contacts/ HTTP/1.1Depth: 1Content-Type: application/xml; charset=utf-8<card:addressbook-multiget xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav"> <d:prop> <d:getetag /> <card:address-data /> </d:prop> <d:href>/addressbooks/johndoe/contacts/abc-def-fez-123454657.vcf</d:href> <d:href>/addressbooks/johndoe/contacts/acme-12345.vcf</d:href></card:addressbook-multiget>
This request will simply return a multi-status again with the address-data andetag.
A small note about writing code for this.
If you read this far and understood what's been said, you may have realized thatit's a bit cumbersome to have a separate step for the initial sync, andsubsequent updates.
It would totally be possible to skip the 'initial sync', and just useaddressbook-query and addressbook-multiget REPORTS for the initial sync as well.
Updating a vCard is rather simple:
PUT /addressbook/johndoe/contacts/some-contact.vcf HTTP/1.1Content-Type: text/vcard; charset=utf-8If-Match: "2134-314"BEGIN:VCARD....END:VCARD
A response to this will be something like this:
HTTP/1.1 204 No ContentETag: "2134-315"
The update gave us back the new ETag. SabreDAV returns this ETag on updatesmost of the time, but not always.
There are cases where the CardDAV server must modify the vCard immediately after receiving it, for various reasons. In those situations an ETag willnot be returned, and you should ideally issue a GET request immediatelyafter to figure out how the server changed the contact.
Many clients skip the GET
step though.
A few notes:
Don't change the UID
The UID
and the url of the object are important to not change. Changingeither will highly confuse other clients and the server should reject thosechanges (although they don't always).
Creating a contact
Creating a contact is almost identical, except that you (as a client) areresponsible for determining the url of the object, and UID.
PUT /addressbooks/johndoe/contacts/somerandomstring.vcf HTTP/1.1Content-Type: text/vcard; charset=utf-8BEGIN:VCARDVERSION:3.0UID:some-other-random-string....END:VCARD
A response to this will be something like this:
HTTP/1.1 201 CreatedETag: "21345-324"
Similar to updating, an ETag is often returned, but there are cases where thisis not true.
Deleting is simple enough:
DELETE /addressbooks/johndoe/contacts/132456762153245.vcf HTTP/1.1If-Match: "2134-314"
Speeding up Sync with WebDAV-Sync
WebDAV-Sync is a protocol extension that is defined in rfc6578.Because this extension was defined later, some servers may not support thisyet.
SabreDAV supports this since 2.0.
WebDAV-Sync allows a client to ask just for address books that have changed.The process on a high-level is as follows:
- Client requests sync-token from server.
- Server reports token
15
. - Some time passes.
- Client does a Sync REPORT on an addressbook, and supplied token
15
. - Server returns vcard urls that have changed or have been deleted and returns token
17
.
As you can see, after the initial sync, only items that have been created,modified or deleted will ever be sent.
This has a lot of advantages. The transmitted xml bodies can generally be alot shorter, and is also easier on both client and server in terms of memoryand CPU usage, because only a limited set of items will have to be compared.
It's important to note, that a client should only do Sync operations, if theserver reports that it has support for it. The quickest way to do so, is torequest {DAV}sync-token
on the addressbook you wish to sync.
Technically, a server may support 'sync' on one addressbook, and it may notsupport it on another, although this is probably rare.
Getting the first sync-token
Initially, we just request a sync token when asking for address bookinformation:
PROPFIND /addressbooks/johndoe/contacts/ HTTP/1.1Depth: 0Content-Type: application/xml; charset=utf-8<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/"> <d:prop> <d:displayname /> <cs:getctag /> <d:sync-token /> </d:prop></d:propfind>
This would return something as follows:
<d:multistatus xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/"> <d:response> <d:href>/addressbooks/johndoe/contacts/</d:href> <d:propstat> <d:prop> <d:displayname>My Address Book</d:displayname> <cs:getctag>3145</cs:getctag> <d:sync-token>http://sabredav.org/ns/sync-token/3145</d:sync-token> </d:prop> <d:status>HTTP/1.1 200 OK</d:status> </d:propstat> </d:response></d:multistatus>
As you can see, the sync-token is a url. It always should be a url.Even though a number appears in the url, you are not allowed to attach anymeaning to that url. Some servers may have use an increasing number,another server may use a completely random string.
Receiving changes
After a sync token has been obtained, and the client already has the initialcopy of the addressbook, the client is able to request all changes since thetoken was issued.
This is done with a REPORT
request that may look like this:
REPORT /addressbooks/johndoe/contacts/ HTTP/1.1Host: dav.example.orgContent-Type: application/xml; charset="utf-8"<?xml version="1.0" encoding="utf-8" ?><d:sync-collection xmlns:d="DAV:"> <d:sync-token>http://sabredav.org/ns/sync/3145</d:sync-token> <d:sync-level>1</d:sync-level> <d:prop> <d:getetag/> </d:prop></d:sync-collection>
This requests all the changes since sync-token identified byhttp://sabredav.org/ns/sync/3145
, and for the contacts that have been addedor modified, we're requesting the etag.
The response to a query like this is another multistatus xml body. Example:
HTTP/1.1 207 Multi-StatusContent-Type: application/xml; charset="utf-8"<?xml version="1.0" encoding="utf-8" ?><d:multistatus xmlns:d="DAV:"> <d:response> <d:href>/addressbooks/johndoe/contacts/newcard.vcf</d:href> <d:propstat> <d:prop> <d:getetag>"33441-34321"</d:getetag> </d:prop> <d:status>HTTP/1.1 200 OK</d:status> </d:propstat> </d:response> <d:response> <d:href>/addressbooks/johndoe/contacts/updatedcard.vcf</d:href> <d:propstat> <d:prop> <d:getetag>"33541-34696"</d:getetag> </d:prop> <d:status>HTTP/1.1 200 OK</d:status> </d:propstat> </d:response> <d:response> <d:href>/addressbooks/johndoe/contacts/deletedcard.vcf</d:href> <d:status>HTTP/1.1 404 Not Found</d:status> </d:response> <d:sync-token>http://sabredav.org/ns/sync/5001</d:sync-token> </d:multistatus>
The last response reported two changes: newcard.vcf
and updatedcard.vcf
.There's no way to tell from the response whether those cards got created orupdated, you, as a client can only infer this based on the vcards you arealready aware of.
The entry with name deletedvcard.vcf
got deleted as indicated by the 404
status. Note that the status element is here a child of d:response
when inall previous examples it has been a child of d:propstat
.
The other difference with the other multi-status examples, is that this onehas a sync-token
element with the latest sync-token.
Caveats
Note that a server is free to 'forget' any sync-tokens that have beenpreviously issued. In this case it may be needed to do a full-sync again.
In case the supplied sync-token is not recognized by the server, a HTTP erroris emitted. SabreDAV emits a 403
.
Discovery
Ideally you will want to make sure that all the addressbooks in an account areautomatically discovered. The best user interface would be to just have toask for three items:
- Username
- Password
- Server
And the server should be as short as possible. This is possible with mostservers.
If, for example a user specified 'dav.example.org' for the server, the firstthing you should do is attempt to send a PROPFIND
request tohttps://dav.example.org/
. Note that you should try the https url before thehttp url.
This PROPFIND
request looks as follows:
PROPFIND / HTTP/1.1Depth: 0Content-Type: application/xml; charset=utf-8<d:propfind xmlns:d="DAV:"> <d:prop> <d:current-user-principal /> </d:prop></d:propfind>
This will return a response such as the following:
HTTP/1.1 207 Multi-statusContent-Type: application/xml; charset=utf-8<d:multistatus xmlns:d="DAV:"> <d:response> <d:href>/</d:href> <d:propstat> <d:prop> <d:current-user-principal> <d:href>/principals/users/johndoe/</d:href> </d:current-user-principal> </d:prop> <d:status>HTTP/1.1 200 OK</d:status> </d:propstat> </d:response></d:multistatus>
A 'principal' is a user. The url that's being returned, is a url that refersto the current user. On this url you can request additional information aboutthe user.
What we need from this url, is their 'addressbook home'. The addressbook homeis a collection that contains all of the users' addressbooks.
To request that, issue the following request:
PROPFIND /principals/users/johndoe/ HTTP/1.1Depth: 0Content-Type: application/xml; charset=utf-8<d:propfind xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav"> <d:prop> <card:addressbook-home-set /> </d:prop></d:propfind>
This will return a response such as the following:
HTTP/1.1 207 Multi-statusContent-Type: application/xml; charset=utf-8<d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav"> <d:response> <d:href>/</d:href> <d:propstat> <d:prop> <c:addressbook-home-set> <d:href>/addressbooks/johndoe/</d:href> </c:addressbook-home-set> </d:prop> <d:status>HTTP/1.1 200 OK</d:status> </d:propstat> </d:response></d:multistatus>
Lastly, to list all the addressbooks for the user, issue a PROPFIND requestwith Depth: 1
.
PROPFIND /addressbooks/johndoe/ HTTP/1.1Depth: 1Content-Type: application/xml; charset=utf-8<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/"> <d:prop> <d:resourcetype /> <d:displayname /> <cs:getctag /> </d:prop></d:propfind>
In that last request, we asked for 3 properties.
The resourcetype
tells us what type of object we're getting back. You mustread out the resourcetype
and ensure that it contains at least anaddressbook
element in the CardDAV namespace. Other items may be returned,including non-addressbooks, which your application should ignore.
Advanced discovery topics
Read the Service Discovery documentation