Fix XML namespace handling for Radicale responses

Radicale uses default DAV: namespace (no d: prefix) and CR: for CardDAV
instead of c:. Fixed all regex patterns to handle both variants:
- <href> and <d:href>
- <displayname> and <d:displayname>
- <CR:address-data> and <c:address-data>
- <C:calendar-data> and <c:calendar-data>
- </response> and </d:response>

Also fixed calendar discovery to match <C:calendar/> resourcetype
instead of looking for VEVENT string in the response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Stefan Lohmaier
2026-06-12 10:17:14 +02:00
parent 45cd6935fb
commit 80fc323374
3 changed files with 21 additions and 12 deletions
+4 -4
View File
@@ -29,12 +29,12 @@ def _discover_ab(user):
for base in AB_PATHS.get(user, []):
body = '<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/><d:displayname/></d:prop></d:propfind>'
r = httpx.request("PROPFIND", RADICALE + base, content=body, auth=_auth(user), headers={"Depth": "1", "Content-Type": "application/xml"}, timeout=30)
for m in re.finditer(r'<d:href>([^<]+)</d:href>', r.text):
for m in re.finditer(r'<(?:d:)?href>([^<]+)</(?:d:)?href>', r.text):
href = m.group(1)
if href.rstrip("/") == base.rstrip("/"): continue
block = r.text[m.start():r.text.find("</d:response>", m.end())]
block = r.text[m.start():r.text.find("</response>", m.end())]
if "addressbook" not in block.lower(): continue
nm = re.search(r'<d:displayname>([^<]*)</d:displayname>', block)
nm = re.search(r'<(?:d:)?displayname>([^<]*)</(?:d:)?displayname>', block)
books.append({"name": nm.group(1) if nm else href.split("/")[-2], "href": href})
return books
@@ -42,7 +42,7 @@ def _get_contacts(href, auth):
body = '<?xml version="1.0"?><c:addressbook-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:carddav"><d:prop><d:getetag/><c:address-data/></d:prop></c:addressbook-query>'
r = httpx.request("REPORT", RADICALE + href, content=body, auth=auth, headers={"Depth": "1", "Content-Type": "application/xml"}, timeout=60)
contacts = []
for m in re.finditer(r'<c:address-data[^>]*>(.*?)</c:address-data>', r.text, re.DOTALL):
for m in re.finditer(r'<(?:c|CR):address-data[^>]*>(.*?)</(?:c|CR):address-data>', r.text, re.DOTALL):
raw = m.group(1).replace("&lt;","<").replace("&gt;",">").replace("&amp;","&")
try: contacts.append(vobject.readOne(raw))
except: pass