Neue Funktion delete_group_members_by_uids in db.py entfernt Member-Einträge aus group_members wenn ein Kontakt gelöscht wird. Nutzt JOIN über groups Tabelle für Account-Scoping. Doku in README.md aktualisiert.
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.
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:
- Auf
account.apple.commit dieser Apple-ID anmelden. - Zu "Anmelden & Sicherheit" → "App-spezifische Passwörter" gehen.
- 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:
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:
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:
SELECT account, sync_token, updated_at FROM sync_state;
Versandhistorie der Geburtstagsmails:
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_HOURkonfigurierte Stunde (Default 7 Uhr) innerhalb desselben Containers. - Versendet eine HTML-E-Mail mit stylisierten Geburtstagskarten und
Links zur Kontakt-Detailseite (falls
WEB_URLgesetzt). - Über
MAILER_ENABLED=falselä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_logfü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
mainbaut automatisch ein neues Image und pusht es nachghcr.io/<owner>/icloud-contacts-sync. - Ein separater Cleanup-Job behält jeweils nur die letzten 4 erfolgreich gebauten, getaggten Images.
- 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-AnzahlGET /api/groups/{id}— Gruppe mit aufgelösten MembersGET /api/groups/{id}/members— Nur Members einer GruppeGET /api/contacts/{id}— Enthältgroups-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:
- Sync-Container stoppen:
docker compose stop icloud-contacts-sync - Sync-State zurücksetzen:
DELETE FROM sync_state;(erzwingt vollen Re-Sync) - 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:
{
"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 - Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)