Files
email-assistant/openspec/changes/contacts-caldav-server/specs/contacts/carddav-sync/spec.md
T

160 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:<pass>
http://127.0.0.1:5232/estorozhenko/Контакты/`
**THEN** возвращается HTTP 207 (Multi-Status) и в ответе есть
`<D:resourcetype>` с `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** файл существует и содержит актуальную базу контактов.
### Requirement: REQ-CON-CARDDAV-008: Изменение карточки на телефоне
**MUST** — если vCard на сервере изменена клиентом (DAVx5) после последнего
синка (ETag изменился), синк должен обновить соответствующий контакт в
`contacts.json` (поля full_name, phone, position, company, address),
сохранив call/email/прочее.
#### Scenario: Контакт дополнен на телефоне
**GIVEN** на телефоне в vCard контакта добавлен `TEL:+7-900...`
**WHEN** выполняется синк
**THEN** контакт в contacts.json содержит этот телефон.
### Requirement: REQ-CON-CARDDAV-009: Новая карточка на сервере
**MUST** — если на сервере появилась новая vCard, не имеющая соответствия в
`contacts.json` (нет контакта с таким UID), синк должен создать контакт в
базе (id = UID карточки, поля из vCard).
#### Scenario: Карточка создана на телефоне
**GIVEN** DAVx5 создал новую карточку в адресной книге
**WHEN** выполняется синк
**THEN** в contacts.json появляется соответствующий контакт.
### Requirement: REQ-CON-CARDDAV-010: Удаление карточки на сервере
**MUST** — если vCard удалена с сервера клиентом (известный UID, карточки
больше нет), синк должен пометить контакт в базе как удалённый
(`deleted: true`), а не физически удалять (сохранение данных).
#### Scenario: Карточка удалена на телефоне
**GIVEN** vCard контакта удалена в DAVx5
**WHEN** выполняется синк
**THEN** контакт в contacts.json имеет `deleted: true`.
### Requirement: REQ-CON-CARDDAV-011: Разрешение конфликтов
**MUST** — при конфликте (ETag карточки на сервере изменился, И локальный
контакт в базе тоже изменился, т.е. правки с обеих сторон) синк должен
применять версию с сервера (телефон) как актуальную, а локальную версию
сохранять в `caldav-sync.log` (поле `conflict_local`) — данные не теряются.
#### Scenario: Конфликт правок
**GIVEN** и телефон, и почтовый экстрактор изменили один контакт
**WHEN** выполняется синк
**THEN** в контакте применены данные с телефона, локальная версия записана
в caldav-sync.log, следующий запуск не повторяет конфликт.
## Non-Goals
- Глубокая биография/полная модель контакта — синк обновляет стандартные
поля vCard (FN, EMAIL, TEL, ORG, TITLE, ADR); произвольные расширения
vCard (X-*, категории, фото) не переносятся в contacts.json.
- Миграция существующих .vcf из локальных файлов (contacts.vcf остаётся
как есть, синк идёт из contacts.json).
- Синхронизация с внешними CardDAV/Google/Cloud — только локальный Radicale.