6.7 KiB
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.
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 |
/ |
Web-UI — zeigt Kontakte des eingeloggten Users (HTML) |
GET |
/contacts/{id} |
Web-UI — Detailseite eines einzelnen Kontakts |
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 |
GET |
/api/contacts/birthdays/today |
Heutige Geburtstage |
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