Skip to content

CalDAV and CardDAV

Ed Mozley edited this page Sep 16, 2026 · 8 revisions

CalDAV and CardDAV β€” syncing calendars and contacts with an open standard

Status: ALL SEVEN STEPS ARE BUILT. CardDAV contact sync works in both directions (the import shipped in 1.8.0, the write-back in 1.9.0), and CalDAV calendar sync is built too: scheduled tickets and tasks go into a calendar on any CalDAV server, two-way, signed in as each analyst. See Scheduled work in your own calendar Β§3b for CalDAV, CardDAV contact sync for contacts, and the developer guides linked from each. This page is kept as the analysis that led there.

Raised by mbsouth in #133, who runs SabreDAV for both calendars and contacts.

⭐ The estimate in Β§5 held up. It called this "a new fetch-and-map layer under an existing policy layer β€” genuinely smaller than starting from nothing, and genuinely larger than adding a flavour", and that is exactly what it turned out to be: about 1,000 lines across two new files, with nine policy-layer functions reused unchanged and one touched additively. Β§5 also predicted the test fixture would be needed "or it ships untested against a real server" β€” docker/carddav-test/ exists because of that sentence.


What is built, and what is not

The work was planned as seven steps. Five shipped in 1.8.0, the write-back followed in 1.9.0, and CalDAV came last.

Step State
1 Show the person fields on the Edit user dialog (no CardDAV involved) βœ… Done β€” all seven, on both people screens. Contact details
2 Let a portal user edit their own (no CardDAV involved) βœ… Done β€” job title, office, phone, mobile
3 Connect to a CardDAV server βœ… Done β€” includes/carddav.php, plus a connection test that enumerates the server's address books
4 Import contacts from one group βœ… Done, and wider than planned β€” the server enumerates its own groups and categories, and you tick as many as you like
5 Write changes back (the hard one) βœ… Done β€” surgical vCard editing, a per-field three-way merge, and a permission probe. CardDAV write-back internals
6 A test server to develop against βœ… Done β€” docker/carddav-test/, BaΓ―kal 0.12.1
7 CalDAV βœ… Done - a second provider under the existing calendar sync, two-way, signed in as each analyst. Calendar sync Β§3b, developer guide Β§3

πŸ”‘ The import is still provably read-only, and that claim survived step 5. PUT lives in a separate file, includes/carddav_write.php, that the import path never loads β€” so "the import issues only PROPFIND and REPORT" remains a fact about the code rather than a promise. Write-back is off by default and gated on its own column.

What step 5 was expected to take β€” and what it actually took

The predictions below were written before it was built. Two held, one was answered differently, and one turned out to be the wrong worry.

  • βœ… The conflict rules were the real cost, exactly as Β§10 said. The answer is a per-field three-way merge: re-read the card, compare the server's current value against what FreeITSM held before the save, and refuse only on a same-field difference.
  • βœ… The ETag groundwork paid off. It was already being read and discarded; it is now stored per person and sent as If-Match.
  • πŸ”„ "Precedence" turned out to be the wrong frame. There is no per-field timestamp and there did not need to be β€” the question is not whose value is newer but did anyone else touch this same field since we last looked, which the three-way merge answers without a clock.
  • πŸ”„ The scope question was settled by the vCard, not by policy. The writable set is the five details a card can actually hold; employee ID and manager have nowhere to go. Office is in, resolving the open question below.
  • ⚠️ The failure mode worry was right and shaped the design. The answer to "silently overwriting a corrected number is hard to notice and harder to undo" is that FreeITSM never generates a card β€” it edits the one line that changed and leaves every other byte alone.

The original analysis, kept for the record

Not a CardDAV client β€” there is one, and it works. What is missing is the part that was always the real cost:

  • Precedence. When a phone number differs on both sides, which wins? "Last write" needs a per-field timestamp neither side reliably has.
  • What a person owns about themselves. Β§8 phase 3 named phone, mobile and job title β€” deliberately not department, employee ID or manager, because those are the service desk's to state rather than the person's. ⚠️ Note that the local answer shipped as four fields, not three: USER_SELF_EDITABLE_FIELDS is job_title, office, phone, mobile. Whoever builds step 5 should decide whether office joins the write-back list too β€” the argument that a person knows their own site better than the service desk does is the same argument that put it in the portal.
  • The failure mode is worse than not having it. Silently overwriting a corrected number in somebody's address book is harder to notice, and harder to undo, than never having pushed it.
  • A PUT needs an ETag to avoid clobbering a card somebody edited meanwhile. The transport already reads ETags on fetch and simply does not use them yet, which is the one piece of groundwork that is done.

βœ… This was the half mbsouth actually argued for, and his Article 16 case is now answered as a setting. Write-back first sent only an analyst's edit. His scenario is a customer correcting their own details, and that is now possible from the portal too, off by default because it lets customers write into the operator's address book: System β†’ Portal profile β†’ Let them change these too, and send their change to the address book. The customer's change goes to the card first and is saved in FreeITSM only once the address book accepts it. See Contact details.


0. What the reporter answered, and three things it changed

He replied on 8 September, and it settles Β§6 as well as correcting something in Β§4.

  • The fork is answered. One-way "would be much better than none and a very good start" β€” but the focus should also be on two-way. His reason is the sharp one: the change surfaces during the phone call, and having to fix it in a second application is exactly what an ITSM tool should remove. Β§10 says an answer like that is what would revive this.
  • πŸ”΄ He disagrees with "nobody is ever deleted" (Β§4 rule 1), and wants a person removed once their last relationship goes. FreeITSM already agrees with him more than Β§4 implies: api/tickets/delete_user.php is FK-safe rather than absolute β€” a person with no tickets and no users_assets rows deletes outright, and the refusal only fires when something still points at them, naming it. What does not exist, and should not, is a sync deciding that by itself: a source that stops returning somebody is not evidence they should be removed.
  • ⭐ A legal-basis argument that was nowhere in this page. If the customer database is the record of who you have a lawful basis to process, then "is this caller in the address book" stops being a lookup and becomes a different question with different consequences for the analyst. That makes the sync more than a copy of some fields, and it is the most interesting idea in the thread.

⚠️ And a correction to Β§4 in his favour. He assumed the LDAP/AD import applies only to employees and analysts. It does not: includes/directory_sync.php writes to users, the requester table, and mentions analysts in exactly three places, all "who triggered this run". So the policy layer already governs precisely the records he cares about. The distance to a CardDAV source is shorter than Β§4 makes it sound β€” the policy is there and it is the transport that is missing.


1. Two requests wearing one paragraph

They arrived together and they are not the same size at all, so this page treats them separately.

What it is Size Case for it
CalDAV A read/write calendar protocol Moderate, self-contained Good, but partly already served
CardDAV A contacts protocol β€” really, a request to stop keeping contacts in two places Larger, and touches the person record Stronger

The second is the interesting one. The first is closer to being answered already than it looks.


2. CalDAV β€” the calendar half

FreeITSM already publishes an iCalendar subscription feed (Ticket calendar sync), which is the read-only half of what CalDAV does. Any client that speaks CalDAV also speaks iCalendar subscriptions, so somebody running SabreDAV can already get FreeITSM's scheduled work into their calendar today.

What CalDAV would add on top:

  • Writing back. Move an appointment in your calendar and have the ticket's scheduled work move with it. That is the real prize, and it is also the whole difficulty.
  • Faster updates. A subscription is polled on the client's own schedule β€” often hours. CalDAV can be pulled on demand.

The honest counter is that FreeITSM already has a two-way route, the Microsoft Graph push, and a second two-way route is a second set of conflict rules, a second set of "who wins when both sides changed", and a second thing to keep correct forever. The value of CalDAV is not that it does something new; it is that it does the same thing without requiring one particular vendor. That is a real argument β€” it is just not an argument about capability.

The reporter's fair observation. He noted that "Microsoft solutions seem to be preferred". That is about the order things were built in rather than a preference: the Graph route was built first because it was what was being asked for, and the iCalendar feed exists precisely so that it is not the only option. The principle on record is that "your calendar system is not implemented" must never mean "you get nothing".


3. CardDAV β€” the contacts half, and the better idea

Strip out the protocol and the request is this:

The people in FreeITSM are an island. Every customer, partner and colleague already exists in one address book. Setting FreeITSM up meant typing a large number of them in a second time, and from that moment the two lists start drifting apart.

He also asked for something more specific and more sensible than "sync everything": scope it to one group in the address book β€” an itsm group β€” so FreeITSM receives the contacts that are relevant to it and not a personal address book of several thousand entries.

That scoping instinct is right, and it matches how FreeITSM already treats directories, which restrict what they import by search scope rather than swallowing the whole tree.

The GDPR argument, decoded

He cites Article 16 (the right to have your details corrected), Recitals 64–66 and Article 19 (telling everyone you passed the details on to).

The argument is practical rather than legal. If a customer can correct their own phone number in the self-service portal, and that correction flows back to the address book, then the customer does the correcting instead of the service desk. That is what "we can leave that up to our customers" means β€” it converts a manual obligation into a self-service one.

It is a good argument, and it is worth noticing what it actually requires: the write-back direction, which is the harder one. A read-only import of contacts does not help with Article 16 at all.


4. What already exists β€” and this is more than it sounds

FreeITSM has already built most of the shape of a contact sync, for LDAP and Active Directory. The person record in users already carries provenance:

Column What it records
auth_provider_id Which external source this person came from
directory_username Their identity in that source
is_managed This record is owned by the source, not by hand
last_seen_in_source When the source last confirmed they exist
sync_missed_count How many consecutive runs have not seen them

And includes/directory_sync.php enforces three rules that were expensive to learn and would be identical for any other source:

  1. Nobody is ever deleted. Assets, tickets and handover documents all hang off users.id. Someone who has left is deactivated and keeps their history.
  2. A run that looks wrong changes nothing. If a source suddenly returns far fewer people than last time, that is far more likely to be a misconfiguration or a slow replica than a redundancy round. A sanity brake stops the run rather than deactivating a company.
  3. Missing once is noise. Nobody is deactivated on a single absence β€” sync_missed_count has to reach a threshold, and any sighting resets it.

There is also a preview mode that runs the identical code path and writes nothing, so what an administrator is shown is what would happen rather than a separate estimate that can drift from it.

None of that would need rebuilding. A CardDAV source would reuse the entire policy layer. See Directory sync and Importing people from a directory.


5. The catch β€” CardDAV is not "a third flavour"

It is tempting to read the above and conclude this is a small job, so this section exists to stop that.

Extending directory sync Β§2 documents how to add another kind of directory, and every one of its extension points assumes LDAP is the transport:

  • the configuration columns are named auth_providers.ldap_attr_*;
  • dsyncFetchPeople() pages results using LDAP_CONTROL_PAGEDRESULTS;
  • identity handling is objectGUID versus entryUUID, both LDAP concepts;
  • manager is assumed to hold a distinguished name.

CardDAV is HTTP and vCard. It has no DNs, no paged LDAP controls, and its identity is a URL plus an ETag. So the work is a new fetch-and-map layer under an existing policy layer β€” genuinely smaller than starting from nothing, and genuinely larger than adding a flavour. Anyone estimating this should read that section first and price the transport honestly.

There is a second, quieter catch: the test fixture. docker/ldap-test/ runs Samba AD and OpenLDAP side by side precisely so a new flavour cannot be added blind. A CardDAV source would need its own equivalent, or it ships untested against a real server.


6. The fork that decides the size

Everything above collapses into one question, and it is the reporter's to answer:

What it does Cost Serves the GDPR argument?
One-way (address book β†’ FreeITSM) Stops contacts being typed twice, and keeps them current Much smaller. Reuses the whole policy layer No
Two-way (also FreeITSM β†’ address book) The above, plus customers maintaining their own details Much larger β€” conflict rules, precedence, what happens when both sides change Yes

One-way probably delivers most of the day-to-day value. Two-way is what he actually argued for.


7. The prerequisite nobody mentioned

Before any of this, there is a gap inside FreeITSM itself.

The self-service portal does not let people edit their own contact details. A signed-in requester can change their password, their theme, and their preferred name β€” and that is all. Not phone, not mobile, not job title, not department, not office.

So the self-service rectification the whole GDPR argument rests on does not exist yet locally, before any question of syncing it anywhere.

That is worth separating out, because it is:

  • small, compared with everything else on this page;
  • useful to every installation, whether or not CardDAV is ever built;
  • a strict prerequisite for the two-way case.

If any part of this page is ever acted on, that is the piece to do first, and it can be done on its own merits.

βœ… It was, in 1.8.0. A portal user can now maintain their own job title, office, phone and mobile. Deliberately not department, employee ID or manager β€” the reasons differ per field and are set out in Contact details; the short version is that nobody should choose their own approver. So the rectification the GDPR argument rests on now exists locally, and if the two-way half is ever built it has somewhere to write to and a defensible list of what a person owns about themselves.

πŸ”‘ The useful discovery was that this prerequisite was even smaller and even more missing than the page thought. The seven columns had existed since directory sync slice 1, api/tickets/save_user.php had accepted and guarded them the whole time, and Assets β†’ Users had edited them from the start β€” but Tickets β†’ Users, the screen an analyst actually lands on when looking up a requester, had none of them and its endpoint did not return them. A blue-sky page about a protocol turned up a plain inconsistency two screens deep.


8. A phase order, if it were ever built

  1. βœ… Let a portal user maintain their own contact details. BUILT β€” 1.8.0. Independently useful. Prerequisite for everything below. ⚠️ It turned out to be two steps, not one: the fields were missing from Tickets β†’ Users as well, and api/tickets/get_users.php did not even return the columns β€” so FreeITSM had two people editors of different depth before it had a portal one at all. Both now show the same seven; the portal offers the four a person owns about themselves. See Contact details.
  2. βœ… Read-only CardDAV import, reusing the directory-sync policy layer. BUILT β€” 1.8.0. Delivers "stop typing contacts twice". ⚠️ One thing changed on the way: the plan said "scoped to one group", which is what was asked for. It ships with the server enumerating its own groups and categories and the operator ticking whichever they want β€” cardDavScanBook() has to read every card to count them anyway, so offering a list cost nothing over accepting a typed name, and it removed the need to explain which of the two conventions their address book uses. See CardDAV contact sync.
  3. βœ… Write-back of the fields a person owns about themselves β€” phone, mobile, job title. Deliberately not everything: the fields the service desk owns should not be overwritten from outside. BUILT β€” 1.9.0.
  4. βœ… CalDAV, as a separate piece, on its own argument. BUILT. It turned out to be a second provider under the calendar sync that already existed for Microsoft, not a new sync: the "which ticket goes in whose calendar" logic, the three pull guards and the event map were all reused unchanged. The genuinely new part was that FreeITSM has to sign in as each analyst, because a CalDAV server has nothing like Microsoft's tenant-wide app permission.

Phase 3 is where the conflict rules live, and it should not be started until 1 and 2 have been in use long enough to know what actually drifts.


9. Why the rest is still parked

  • Uncertain demand. One request, from one installation, from somebody already running a DAV server. That is a real need and not yet evidence of a common one. CalDAV and CardDAV are the sort of thing that either matters enormously to a site or not at all.
  • Complexity versus reward for the two-way half specifically. A second bidirectional sync route is a permanent maintenance commitment, and the failure modes of a contact sync that gets precedence wrong are worse than not having one: silently overwriting a corrected phone number is harder to notice, and harder to undo, than never importing it.
  • The cheap half was not blocked by any of this, and was done first β€” which is the point rather than a caveat. Phase 1 needed no protocol work at all, and splitting it off got the reporter something real while the protocol question stayed open. βœ… Shipped in 1.8.0, and phase 2 followed the same day once it was clear how little the transport actually needed.
  • ⚠️ The demand argument did not change, and the work happened anyway. It is still one site asking. What moved was the cost: reading the policy layer properly showed it had no LDAP in it, so the one-way import was a fetch-and-map layer rather than a second sync engine. Worth remembering as a pattern β€” "uncertain demand" and "expensive" were doing the parking jointly, and only one of them turned out to be true.

Being here is not a rejection. The analysis is on record so it does not have to be re-derived. βœ… Nothing from this request is parked any more. Both directions of the contact sync are built, and so is CalDAV.

⚠️ And the demand argument was overtaken twice, in the same way both times. It was never demand that moved this work β€” it was discovering the cost was lower than the parking assumed. First the policy layer turned out to have no LDAP in it, so the import was a fetch-and-map layer rather than a second sync engine. Then write-back turned out not to need precedence rules or timestamps at all, because "did anyone else touch this field" is a cheaper question than "whose value is newer". Worth keeping as a pattern: when something is parked on two grounds, check whether only one of them is real.


10. What would revive the write-back

⚠️ Kept as the record of what was expected to move it, since it was built before most of this happened. The predictions are worth reading against the outcome rather than deleting.

  • βœ… Phase 2 shipping, which is now done β€” and it changed the write-back question rather than answering it. Correct, and the remaining cost was indeed the conflict rules.
  • πŸ”„ Feedback from the one site now running it β€” called "the thing most likely to move it". It is not what moved it: the write-back was built before that feedback arrived. Which fields actually drift is still unknown, and is still worth asking.
  • ❌ A second and third installation asking. Still outstanding: it remains one site, and that never changed.
  • βœ… A concrete answer to Β§6 from somebody who would actually use it. Given β€” see Β§0. He would take one-way as "a very good start" but wanted two-way as the target.
  • Interest in CalDAV specifically from anyone who cannot use the Microsoft Graph route, which is the argument that route cannot answer. βœ… Built - and again not because demand grew: the calendar sync had been written with a provider layer from the start, so CalDAV was a provider rather than a project.

See also

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally