To determine if a calendar is writable, libkolab issues a PROPFIND for various properties and processes them in kolab_dav_client.php getFolderPropertiesFromResponse()
In that code, it makes the assumption that tag names will be prefixed. But Radicale doesn't write privileges like <D:privilege><D:write/></D:privilege>. Instead it declares its top level element like <multistatus xmlns="DAV:"> and all the child tags inherit that default namespace. <privilege><write/></privilege> is thus valid, but libkolab fails to accept it.
Example:
<multistatus xmlns="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/" xmlns:ns4="Kolab:">
<response>
<href>/calendar/.../.../</href>
<propstat>
<prop>
<ICAL:calendar-color>#ff0000ff</ICAL:calendar-color>
<C:supported-calendar-component-set>
<C:comp name="VEVENT"/>
<C:comp name="VJOURNAL"/>
<C:comp name="VTODO"/>
</C:supported-calendar-component-set>
<CS:getctag>"..."</CS:getctag>
<current-user-privilege-set>
<privilege>
<read/>
</privilege>
<privilege>
<all/>
</privilege>
<privilege>
<write/>
</privilege>
...This is a fix for the issue:
--- plugins/libkolab/lib/kolab_dav_client.php.orig 2025-10-14 12:34:56.0 +0000 +++ plugins/libkolab/lib/kolab_dav_client.php 2025-10-14 12:34:56.0 +0000 @@ -1046,16 +1046,16 @@ if ($grant_element = $ace->getElementsByTagName('grant')->item(0)) { foreach ($grant_element->childNodes as $privilege) { - if (strpos($privilege->nodeName, ':privilege') !== false && $privilege->firstChild) { - $grant[] = preg_replace('/^[^:]+:/', '', $privilege->firstChild->nodeName); + if ($privilege->localName == 'privilege' && $privilege->firstChild) { + $grant[] = $privilege->firstChild->localName; } } } if ($deny_element = $ace->getElementsByTagName('deny')->item(0)) { foreach ($deny_element->childNodes as $privilege) { - if (strpos($privilege->nodeName, ':privilege') !== false && $privilege->firstChild) { - $deny[] = preg_replace('/^[^:]+:/', '', $privilege->firstChild->nodeName); + if ($privilege->localName == 'privilege' && $privilege->firstChild) { + $deny[] = $privilege->firstChild->localName; } } } @@ -1075,8 +1075,8 @@ $rights = []; foreach ($set_element->childNodes as $privilege) { - if (strpos($privilege->nodeName, ':privilege') !== false && $privilege->firstChild) { - $rights[] = preg_replace('/^[^:]+:/', '', $privilege->firstChild->nodeName); + if ($privilege->localName == 'privilege' && $privilege->firstChild) { + $rights[] = $privilege->firstChild->localName; } }