mirror of
https://github.com/skoelle/icloud-contacts-sync.git
synced 2026-09-17 15:30:24 +00:00
165 lines
5.6 KiB
Markdown
165 lines
5.6 KiB
Markdown
# 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, Python-based scheduler (no external 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.json + 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 — Python-based scheduler (PID 1 in container)
|
|
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.json — Per-account credentials (NOT in git, volume-mounted)
|
|
docker/
|
|
entrypoint.sh — Starts scheduler.py (or exec's custom command)
|
|
demo.py — SQLite demo app with fake contacts (screenshot/showcase)
|
|
demo.sh — Launches demo: creates venv, installs deps, starts server
|
|
```
|
|
|
|
## 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.json` + all env vars |
|
|
| `src/db.py` | All MariaDB queries |
|
|
| `sql/schema.sql` | Canonical schema definition |
|
|
| `.env.example` | All supported environment variables |
|
|
| `demo.py` | SQLite demo app with fake contacts (screenshot/showcase) |
|
|
| `demo.sh` | Launches demo: creates venv, installs deps, starts server |
|
|
|
|
## Commands
|
|
|
|
### Lint
|
|
|
|
```bash
|
|
ruff check src/
|
|
```
|
|
|
|
### Run sync locally (no Docker)
|
|
|
|
```bash
|
|
cd src && python3 sync.py
|
|
```
|
|
|
|
### Run mailer locally
|
|
|
|
```bash
|
|
cd src && python3 mailer.py
|
|
```
|
|
|
|
### Docker build
|
|
|
|
```bash
|
|
docker compose build
|
|
```
|
|
|
|
### Docker run
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
### Run demo (no MariaDB needed)
|
|
|
|
```bash
|
|
./demo.sh
|
|
```
|
|
|
|
Starts SQLite-based demo on `0.0.0.0:8000` with 6 fake contacts.
|
|
|
|
## 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.json` 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: scheduler (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).
|
|
|
|
## Documentation
|
|
|
|
**IMPORTANT:** `README.md` and `SPEC.md` are the authoritative German-language documentation for this project. They must be kept up to date whenever code, configuration, schema, or architecture changes. Always update these files as part of any feature or fix implementation.
|
|
|
|
## 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. Update endpoint table in `SPEC.md` and `README.md`
|
|
5. 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
|
|
5. Update `SPEC.md` (data model section)
|
|
|
|
### Changing the sync logic
|
|
1. Edit `src/carddav_client.py` for CardDAV protocol changes
|
|
2. Edit `src/sync.py` for orchestration changes
|
|
3. Update `SPEC.md` if sync behavior changes
|
|
4. Test with a single account first: set `LOG_LEVEL=DEBUG`
|
|
|
|
## License
|
|
|
|
MIT License - Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
|
- Full text in `LICENSE`
|
|
- License headers in all source code files
|