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

13 KiB
Raw Blame History

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