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:
2026-09-15 17:48:05 +00:00
parent 65c167acc2
commit defcf53d75
10 changed files with 982 additions and 28 deletions
@@ -0,0 +1,150 @@
# 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`, классификатор, обработчики) не меняют поведение
при отказе от сервисов — они работают как раньше.