13 KiB
Proposal: Потоковая синхронизация почты (IMAP IDLE) и онлайновая копия ящика
Why
Сейчас mail_archive.py опрашивает IMAP каждые 5 минут по расписанию (Hermes cron).
Это pull-модель с двумя фундаментальными ограничениями:
- Задержка до 5 минут — новые письма попадают в архив и в RAG-индексы не сразу, а в лучшем случае через 5 минут (а с учётом очереди классификатора/обработчиков — дольше).
- Слепота к изменениям, которые делает сам пользователь. Пользователь активно
работает с почтой в клиенте: переносит письма между папками, удаляет, отвечает,
помечает прочитанными. Архиватор знает только
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-handlerscron, но по событиям, а не по расписанию); - журнал изменений = история «что, когда, с каким письмом произошло» — его можно показывать пользователю и использовать в RAG (в отличие от текущей модели, где у нас только статичные снимки).
Новые компоненты
scripts/imap_stream.py— svc 1 (постоянный процесс; systemd unitemail-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 cronmail-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
- Остановить systemd units (
systemctl --user stop email-imap-stream email-change-analyzer). - Вернуть Hermes cron
mail-archive-every-5min(он никуда не делся — просто выключен). - Удалить новую таблицу/базу (online-копия) — она производная, письма в
/opt/hermes/email/не затрагиваются. - Существующие скрипты (
mail_archive.py, классификатор, обработчики) не меняют поведение при отказе от сервисов — они работают как раньше.