feat: Kontakte mit Outlook-Feldern + CardDAV-Server + Sharing

Komplette Kontakte-Ueberarbeitung analog zum Kalender-Ausbau.

Backend-Model:
* AddressBook: color (pro Buch), ausserdem Per-User-Color via
  AddressBookShare.color wie bei CalendarShare.
* Contact: volle Outlook-artige Struktur - prefix/first/middle/
  last/suffix, display_name, nickname, organization, department,
  job_title, birthday, anniversary, notes, photo sowie JSON-
  Spalten fuer mehrfach vorhandene Felder (emails, phones,
  addresses mit allen Adressteilen, websites, impp, categories).

Backend-API:
* REST CRUD uebernimmt die neuen Felder und generiert vCard 3.0
  als Source of Truth fuer CardDAV. Voller vCard-Parser +
  -Builder mit Escape/Unescape, TYPE-Parametern, Line-Folding.
* Neuer Endpoint PUT /addressbooks/<id>/my-color - persoenliche
  Farbe pro Buch ohne den Besitzer zu beeinflussen.
* SSE-Events vom Typ 'addressbook' an Besitzer + alle Share-
  Empfaenger bei jeder Aenderung.

CardDAV-Server (backend/app/dav/carddav.py):
* Volle Discovery via principal - addressbook-home-set wird
  neben calendar-home-set annonciert.
* PROPFIND/REPORT/GET/PUT/DELETE/MKCOL fuer
  /dav/<user>/ab-<id>/ und /<...>/{uid}.vcf
* addressbook-query + addressbook-multiget REPORTs
* ETag-basierte Konfliktpruefung via If-Match/If-None-Match

Frontend (ContactsView.vue):
* Komplett neuer Editor mit vier Tabs: Allgemein (Name, Org),
  Kommunikation (Emails/Phones/Websites/IMPP dynamisch),
  Adressen (mehrere mit allen Teilen), Details (Geburtstag,
  Jahrestag, Kategorien, Notizen).
* Avatar mit Fotoauswahl oder Initialen-Farbkreis.
* Kalender-Sharing-Flow 1:1 uebernommen: Autocomplete fuer
  Benutzersuche, Share-Liste mit Stift zum Bearbeiten, Muelleimer
  zum Entfernen, Per-User-Farbe, CardDAV-URL-Info-Block pro
  Adressbuch, Live-Refresh via SSE.
* Suche durchsucht Displayname, E-Mail und Firma.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-12 15:16:01 +02:00
parent fbf10197d7
commit 9c102823e4
6 changed files with 1401 additions and 208 deletions
+10 -1
View File
@@ -180,6 +180,10 @@ def _principal_response(user: User) -> ET.Element:
ET.SubElement(pu, _qn('d', 'href')).text = href
home = ET.SubElement(prop, _qn('c', 'calendar-home-set'))
ET.SubElement(home, _qn('d', 'href')).text = href
# CardDAV address-book home set - same principal URL, addressbook
# collections live next to calendars under /dav/<username>/
ab_home = ET.SubElement(prop, '{urn:ietf:params:xml:ns:carddav}addressbook-home-set')
ET.SubElement(ab_home, _qn('d', 'href')).text = href
return _make_response(href, populate)
@@ -265,7 +269,7 @@ def propfind(subpath=''):
multistatus.append(_principal_response(user))
return _xml_response(multistatus)
# /dav/<username>/ : principal + list calendars
# /dav/<username>/ : principal + list calendars AND addressbooks
if len(parts) == 1:
if parts[0] != user.username:
return Response('', 403)
@@ -273,6 +277,11 @@ def propfind(subpath=''):
if depth != '0':
for cal in _user_calendars(user):
multistatus.append(_calendar_response(user, cal))
# Addressbooks live next to calendars. Import here to avoid a
# circular import at module load time.
from .carddav import _addressbook_response, _user_addressbooks
for ab in _user_addressbooks(user):
multistatus.append(_addressbook_response(user, ab))
return _xml_response(multistatus)
# /dav/<username>/cal-<id>/ : calendar + events