2026-08-07 13:06:48 +02:00
2026-08-04 21:02:28 +02:00
2026-08-05 20:47:00 +02:00
2026-08-05 02:29:18 +02:00
2026-08-05 15:56:16 +02:00
2026-08-07 13:06:48 +02:00
2026-08-07 13:06:48 +02:00
2026-08-05 16:02:20 +02:00
2026-08-05 02:29:18 +02:00
2026-08-05 02:29:18 +02:00
2026-08-05 02:29:18 +02:00
2026-08-05 05:56:28 +02:00
2026-08-05 16:48:06 +02:00
2026-08-07 11:47:57 +02:00

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:

  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:

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_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/<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 / 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
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/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/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)

S
Description
No description provided
Readme MIT
430 KiB
Languages
Python 70.4%
HTML 28.5%
Shell 0.7%
Dockerfile 0.4%