# icloud-contacts-sync Synct alle Kontakte mehrerer iCloud-Accounts per CardDAV Delta-Sync (RFC 6578) automatisiert alle 15 Minuten in eine MariaDB-Datenbank (`mariadb.internal`), plus täglichem Mailversand der heutigen Geburtstage. Für den vollständigen technischen Hintergrund siehe [SPEC.md](./SPEC.md). ## Voraussetzungen - Eine oder mehrere Apple-IDs mit aktivierter Zwei-Faktor-Authentifizierung. - Für jede Apple-ID ein app-spezifisches Passwort. - Eine erreichbare MariaDB-Instanz mit vorbereiteter Datenbank. - Ein SMTP-Relay (z. B. dein Mailprovider oder ein lokaler Relay) für den Geburtstags-Mailer. - Docker bzw. Docker Compose auf dem Zielhost (z. B. der Docker-Host auf deinem Proxmox-Host). ## 1. App-spezifische Passwörter erzeugen Für jede Apple-ID, die du syncen willst: 1. Auf `account.apple.com` mit dieser Apple-ID anmelden. 2. Zu "Anmelden & Sicherheit" → "App-spezifische Passwörter" gehen. 3. Ein neues Passwort mit sprechendem Namen erzeugen (z. B. `contacts-sync-debian`) und sofort sichern. ## 2. Multi-User-Konfiguration anlegen ``` cp config/accounts.json.example config/accounts.json vim config/accounts.json ``` Trage für jede Apple-ID einen Eintrag mit eindeutigem `name`, `apple_email`, `apple_app_password` und `authelia_user` ein. Diese Datei bleibt lokal auf dem Host, sie ist in `.gitignore` ausgeschlossen und wird nur als Volume in den Container gemountet. Siehe `config/README.md` für eine vollständige Beschreibung der Felder. ## 3. Datenbank vorbereiten Falls Datenbank und Benutzer noch nicht existieren, führe dieses Skript einmalig aus: ```bash mysql -u root -p < sql/db-and-user.sql ``` ## 4. Umgebungsvariablen konfigurieren ``` cp .env.example .env vim .env ``` Trage mindestens `MARIADB_USER`, `MARIADB_PASSWORD` sowie (falls du den Mailer nutzen willst) `SMTP_HOST`, `MAIL_FROM` und `MAIL_TO` ein. ## 5. Image beziehen ``` docker login ghcr.io -u DEIN_GITHUB_USER ``` Passe in `docker-compose.yml` den Image-Namen (`ghcr.io/DEIN_GITHUB_USER/icloud-contacts-sync:latest`) auf deinen tatsächlichen GitHub-Namespace an. ## 6. Starten ``` docker compose up -d ``` Beim ersten Start wird für jeden Account automatisch ein vollständiger initialer Sync ausgeführt (kein gespeicherter sync-token vorhanden). Danach laufen alle 15 Minuten nur noch Delta-Syncs, die ausschließlich Änderungen seit dem letzten Lauf übertragen. ## 7. Logs und Status prüfen ``` docker logs -f icloud-contacts-sync ``` Sync-Historie je Account: ```sql SELECT account, sync_type, started_at, finished_at, status, contacts_upserted, contacts_deleted FROM sync_runs ORDER BY started_at DESC LIMIT 20; ``` Aktueller Delta-Sync-Token je Account: ```sql SELECT account, sync_token, updated_at FROM sync_state; ``` Versandhistorie der Geburtstagsmails: ```sql SELECT sent_date, contacts_count, sent_at FROM birthday_mail_log ORDER BY sent_date DESC LIMIT 10; ``` ## 8. Geburtstags-Mailer - Läuft automatisch täglich um die in `MAIL_SEND_HOUR` konfigurierte Stunde (Default 7 Uhr) innerhalb desselben Containers. - Versendet eine HTML-E-Mail mit stylisierten Geburtstagskarten und Links zur Kontakt-Detailseite (falls `WEB_URL` gesetzt). - Über `MAILER_ENABLED=false` lässt sich der Mailer ganz abschalten, ohne den Kontakt-Sync zu beeinträchtigen. - Manueller Testlauf im laufenden Container: ``` docker exec -it icloud-contacts-sync python3 /app/mailer.py ``` - Ein zweiter manueller Lauf am selben Tag versendet keine zweite Mail, solange bereits ein Eintrag in `birthday_mail_log` für heute existiert. ## 9. Lokale Entwicklung (ohne Docker) ``` python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt cd src python3 sync.py python3 mailer.py ``` ## 10. CI/CD - Jeder Push auf `main` baut automatisch ein neues Image und pusht es nach `ghcr.io//icloud-contacts-sync`. - Ein separater Cleanup-Job behält jeweils nur die letzten 4 erfolgreich gebauten, getaggten Images. - Der Lint-Job nutzt `ruff==0.15.22` (gepint) mit der alten Default-Auswahl `E4,E7,E9,F` (siehe `pyproject.toml`), so bleiben die Ergebnisse reproduzierbar und unabhängig von neuen Ruff-Defaults. - Details siehe SPEC.md, Abschnitt 9. ## Bekannte Grenzen und geplante Erweiterungen - Delta-Sync reduziert die übertragene Datenmenge stark, ersetzt aber keine vollständige Historie: ein gelöschter iCloud-Kontakt wird auch aus MariaDB entfernt, ohne Archiv. - Nur iCloud als Quelle, Google/Microsoft sind nicht Teil dieses Repos. ## Kontaktruppen iCloud-Länder speichern Gruppen als eigene vCards mit `X-ADDRESSBOOKSERVER-KIND:group`. Diese werden beim Sync automatisch erkannt und separat in den Tabellen `groups` und `group_members` gespeichert (nicht als Kontakte). Gruppen sind über die API abrufbar: - `GET /api/groups` — Alle Gruppen mit Member-Anzahl - `GET /api/groups/{id}` — Gruppe mit aufgelösten Members - `GET /api/groups/{id}/members` — Nur Members einer Gruppe - `GET /api/contacts/{id}` — Enthält `groups`-Feld mit Gruppennamen Wird eine Gruppe gelöscht, werden zugehörige Memberschaften automatisch entfernt (`ON DELETE CASCADE`). Wird ein Mitglied-Kontakt gelöscht, wird der Member-Eintrag in allen Gruppen ebenfalls entfernt (manueller Cleanup im Sync-Code). Die Gruppe selbst bleibt erhalten. ### Migration bei erstem Deploy Bei Bestands-DBs lagen Gruppen bisher als normale Kontakte in der `contacts`-Tabelle. Nach dem Deploy müssen diese einmalig bereinigt werden: 1. Sync-Container stoppen: `docker compose stop icloud-contacts-sync` 2. Sync-State zurücksetzen: `DELETE FROM sync_state;` (erzwingt vollen Re-Sync) 3. Container neu starten: `docker compose start icloud-contacts-sync` Beim nächsten Sync-Lauf werden alle vCards neu klassifiziziert — Gruppen landen in `groups`, Kontakte bleiben in `contacts`. ## 11. Web-Ansicht und API (interner Zugriff über Authelia) Läuft als zweiter Service aus demselben Image, aber mit anderem Startbefehl, siehe `docker-compose.yml` (`icloud-contacts-api`). Die API selbst hat kein eigenes Login, sie vertraut vollständig dem vorgeschalteten Reverse-Proxy mit Authelia. ### Voraussetzung: Reverse-Proxy mit Authelia Dein bestehender Reverse-Proxy muss für den Pfad/Host der Web-Ansicht einen `auth_request` gegen Authelia ausführen und danach den authentifizierten Benutzernamen im Header `Remote-User` an `127.0.0.1:8000` weiterreichen. Ein typischer nginx-Ausschnitt: ``` location / { auth_request /authelia/verify; auth_request_set $user $upstream_http_remote_user; proxy_set_header Remote-User $user; proxy_pass http://127.0.0.1:8000; } ``` Falls dein Setup den Benutzernamen unter einem anderen Header liefert, passe `AUTH_REMOTE_USER_HEADER` in der `.env` entsprechend an. ### Accounts-Mapping ergänzen In `config/accounts.json` bekommt jeder Account zusätzlich ein Feld `authelia_user`: ```json { "accounts": [ { "name": "markus", "apple_email": "...", "apple_app_password": "...", "authelia_user": "mmustermann" } ], "admins": ["mmustermann"] } ``` Ein Benutzer aus `admins` sieht alle Accounts, alle anderen gemappten Benutzer sehen ausschließlich ihren eigenen Account. ### Starten ``` docker compose up -d icloud-contacts-api ``` Der Service läuft nur an `127.0.0.1:8000`, ein direkter externer Zugriff ohne den Reverse-Proxy ist damit nicht möglich. ### Endpunkte (Routes) | Methode | Pfad | Beschreibung | |---------|------|--------------| | `GET` | `/` | Dashboard mit Kontaktdaten-Übersicht, letzten Sync-Status und Geburtstagen der nächsten 7 Tage (HTML) | | `GET` | `/search` | Web-UI -- Suchfunktion, zeigt Kontakte des eingeloggten Users | | `GET` | `/contacts/{id}` | Web-UI -- Detailseite eines einzelnen Kontakts (inkl. Gruppen) | | `GET` | `/api/health` | Health Check (`{"status": "ok"}`), kein Login nötig | | `GET` | `/api/contacts` | Kontaktsuche mit Pagination (`?q=...&limit=...&offset=...`) | | `GET` | `/api/contacts/{id}` | Einzelnen Kontakt per ID abrufen (inkl. `groups`-Feld) | | `GET` | `/api/contacts/count` | Anzahl der Kontakte des eingeloggten Users | | `GET` | `/api/contacts/birthdays/today` | Heutige Geburtstage | | `GET` | `/api/contacts/birthdays/upcoming` | Geburtstage der nächsten N Tage (`?days=7`, Default 7) | | `GET` | `/api/groups` | Gruppenliste mit Member-Anzahl | | `GET` | `/api/groups/{id}` | Einzelne Gruppe mit aufgelösten Members | | `GET` | `/api/groups/{id}/members` | Members einer Gruppe (Kontaktdaten) | | `GET` | `/api/sync-runs` | Letzte 50 Sync-Runs (Status, Zeitstempel, Fehler) | Alle Endpunkte (außer `/api/health`) erfordern eine Authentifizierung über den `Remote-User`-Header. Nicht-Admins sehen nur die Daten ihres eigenen Accounts. ### API kurz testen (lokal auf der Docker-Host, mit Header simuliert) ``` curl -H "Remote-User: mmustermann" http://127.0.0.1:8000/api/contacts ``` ## License Licensed under the [MIT License](LICENSE) - Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)