Files
icloud-contacts-sync/AGENTS.md
T
2026-08-05 02:23:10 +02:00

4.6 KiB

AGENTS.md — iCloud Contacts Sync

Project Overview

Automated CardDAV delta-sync (RFC 6578) of multiple iCloud accounts into a shared MariaDB database, with a daily birthday email mailer and a read-only web UI/API behind Authelia. Runs as Docker containers on a Proxmox host.

Tech Stack

  • Language: Python 3.12
  • Framework: FastAPI (API), PyMySQL (DB), vobject (vCard), lxml (XML), requests (HTTP)
  • Database: MariaDB (InnoDB, utf8mb4)
  • Container: python:3.12-slim, supercronic for cron
  • CI/CD: GitHub Actions — lint (ruff), build+push (ghcr.io)
  • Auth: Authelia reverse-proxy header (Remote-User), no built-in login

Repository Structure

src/
  sync.py            — Main sync orchestrator (cron entry point)
  mailer.py          — Birthday email sender (cron entry point)
  config.py          — Reads accounts.yml + env vars
  db.py              — MariaDB connection and queries
  carddav_client.py  — CardDAV HTTP client (sync-collection, addressbook-query)
  vcard_parser.py    — vCard parsing via vobject
  scheduler.py       — supercronic crontab generation
  api/
    main.py          — FastAPI app (uvicorn entry point)
    auth.py          — Remote-User header dependency
    schemas.py       — Pydantic response models
    static/          — CSS/JS assets
    templates/       — Jinja2 HTML templates
sql/
  schema.sql         — Full schema (contacts, sync_state, sync_runs, birthday_mail_log)
  db-and-user.sql    — One-time DB + user setup
config/
  accounts.yml       — Per-account credentials (NOT in git, volume-mounted)
docker/
  entrypoint.sh      — Generates crontab, starts supercronic

Key Files

File Role
src/sync.py CLI entry point for sync (python3 sync.py)
src/mailer.py CLI entry point for birthday mailer (python3 mailer.py)
src/api/main.py FastAPI app entry point (uvicorn api.main:app)
src/config.py Loads accounts.yml + all env vars
src/db.py All MariaDB queries
sql/schema.sql Canonical schema definition
.env.example All supported environment variables

Commands

Lint

ruff check src/

Run sync locally (no Docker)

cd src && python3 sync.py

Run mailer locally

cd src && python3 mailer.py

Docker build

docker compose build

Docker run

docker compose up -d

Code Conventions

  • All source in src/, single package, no setup.py/pyproject.toml.
  • No comments in code unless explicitly requested.
  • Follow existing code style; no new dependencies unless absolutely necessary.
  • Secrets must never be committed. config/accounts.yml and .env are gitignored.
  • The DB schema uses account column as tenant key — all queries are scoped per account.
  • JSON columns (emails, phones, etc.) store multi-value vCard fields.

Database Schema

Four tables:

  • contacts — All synced contacts (unique on (account, uid))
  • sync_state — Per-account CardDAV sync token for delta sync
  • sync_runs — Sync run history with status and stats
  • birthday_mail_log — Prevents duplicate birthday emails on same day

Schema is defined in sql/schema.sql. Always update schema.sql when changing the data model.

Environment Variables

See .env.example for full list. Key variables:

  • MARIADB_* — Database connection
  • SMTP_* / MAIL_* — Birthday mailer
  • AUTH_REMOTE_USER_HEADER — Authelia header name (default: Remote-User)
  • MAILER_ENABLED — Feature flag for birthday mailer
  • MAIL_SEND_HOUR — Hour (0-23) for daily birthday email

Architecture Notes

  • Single Docker image, two roles: cron (sync+mailer) and API (uvicorn).
  • API is read-only; only the sync container writes to MariaDB.
  • Sync uses CardDAV sync-collection (RFC 6578) for efficient delta sync.
  • Token expiry (~29 days) triggers automatic full re-sync.
  • Deleted contacts are removed from DB (no archival).

Common Tasks

Adding a new API endpoint

  1. Add route in src/api/main.py
  2. Add Pydantic model in src/api/schemas.py if needed
  3. Add DB query in src/db.py if needed
  4. Test with: curl -H "Remote-User: <user>" http://127.0.0.1:8000/<path>

Adding a new contact field

  1. Add column to contacts table in sql/schema.sql
  2. Update src/vcard_parser.py to extract the field
  3. Update src/db.py upsert query
  4. Update src/api/schemas.py if exposing via API

Changing the sync logic

  1. Edit src/carddav_client.py for CardDAV protocol changes
  2. Edit src/sync.py for orchestration changes
  3. Test with a single account first: set LOG_LEVEL=DEBUG