# contacts/carddav-sync Specification ## Purpose Односторонняя синхронизация контактов, извлечённых из подписей писем (pipeline contacts_extractor), в CardDAV-сервер Radicale (:5232), чтобы контакты были доступны клиентам (Android/DAVx5, десктопные клиенты). ## ADDED Requirements ### Requirement: REQ-CON-CARDDAV-001: Адресная книга в Radicale **MUST** — в Radicale должна существовать адресная книга пользователя `estorozhenko` (коллекция `Контакты`, доступ по CardDAV `/estorozhenko/Контакты/`), созданная до начала синка. #### Scenario: Проверка адресной книги **GIVEN** Radicale запущен на :5232 **WHEN** выполняется `curl -X PROPFIND -u estorozhenko: http://127.0.0.1:5232/estorozhenko/Контакты/` **THEN** возвращается HTTP 207 (Multi-Status) и в ответе есть `` с `C:addressbook`. ### Requirement: REQ-CON-CARDDAV-002: vCard для каждого контакта **MUST** — для каждого контакта из `contacts.json` (поле `contacts`), имеющего поле `email`, в адресной книге лежит ровно одна vCard-карточка (.vcf), идентифицируемая по UID, с полями: FN, EMAIL, TEL (если есть), ORG (если есть), TITLE (если есть), ADR (если есть). #### Scenario: Карточка создана **GIVEN** в contacts.json есть контакт `{email: "a@b.ru", full_name: "Иван"}` **WHEN** выполняется `curl -X PROPFIND .../Контакты/...a@b.ru.vcf` **THEN** возвращается 200/207 и vCard содержит `FN:Иван`, `EMAIL:a@b.ru`. ### Requirement: REQ-CON-CARDDAV-003: Повторная синхронизация идемпотентна **MUST** — повторный запуск синка с теми же данными не создаёт дублей (vCard уже существует → только обновление по ETag, не новый ресурс). #### Scenario: Двойной запуск **GIVEN** синк выполнен один раз **WHEN** синк выполняется второй раз без изменений данных **THEN** количество ресурсов в адресной книге не изменяется. ### Requirement: REQ-CON-CARDDAV-004: Обновление существующей карточки **MUST** — при изменении данных контакта в contacts.json (например, у контакта появился телефон) карточка в Radicale обновляется (PUT с If-Match по ETag). Если клиент на телефоне уже изменил карточку (ETag не совпал) — изменения локальной базы не перезаписывают карточку молча; синк пропускает обновление и логирует конфликт (см. также REQ-CON-CARDDAV-006). #### Scenario: Контакт обновлён локально **GIVEN** у контакта в базе появился `phone` **WHEN** выполняется синк **THEN** карточка в Radicale содержит новый `TEL`. ### Requirement: REQ-CON-CARDDAV-005: Удаление карточек **MUST** — контакты, которых больше нет в `contacts.json` (поле `contacts`), могут удаляться из адресной книги (DELETE). Удаление не выполняется по умолчанию (флаг `--prune-caldav`); по умолчанию карточки без соответствующего контакта остаются. #### Scenario: Удаление по флагу **GIVEN** контакт удалён из contacts.json **WHEN** синк запущен с `--prune-caldav` **THEN** соответствующая vCard удаляется из адресной книги. ### Requirement: REQ-CON-CARDDAV-006: Неконфликтная работа с клиентами **MUST** — синк должен использовать ETag (If-Match/If-None-Match) при PUT, чтобы не затирать изменения, сделанные клиентами на телефоне между запусками синка. При конфликте ETag — пропустить и записать предупреждение в лог (файл лога синка). #### Scenario: Конфликт ETag **GIVEN** клиент (DAVx5) изменил vCard на сервере после последнего синка **WHEN** выполняется синк с изменёнными локальными данными этого контакта **THEN** PUT возвращает 412, синк логирует конфликт и продолжает остальные карточки. ### Requirement: REQ-CON-CARDDAV-007: Локальная база остаётся источником **MUST** — contacts.json, contacts.vcf, index.json продолжают обновляться как раньше (источник истины для дедупликации и трекинга). CardDAV — цель синка, не замена локальной базе. #### Scenario: Локальная база не затронута **GIVEN** синк выполнен **WHEN** проверяется содержимое /opt/hermes/email/contacts/contacts.json **THEN** файл существует и содержит актуальную базу контактов. ## Non-Goals - Двусторонняя синхронизация (правки на телефоне не пишутся обратно в contacts.json). - Миграция существующих .vcf из локальных файлов (contacts.vcf остаётся как есть, синк идёт из contacts.json). - Синхронизация с внешними CardDAV/Google/Cloud — только локальный Radicale.