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

9.4 KiB
Raw Blame History

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.