mirror of
https://gitverse.ru/kpa39l/email-assistant.git
synced 2026-09-29 09:15:09 +00:00
Session 2026-09-15 code: imap_client (auth+metrics), handlers (TG date format), cron fix (venv python), openspec imap-realtime-sync proposal
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
# email-attachments Specification
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Вложения сохраняются в каталог письма
|
||||
|
||||
Для каждого письма с вложениями (флаг `has_attachment: true` в frontmatter)
|
||||
вложения MUST быть сохранены в подкаталог `attachments/` каталога письма
|
||||
(`/opt/hermes/email/<folder>/YYYY/MM/<uid>/attachments/`). Скачивание выполняется
|
||||
как при опросе по расписанию (текущее поведение), так и при событийной
|
||||
синхронизации (сервис `imap-realtime-sync`). Скачивание вложений MUST NOT
|
||||
выставлять флаг `\Seen`.
|
||||
|
||||
#### Scenario: Письмо с вложением архивировано
|
||||
- **WHEN** `mail_archive.py` заархивировал письмо с `has_attachment: true`
|
||||
- **THEN** файлы вложений лежат в `<msg_dir>/attachments/` и совпадают с вложениями на IMAP-сервере
|
||||
|
||||
#### Scenario: Вложения при событийной синхронизации
|
||||
- **GIVEN** новое письмо с вложением обнаружено сервисом `email-imap-stream`
|
||||
- **WHEN** письмо архивируется по событию IDLE
|
||||
- **THEN** вложения скачаны в `<msg_dir>/attachments/`, флаг `\Seen` не выставлен
|
||||
|
||||
#### Scenario: Вложения при reconcile
|
||||
- **GIVEN** письмо с вложением было пропущено (обрыв соединения)
|
||||
- **WHEN** сервис выполняет reconcile
|
||||
- **THEN** вложения докачиваются в `<msg_dir>/attachments/` (то же поведение,
|
||||
что существующий `--attachments-backfill`)
|
||||
@@ -0,0 +1,26 @@
|
||||
# email-classification Specification
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Классификация каждого нового письма
|
||||
|
||||
Классификация писем (Qwen3:8b через Ollama) MUST запускаться не только по
|
||||
расписанию (cron `mail-classify-handlers`), но и по событиям из ChangeLog
|
||||
(новое письмо / изменённое письмо), которые генерирует сервис
|
||||
`email-imap-realtime-sync`. Механика классификации (теги info/urgent/task/meeting
|
||||
+ `classification`/`classification_reason` в frontmatter) не меняется.
|
||||
|
||||
#### Scenario: Новое письмо после архивации
|
||||
- **GIVEN** письмо заархивировано (`mail_archive.py`)
|
||||
- **WHEN** `email_classifier.py` обрабатывает письмо
|
||||
- **THEN** в frontmatter появляются `classification` и `classification_reason`
|
||||
|
||||
#### Scenario: Новое письмо классифицируется сразу
|
||||
- **GIVEN** в ChangeLog появилось событие `added`
|
||||
- **WHEN** анализатор `email-change-analyzer` обрабатывает событие
|
||||
- **THEN** письмо классифицируется (тег + обоснование в frontmatter) без ожидания cron
|
||||
|
||||
#### Scenario: Событие обработано повторно (идемпотентность)
|
||||
- **GIVEN** письмо уже классифицировано (есть `classification` в frontmatter)
|
||||
- **WHEN** анализатор видит то же событие повторно (например, после рестарта)
|
||||
- **THEN** классификация не выполняется заново (идемпотентно, как сейчас)
|
||||
@@ -0,0 +1,118 @@
|
||||
# imap-realtime-sync Specification
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: REQ-IMAP-SYNC-001: Постоянное IMAP-соединение с IDLE
|
||||
|
||||
Система MUST поддерживать постоянное IMAP-соединение с почтовым сервером
|
||||
(mail.corpoffice.tech:143, STARTTLS) с использованием команды `IDLE`
|
||||
(поддерживается сервером, проверено в CAPABILITY), чтобы узнавать о новых
|
||||
письмах и изменениях в ящике **мгновенно**, без опроса по расписанию.
|
||||
|
||||
#### Scenario: Новое письмо появляется в INBOX
|
||||
- **GIVEN** сервис `email-imap-stream` запущен и держит IDLE-соединение с INBOX
|
||||
- **WHEN** в INBOX приходит новое письмо
|
||||
- **THEN** сервис получает уведомление `* N EXISTS` от сервера в течение секунд
|
||||
(не дольше таймаута IDLE) И инициирует архивацию письма
|
||||
|
||||
#### Scenario: IDLE-соединение обрывается
|
||||
- **GIVEN** IDLE-соединение активно
|
||||
- **WHEN** сервер закрывает соединение (таймаут/сбой)
|
||||
- **THEN** сервис автоматически переподключается и выполняет полный reconcile
|
||||
(синхронизацию изменений), чтобы не пропустить события, случившиеся во время обрыва
|
||||
|
||||
### Requirement: REQ-IMAP-SYNC-002: Reconcile как страховка от пропущенных событий
|
||||
|
||||
Сервис MUST периодически (не реже 1 раза в 5 минут) выполнять reconcile —
|
||||
полную сверку состояния ящика с сервером (UID FETCH / STATUS), потому что IDLE
|
||||
не гарантирует доставку всех событий (особенно при длительных соединениях).
|
||||
|
||||
#### Scenario: Событие потеряно при обрыве
|
||||
- **GIVEN** сервис работал, но IDLE оборвался на 3 минуты, за это время письмо
|
||||
было перемещено пользователем
|
||||
- **WHEN** сервис переподключается и делает reconcile
|
||||
- **THEN** перемещение обнаруживается и фиксируется в ChangeLog
|
||||
|
||||
### Requirement: REQ-IMAP-SYNC-003: Архивация без установки \Seen
|
||||
|
||||
Любое скачивание тела/вложений (по событию IDLE или reconcile) MUST NOT
|
||||
выставлять IMAP-флаг `\Seen`. Используется существующий
|
||||
`fetch_attachments_imaplib()` (BODY.PEEK[]) и чтение с `--preview`.
|
||||
|
||||
#### Scenario: Новое письмо архивируется по событию
|
||||
- **GIVEN** новое письмо в INBOX с флагами `()` (непрочитанное)
|
||||
- **WHEN** сервис по событию IDLE архивирует письмо
|
||||
- **THEN** на IMAP флаги письма остаются `()` (письмо остаётся непрочитанным)
|
||||
|
||||
### Requirement: REQ-IMAP-SYNC-004: Онлайновая копия ящика
|
||||
|
||||
Система MUST вести онлайновую копию ящика в SQLite (`mailbox_state`): для
|
||||
каждого письма — текущий folder, UID, флаги, Message-ID, References/In-Reply-To,
|
||||
дата и время последнего изменения. Копия обновляется при каждом событии.
|
||||
|
||||
#### Scenario: Письмо перемещено пользователем
|
||||
- **GIVEN** письмо было в INBOX, пользователь переместил его в `INBOX/Проекты`
|
||||
- **WHEN** сервис получает событие пересмещения (MOVE/UIDPLUS или reconcile)
|
||||
- **THEN** в `mailbox_state` folder обновлён на `INBOX/Проекты`, в ChangeLog
|
||||
записано событие `moved`
|
||||
|
||||
#### Scenario: Письмо удалено пользователем
|
||||
- **GIVEN** письмо было в ящике
|
||||
- **WHEN** пользователь удаляет письмо (EXPUNGE/FLAGS \Deleted)
|
||||
- **THEN** в `mailbox_state` письмо помечается удалённым (soft-delete), в ChangeLog
|
||||
записано событие `deleted`; файл письма в архиве сохраняется
|
||||
|
||||
### Requirement: REQ-IMAP-SYNC-005: Журнал изменений (ChangeLog)
|
||||
|
||||
Система MUST вести append-only журнал изменений (`mailbox_events`): каждое
|
||||
событие (added / moved / deleted / flag_changed / replied) с timestamp, folder,
|
||||
UID, Message-ID. Журнал служит источником истины для анализатора и RAG.
|
||||
|
||||
#### Scenario: Запись о новом письме
|
||||
- **GIVEN** сервис обнаружил новое письмо
|
||||
- **WHEN** письмо заархивировано
|
||||
- **THEN** в `mailbox_events` создана запись `added` с полным контекстом (UID,
|
||||
folder, Message-ID, timestamp)
|
||||
|
||||
#### Scenario: Запись об ответе
|
||||
- **GIVEN** пользователь ответил на письмо (флаг \Answered или новое письмо с In-Reply-To)
|
||||
- **WHEN** сервис видит изменение
|
||||
- **THEN** в ChangeLog записано событие `replied` (по References/In-Reply-To) или
|
||||
`flag_changed` (для \Answered)
|
||||
|
||||
### Requirement: REQ-IMAP-SYNC-006: Анализатор изменений
|
||||
|
||||
Система MUST иметь отдельный процесс (`email-change-analyzer`), который читает
|
||||
ChangeLog и обновляет производные данные: online-копию, RAG-индексы,
|
||||
классификацию/обработчики. Анализатор работает по событиям (потоково), а не
|
||||
по расписанию.
|
||||
|
||||
#### Scenario: Новое письмо → классификация и обработчики
|
||||
- **GIVEN** в ChangeLog появилось событие `added`
|
||||
- **WHEN** анализатор обрабатывает событие
|
||||
- **THEN** запускается классификация (Qwen3:8b) и обработчики (urgent→TG,
|
||||
task→VTODO, meeting→VEVENT) — как сейчас, но по событию, без ожидания cron
|
||||
|
||||
#### Scenario: Письмо удалено → RAG-индексы
|
||||
- **GIVEN** в ChangeLog событие `deleted`
|
||||
- **WHEN** анализатор обрабатывает событие
|
||||
- **THEN** RAG/индекс помечает письмо удалённым (не удаляя файл) — при поиске
|
||||
агент может сказать «это письмо удалено пользователем»
|
||||
|
||||
### Requirement: REQ-IMAP-SYNC-007: Управление как systemd-сервисами
|
||||
|
||||
Сервисы MUST работать как постоянные процессы под systemd (user units),
|
||||
автозапуск при логине, auto-restart при падении, логи в journald. Hermes cron
|
||||
остаётся как fallback-reconcile (страховка, если оба сервиса не работают).
|
||||
|
||||
#### Scenario: Сервис упал
|
||||
- **GIVEN** `email-imap-stream.service` запущен
|
||||
- **WHEN** процесс падает/убивается
|
||||
- **THEN** systemd перезапускает его автоматически (Restart=always), при старте —
|
||||
reconcile
|
||||
|
||||
#### Scenario: Оба сервиса не работают
|
||||
- **GIVEN** оба сервиса остановлены (например, после перезагрузки без автозапуска)
|
||||
- **WHEN** проходит 5 минут
|
||||
- **THEN** Hermes cron (fallback) запускает `mail_archive.py` — архивация не
|
||||
останавливается полностью
|
||||
Reference in New Issue
Block a user