9.4 KiB
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.