# 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`, классификатор, обработчики) не меняют поведение при отказе от сервисов — они работают как раньше.