Files
email-assistant/openspec/changes/imap-realtime-sync/proposal.md
T

150 lines
13 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.
# Proposal: Потоковая синхронизация почты (IMAP IDLE) и онлайновая копия ящика
## Why
Сейчас `mail_archive.py` опрашивает IMAP **каждые 5 минут** по расписанию (Hermes cron).
Это pull-модель с двумя фундаментальными ограничениями:
1. **Задержка до 5 минут** — новые письма попадают в архив и в RAG-индексы не сразу,
а в лучшем случае через 5 минут (а с учётом очереди классификатора/обработчиков — дольше).
2. **Слепота к изменениям, которые делает сам пользователь.** Пользователь активно
работает с почтой в клиенте: переносит письма между папками, удаляет, отвечает,
помечает прочитанными. Архиватор знает только `last_uid` и **не видит**:
- перемещение письма из INBOX в подпапку (письмо «пропадает» из INBOX, но в архиве остаётся в INBOX)
- удаление письма (архив хранит удалённое письмо, но не знает, что оно удалено)
- ответ/флаг `\Answered`, `\Flagged` (архив хранит статичные флаги)
- появление новых писем в **существующих** папках (last_uid инкрементален, но только
для писем, добавленных после последнего опроса)
Из-за этого **нельзя строить RAG/граф знаний по почте корректно**: цепочки
«письмо пришло → прочитано → перемещено → на него ответили» в данных отсутствуют,
и при поиске агент не может сказать «это письмо вы удалили» — он просто не находит
его в той папке, где оно было при архивации.
**Хочется:** постоянное (потоковое) соединение с IMAP — как в почтовом клиенте —
которое **мгновенно** узнаёт о новых письмах (через IDLE-уведомления), и отдельный
процесс, который ведёт **онлайновую копию ящика** с полной историей изменений
(лог событий: письмо добавлено/перемещено/удалено/прочитано/получен ответ).
На этой основе потом можно: корректно строить RAG, писать «журнал изменений»,
отвечать на вопросы про историю ящика.
## What Changes
### Архитектура: 2 сервиса (микросервисы на одной машине)
Заменяем «cron-скрипт каждые 5 минут» на два **постоянных процесса** (systemd units):
```
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ svc 1: IMAP Stream/Sync │ │ svc 2: Change Analyzer │
│ (постоянный IDLE-коннект) │ │ (анализ изменений) │
│ │ │ │
│ • держит 1+N IDLE-соединений│ │ • читает журнал изменений │
│ • мгновенно видит события │ │ • обновляет online-копию │
│ • пишет в ChangeLog │ │ • RAG/классификация/ │
│ • скачивает новые письма │ │ обработчики │
└───────────┬─────────────────┘ └─────────────┬───────────────┘
│ журнал изменений (append-only) │
▼ ▼
┌──────────────────────────────────────────────────────┐
│ SQLite: online-копия ящика (состояние) │
│ + ChangeLog (события, append-only) │
└──────────────────────────────────────────────────────┘
```
**svc 1 — IMAP Stream Sync («синхронизатор»):**
- одно постоянное соединение с IMAP (Exchange, :143 STARTTLS), авторизация как у himalaya;
- `IDLE`-команда на INBOX (и ключевых папках) — сервер **пушит** уведомления
`* N EXISTS` / `* N EXPUNGE` / `* N FETCH FLAGS` сразу при изменении;
- на событие — архив письма (через существующий `fetch_attachments_imaplib`, BODY.PEEK[],
не ставя `\Seen`), обновление online-копии, запись события в ChangeLog;
- периодический (раз в N мин) **reconcile** — полный `UID FETCH` изменений с сервера
(страховка от пропущенных событий при обрыве IDLE: сервер не гарантирует доставку всех
событий через IDLE, но reconcile это закрывает);
- папки: динамическое обнаружение (`get_inbox_subfolders()` уже есть) + CHILDREN;
- **MOVE/UIDPLUS** (поддерживается Exchange) — позволяет точнее отслеживать перемещения
(`UID MOVE` возвращает старый/новый UID).
**svc 2 — Change Analyzer («анализатор»):**
- постоянно крутится, читает ChangeLog из SQLite (или файловый след);
- поддерживает **online-копию ящика**: для каждого письма — текущий folder, flags, uid,
thread (References/In-Reply-To), время последнего изменения;
- корректно строит **цепочки**: письмо пришло → прочитано (flag) → перемещено в папку →
на него ответили (по References) → удалено (если пользователь удалил);
- на основе изменений обновляет RAG/индексы и запускает классификатор/обработчики
(аналог текущего `mail-classify-handlers` cron, но по событиям, а не по расписанию);
- **журнал изменений** = история «что, когда, с каким письмом произошло» — его можно
показывать пользователю и использовать в RAG (в отличие от текущей модели, где
у нас только статичные снимки).
### Новые компоненты
- `scripts/imap_stream.py` — svc 1 (постоянный процесс; systemd unit `email-imap-stream.service`)
- `scripts/change_analyzer.py` — svc 2 (анализ ChangeLog → online-копия → RAG/обработчики)
- `schema`: таблица `mailbox_state` (online-копия) + `mailbox_events` (ChangeLog) в
существующей SQLite (`/opt/hermes/email/mail_index.db`) или отдельной
`/opt/hermes/email/state/mailbox.db`
- systemd units (user-level) вместо Hermes cron; cron остаётся как fallback/страховка
(например, каждые 5 минут — reconcile, если оба сервиса умерли)
### Существующие скрипты не ломаются
- `mail_archive.py` остаётся (он уже умеет BODY.PEEK[] и не ставит Seen);
svc 1 может использовать его функции как библиотеку (или дублировать минимально).
- `email_classifier.py`, `email_handlers.py`, `mail_index.py`, `digest.py` — вызываются
из svc 2 (по событиям) и/или остаются по cron (по расписанию) — поведение не меняется.
## Capabilities
### New Capabilities
- `imap-realtime-sync`: Постоянное IMAP-соединение (IDLE) с мгновенным обнаружением
новых писем и изменений (перемещение/удаление/флаги) — без опроса по расписанию.
- `mailbox-online-copy`: Онлайновая копия ящика (folder/flags/uid/thread у каждого письма)
+ журнал изменений (ChangeLog: добавить/переместить/удалить/прочитать/ответить) —
источник истины для RAG и ответов «что случилось с письмом».
- `email-thread-tracking`: Отслеживание цепочек писем (References/In-Reply-To) и событий
жизни письма (пришло → прочитано → перемещено → ответ → удалено).
### Modified Capabilities
- `email-attachments`: скачивание вложений теперь инициируется событиями IDLE,
а не только опросом по расписанию (механика та же: BODY.PEEK[]).
- `email-classification` / `email-handlers`: запускаются по событиям ChangeLog
(новое письмо/изменение), а не только по cron.
## Impact
- **Процессы:** 2 новых systemd user unit (`email-imap-stream.service`,
`email-change-analyzer.service`), всегда запущены. Hermes cron `mail-archive-every-5min`
→ заменяется на reconcile-cron (или остаётся как fallback).
- **Сеть:** постоянное TCP-соединение с mail.corpoffice.tech:143 (keep-alive, IDLE
продлевается каждые ~29 мин). Раньше соединение открывалось каждые 5 минут.
- **Данные:** новая таблица SQLite (online-копия + ChangeLog). Письма в `/opt/hermes/email/`
не переезжают — формат `email.md` и структура папок сохраняются (совместимость с
`mail_index.py`, Obsidian-бэкапами, поиском).
- **Риски:**
- Exchange может рвать IDLE-соединения (keep-alive не вечен) — нужен auto-reconnect
с reconcile после переподключения (обязательное требование).
- IDLE на Exchange поддерживается (проверено: в CAPABILITY есть `IDLE`), но поведение
сервера при `EXPUNGE`/перемещении может отличаться — reconcile закрывает.
- Логин: himalaya логинится успешно. Разобрано 2026-09-15: отказ LOGIN
с чистого скрипта — это rate-limit Exchange (после серии быстрых попыток сервер
молчит timeout), а не TLS-fingerprinting. Работает простой `LOGIN` из
`imap_connect()` с re-try (экспоненциальная пауза). Подробности — в design.md.
- Два постоянных процесса = чуть больше памяти/CPU, но для одной учётки это копейки.
- **Наблюдаемость:** авторизация и метрики (доступность сервера, успешность,
активная сессия) логируются в `/opt/hermes/email/logs/imap_client.log`
(`imap_log()`/`imap_metrics()`/`imap_session` в `scripts/imap_client.py`).
Полноценный Prometheus/статус-эндпоинт для stream-сервиса — позднее (tasks.md).
- **Документация:** README/STATUS/WALKTHROUGH — новые сервисы, порты (нет новых внешних),
как перезапускать, как смотреть журнал изменений.
## Rollback
1. Остановить systemd units (`systemctl --user stop email-imap-stream email-change-analyzer`).
2. Вернуть Hermes cron `mail-archive-every-5min` (он никуда не делся — просто выключен).
3. Удалить новую таблицу/базу (online-копия) — она производная, письма в `/opt/hermes/email/`
не затрагиваются.
4. Существующие скрипты (`mail_archive.py`, классификатор, обработчики) не меняют поведение
при отказе от сервисов — они работают как раньше.