mirror of
https://gitverse.ru/kpa39l/email-assistant.git
synced 2026-09-29 09:15:09 +00:00
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:
@@ -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`, классификатор, обработчики) не меняют поведение
|
||||
при отказе от сервисов — они работают как раньше.
|
||||
Reference in New Issue
Block a user