SRS — secretary-server
Software Requirements Specification des CalDAV/CardDAV-Servers, abgeleitet aus
dem Einsatzprofil des Android-Clients secretary-notes-android
(~/Projects/secretary-notes-android). Maßgeblich ist das Funktionsprofil,
das der Client auf dem Server voraussetzt.
- Projekt:
secretary-server(Rust, ehemalsbeowulf-vdir-server) - Referenz-Client:
secretary-notes-android(Kotlin, CalDAV VJOURNAL-Notizen) - Standards: RFC 4791 (CalDAV), RFC 4918 (WebDAV), RFC 5545 (iCalendar), RFC 2617 (Basic Auth)
- Stand: 2026-08-29
1. Einleitung
1.1 Zweck
Der secretary-server stellt Einzel-Benutzern CalDAV- und CardDAV-Collections
bereit. Im Vordergrund dieses SRS steht die Notiz-Funktionalität: Jede Notiz
des Android-Clients ist ein VJOURNAL-Komponent (RFC 5545 §3.6.3) in einer
.ics-Datei unter einer Kalender-Collection. Der Server behandelt VJOURNAL wie
jedes andere Kalender-Komponent (Storage-Sniff, supported-calendar-component-set,
ETags) – es sind keine server-seitigen Sonderfälle für Notizen nötig.
1.2 Referenzdokumente
secretary-notes-android/CONTEXT.md— Datenmodell, Protokoll, Offline-Konzept- Wiki
Apps/notes/gui-protocol— GUI- und Protokoll-Notizen, VJOURNAL-Beispiel secretary-server/CONTEXT.md— Architektur, Storage-Layout, Konfigurationsecretary-server/src/lib.rs— Handler-Implementierung- RFC 4791 / RFC 4918 / RFC 5545
1.3 Begriffe
| Begriff | Bedeutung |
|---|---|
| Collection | Kalender-Verzeichnis unter <storage>/calendars/<id>/ |
| Member | .ics-Datei in einer Collection (genau eine Notiz) |
| VJOURNAL | iCalendar-Komponent für Journal-/Notiz-Einträge |
| ETag | Stabiler Hash (Inhalts-Hash) zur Änderungserkennung / If-Match |
| ctag | Collection-Tag, ändert sich bei jeder Änderung der Collection |
| sync-token | Änderungsspur für REPORT sync-collection |
2. Anforderungsüberblick
Der Android-Client durchläuft folgende Abläufe gegen den Server:
| Ablauf | Server-Route | Methoden |
|---|---|---|
| Anmeldung/Validierung | /calendars/ |
PROPFIND Depth 1 |
| Collections auflisten | /calendars/ |
PROPFIND Depth 1 |
| Notizen auflisten | /calendars/<id>/ |
PROPFIND Depth 1 |
| Notiz laden | /calendars/<id>/<uid>.ics |
GET |
| Notiz anlegen/ändern | /calendars/<id>/<uid>.ics |
PUT (+ If-Match) |
| Notiz löschen | /calendars/<id>/<uid>.ics |
DELETE |
| Notiz-Sammlung anlegen | /calendars/<id>/ |
MKCALENDAR |
3. Funktionale Anforderungen
3.1 Authentifizierung
- FR-AUTH-01: Der Server authentifiziert pro Request per HTTP Basic Auth (RFC 2617). Es gibt genau ein Passwort (Single-User-Betrieb).
- FR-AUTH-02: Der Benutzername wird zur Laufzeit aus der
$USER-Umgebung gelesen (kein Config-Feld, kein Multi-User). - FR-AUTH-03: Das Passwort wird als SHA256-Hash mit optionalem Salt gespeichert und constant-time verglichen.
- FR-AUTH-04: Ungültige oder fehlende Credentials liefern
401 Unauthorized. Der Client wertet 401 als „Anmeldung fehlgeschlagen" aus.
3.2 Collection-Auflistung (PROPFIND /calendars/, Depth 1)
- FR-CAL-01:
PROPFIND /calendars/mitDepth: 1liefert207 Multistatusmit genau einem<response>pro Collection. - FR-CAL-02: Jede Collection liefert mindestens:
resourcetype(<collection/><C:calendar/>),displayname,getctag,sync-tokenundsupported-calendar-component-set. - FR-CAL-03: Der Request-Body filtert die angefragten Properties
(
<D:prop>); leere Bodies bzw.allpropliefern alle Properties. - FR-CAL-04:
supported-calendar-component-setlistet die tatsächlich in der Collection gespeicherten Komponenten als<C:comp name="…"/>-Elemente, z. B.<C:comp name="VEVENT"/>bzw.<C:comp name="VJOURNAL"/>. Quelle ist diecomponents=-Zeile in.metadata(autoritativ); fehlt sie, wird per Storage-Sniff über die enthaltenen.ics-Dateien ermittelt (FR-CAL-09). - FR-CAL-05: Der Client wählt eine Collection als Notiz-Sammlung nur dann,
wenn ihr
supported-calendar-component-setVJOURNALenthält.
3.3 Mitglieder-Auflistung (PROPFIND /calendars/<id>/, Depth 1)
- FR-CAL-06:
PROPFIND /calendars/<id>/mitDepth: 1liefert207 Multistatus: ein<response>für die Collection selbst plus ein<response>pro.ics-Member (bzw. pro.vcf-Member bei Addressbooks). - FR-CAL-07: Jeder Member liefert mindestens:
resourcetype(leer für Ressourcen),getetag,getcontenttype(text/calendar; charset=utf-8),getlastmodified,getcontentlength. - FR-CAL-08: Die Member-Auflistung funktioniert bei
Depth: 1undDepth: Infinityidentisch (wichtig für Thunderbird-V1-Sync und den Android-Client). - FR-CAL-09 (Storage-Sniff): Fehlt
.metadata/components=, werden die Komponenten einer Collection durch Scannen der.ics-Inhalte ermittelt (BEGIN:VJOURNAL→VJOURNAL). Eine gemischte Collection (VEVENT + VJOURNAL) meldet beide Komponenten. - FR-CAL-10:
.ics-Dateien, die keinem bekannten Komponenten-Block entsprechen, werden in der Member-Liste weiterhin mit ETag gelistet.
3.4 Notiz laden (GET /calendars/<id>/<uid>.ics)
- FR-CAL-11:
GETauf eine existierende.ics-Datei liefert200mitContent-Type: text/calendar; charset=utf-8,ETag(Inhalts-Hash) undLast-Modified. - FR-CAL-12: Der Body enthält den vollständigen iCalendar-Inhalt
unverändert; VTODO-Blöcke werden ggf. per
adjust_vtodo_dtstartnormalisiert (DTSTART-Handling), VJOURNAL bleibt unberührt. - FR-CAL-13: Nicht existierende Ressourcen liefern
404 Not Found.
3.5 Notiz anlegen/ändern (PUT /calendars/<id>/<uid>.ics)
- FR-CAL-14:
PUTlegt eine neue Ressource an (Status201 Created) oder aktualisiert eine bestehende (Status204 No Content); Erfolg liefert den neuenETagim Response-Header. - FR-CAL-15 (If-Match / 412): Existiert die Ressource und stimmt
If-Matchweder mit*noch mit dem aktuellen ETag überein, antwortet der Server mit412 Precondition Failed. Der Client behandelt 412 als Konflikt („Notiz wurde auf dem Server geändert") und erzwingt ein Neu-Laden. - FR-CAL-16:
If-Match: *auf eine nicht existierende Ressource liefert ebenfalls412(erzwingt Anlegen über PUT ohne If-Match). - FR-CAL-17:
If-None-Match: *auf eine existierende Ressource liefert412(verhindert Überschreiben ohne explizites Aktualisieren). - FR-CAL-18: Leerer Body auf
.icsliefert400 Bad Request. - FR-CAL-19: Der Server schreibt atomar (Temp-Datei + Rename) und
aktualisiert
ctag/sync-token/.sync-logder Collection.
3.6 Notiz löschen (DELETE /calendars/<id>/<uid>.ics)
- FR-CAL-20:
DELETEauf eine existierende Ressource liefert204 No Contentund entfernt die Datei sowie den Eintrag ausctag/sync-token. - FR-CAL-21: Nicht existierende Ressourcen liefern
404. Der Client verwirft in diesem Fall ausstehende Offline-Löschungen (siehe FR-OFF-04).
3.7 Collection anlegen (MKCALENDAR /calendars/<id>/)
- FR-CAL-22:
MKCALENDARlegt eine Collection unter/calendars/<id>/an (Status201 Created) und schreibt.metadatainklusivecomponents=-Zeile. - FR-CAL-23: Die Komponenten werden aus dem Request-Body
(
<C:comp name="…"/>insupported-calendar-component-set) gelesen. Der Client sendet<C:comp name="VJOURNAL"/>. - FR-CAL-24: Fehlt der Body bzw. die Komponenten-Angabe, wird
VEVENTals Default verwendet. - FR-CAL-25: Leerer/ungültiger Collection-Name (
/calendars/allein) liefert404; Storage-Fehler liefern500.
3.8 ETag-Semantik
- FR-CAL-26: Der ETag ist ein stabiler Inhalts-Hash
(
compute_etag): identischer Inhalt → identischer ETag. Er ändert sich genau dann, wenn sich der Inhalt ändert. - FR-CAL-27: Derselbe Hashwert wird in
PROPFIND getetag,GET ETagundPUT-Antwort konsistent geliefert (Grundlage des If-Match-Ablaufs).
3.9 Komponenten-Handling allgemein
- FR-CAL-28: Der Server unterstützt
PROPFIND(Depth 1/Infinity),GET,HEAD,PUT,DELETE,PROPPATCH,MKCALENDAR,MKCOL,REPORT(multiget, sync-collection, free-busy) undOPTIONS(Allow-Header). - FR-CAL-29:
PROPFIND-Antworten sind validemultistatus-Dokumente (application/xml; charset=utf-8); 404-Items immultiget-REPORT verwenden<status>auf<response>-Ebene (RFC 4918 simple form).
3.10 Verhalten aus Sicht des Android-Clients (Offline/Fehler)
- FR-OFF-01: Verbindungs-/
IOException-Fehler (Timeout, DNS, TLS, Server weg) darf der Server nicht erzeugen; der Client schaltet darauf in den Offline-Modus (Stale-While-Revalidate aus dem lokalen Cache). - FR-OFF-02:
DavExceptionmit HTTP-Status (401/412/404/500/…) wird vom Client als echter Fehler angezeigt und NICHT als Offline gewertet. - FR-OFF-03: Der Client spielt lokale Offline-Änderungen (PUT/DELETE/
MKCALENDAR) nach erfolgreichem Sync in Reihenfolge ab (
flushPending). - FR-OFF-04: Ein 404 bei einem ausstehenden DELETE bestätigt den Lösch- Vorgang (Ressource existiert nicht mehr) und verwirft den Queue-Eintrag ohne Fehler.
- FR-OFF-05: Ein 412 bei einem ausstehenden PUT verwirft den Queue-Eintrag mit Konflikt-Meldung (kein unendlicher Retry).
3.11 Server-gesteuerter Sync mit Fortschritts-Metriken
Hintergrund: Der zugrunde liegende CalDAV/CardDAV-Sync ist pull-basiert – der
Client fragt die Collections ab. Damit Clients während des Syncs einen
Fortschrittsbalken (Spinner/Progressbar) und ein Abschluss-Resultat
(„Sync abgeschlossen"/„fehlgeschlagen") anzeigen können, stellt der Server
zusätzlich einen JSON-„Sync-Job"-Dienst bereit: Der Server startet einen
Hintergrund-Job, der eine Collection durchwandert, und liefert per Status-Endpunkt
Fortschritts-Metriken (total/processed), die der Client pollen kann.
- FR-SYNC-01:
POST /sync/startmit JSON-Body{"collection":"/calendars/<id>", "from_token":"<optional>"}startet einen asynchronen Sync-Job. Gültigecollection-Werte liegen unter/calendars/oder/addressbooks/(root- oder Collection-Pfad); unbekannte/ungültige Pfade liefern400 Bad Request. Der Server antwortet200mit{"id":"<job-id>", "total":<n>}. - FR-SYNC-02: Der Sync-Job ist ein Hintergrund-Task (kein Blockieren des
Requests).
totalist die Anzahl der zu verarbeitenden Ressourcen: beifrom_tokendie Anzahl der.sync-log-Delta-Einträge nach dem Token, sonst die Anzahl der Member-Ressourcen (list_resources) der Collection. - FR-SYNC-03:
GET /sync/status/{id}liefert den Job-Status als JSON:{"id","collection","state","total","processed","error"}, wobeistaterunning|done|failedist.processedwird mit jedem verarbeiteten Element erhöht;total==0gilt sofort als done. Ein bereits vorhandener Job liefert200, ein unbekannter404. - FR-SYNC-04:
GET /sync/jobslistet die aktuell laufenden (und kürzlich beendeten, bis zur nächsten Bereinigung) Sync-Jobs als JSON-Array unter dem Schlüsseljobs. - FR-SYNC-05 (Job-Lebenszyklus): Jobs leben in einem
SyncJobRegistry-Arc<Mutex<HashMap>>auf demAppState(In-Memory, nicht persistent). Beim Start wird der Bestand von bereits beendeten Jobs bereinigt (prune_finished); der Job selbst markiert sich beim Durchlaufen der Collection alsdone. - FR-SYNC-12 (Metric-Definition): Die Fortschritts-Metrik ist
progress = min(processed / total, 1)(beitotal==0⇒ 1). Der Client kann daraus einen Determinat-Progressbar (0…1) rendern;state==doneschließt mit Erfolg,state==failedmit Fehlermeldung ab. - FR-SYNC-13 (Client-Anzeige): Der Android-Client (Kalender, Kontakte,
Notizen, Aufgaben, Journal) startet beim manuellen Refresh (
forceRefresh/loadAddressBooks/loadCollections) für jede bekannte Collection einen Server-Job (POST /sync/start) und polltGET /sync/status/{id}. Während der Sync läuft, zeigt er einen Spinner und einen Determinat-Progressbar (progressaus FR-SYNC-12) plusprocessed / total; sobald alle Jobsdone/failedsind, blendet er einen einmaligen Toast aus („Sync abgeschlossen"/„Sync fehlgeschlagen: …"). Ein bereits laufender Sync-Job wird nicht erneut gestartet.
4. Nicht-funktionale Anforderungen
- NFR-01 (Einzel-Benutzer): Ein Benutzer, ein Passwort; keine User-Verwaltung, kein Multi-Tenant.
- NFR-02 (File-basiert): Alle Daten liegen als
.ics/.vcf-Dateien im Storage-Pfad; kein Datenbank-Backend. - NFR-03 (Atomare Schreibvorgänge): Schreibvorgänge laufen über Temp-Datei + Rename; abgebrochene Writes hinterlassen keinen halben Zustand.
- NFR-04 (TLS): TLS via rustls, optional konfigurierbar; Zertifikate per Let's-Encrypt-Deploy-Hook erneuerbar.
- NFR-05 (Pfad-Traversal-Schutz):
resolve_pathverhindert Zugriffe außerhalb des Storage-Roots. - NFR-06 (Antwortzeiten): Alle hier spezifizierten Endpunkte sind lokal (LAN) im niedrigen Millisekundenbereich; der Android-Client setzt Connect/Read-Timeouts (20 s/60 s) auf dem Server nicht voraus.
- NFR-07 (Betrieb): systemd --user Service; Konfiguration per TOML
(
SECRETARY_CONFIG-Env, Default~/.config/secretary-server/secretary-server.toml). - NFR-08 (Logging): Pro Request wird Methode, Pfad, Status-Code und
User-Agent geloggt (Format
METHOD /path → STATUS (UA: …)). - NFR-09 (Sync-Dienst): Die Sync-Endpunkte sind reiner JSON-REST-Dienst, in-memory und zustandslos über Requests hinweg (außer dem Job-Registry); sie beeinflussen die CalDAV/CardDAV-Datenablage nicht.
5. Verifikation
| Anforderung | Verifikation |
|---|---|
| FR-CAL-01..05 | Integrationstests tests/server.rs + Client-Test RealServerParserTest (PROPFIND /calendars/ gegen live Server) |
| FR-CAL-06..10 | Integrationstests PROPFIND Depth 1/Infinity inkl. Member-Eigenschaften |
| FR-CAL-11..13 | GET-Roundtrip (200/404, ETag/Last-Modified) |
| FR-CAL-14..19 | PUT-Roundtrip inkl. 412- und 400-Fällen |
| FR-CAL-20..21 | DELETE-Roundtrip (204/404) |
| FR-CAL-22..25 | MKCALENDAR mit <comp name="VJOURNAL"/> → .metadata/components=VJOURNAL |
| FR-CAL-26..27 | ETag-Konsistenz über PROPFIND/GET/PUT |
| FR-AUTH-01..04 | 401 bei falschem Passwort, 207 bei korrektem |
| FR-OFF-02..05 | Client-seitige Unit-/UI-Prüfung der Fehlerpfade |
| FR-SYNC-01..12 | Integrationstests tests/server.rs (test_sync_*): Start/Status/Rundlauf, 404 für unbekannte Jobs, 400 für ungültige Collection |
| FR-SYNC-13 | Client-Kompilierung (gradle :app:compileDebugKotlin) aller fünf secretary-Apps + Funktionstest Poll/Job-Lebenszyklus gegen den Sync-Job-Dienst |
6. Offene Punkte
- Suche serverseitig:
calendar-query/calendarserver-query-REPORT (aktuell filtert der Client clientseitig). - Passwortverschlüsselung im Client: EncryptedSharedPreferences (Server-Hash ist bereits implementiert).
- Multiget-Nutzung durch den Client: Statt N× GET könnte der Client
calendar-multigetnutzen (Server-seitig unterstützt).