diff --git a/STATUS.md b/STATUS.md index 74d87e5..449023c 100644 --- a/STATUS.md +++ b/STATUS.md @@ -157,6 +157,30 @@ Hermes cron: - [x] **Репозиторий переведён на gitverse (источник истины)**: создан приватный `kpa39l/email-assistant` на gitverse.ru (id 336792) через API; remote `gitverse` добавлен; master запушен; **gitea: pull mirror `estorozhenko/email-assistant` ← gitverse (12h, id 54)** — gitea.nixg.ru остаётся зеркалом, не источником - [x] **AGENT.md создан** — политика репозитория: gitverse = истина, gitea = зеркало, команды пуша, **местоположение токенов** (git-tokens.env, VESTI_BOT_TOKEN, TELEGRAM_CHAT_ID) +## Сделано в сессии 2026-09-15 (вечер) — imap_stream.py реализован + backlog urgent закрыт + +### imap_stream.py — Фаза 1 (imap-realtime-sync, задачи 3–5) ✅ +- [x] **`scripts/imap_stream.py` (новый, ~690 строк)** — постоянный IMAP IDLE-поток на сыром socket поверх `imap_client.imap_connect` (aioimaplib не ставится): + - IDLE-цикл: `idle_start` → `idle_wait(25с)` → `idle_done` → перевыпуск (Exchange рвёт IDLE ~60с, поэтому IDLE_TIMEOUT=25с) + - Reconnect с паузой `CONNECT_PAUSE=45с` (rate-limit Exchange), цикл переживает обрыв (проверено live: 150с демон → обрыв → reconnect → SELECT) + - `reconcile_new` — новые UID (архивация через mail_archive) + `reconcile_full` каждые 5 циклов (флаги/удаления) + - SQLite `/opt/hermes/email/state/mailbox.db`: `mailbox_state` (uid, folder, message_id, in_reply_to, refs, flags, has_attachment, archive_path, last_seen, deleted) + `mailbox_events` + - CLI: `--check`, `--test-idle`, `--status`, `--metrics`, без аргументов — демон +- [x] **Live-тесты Exchange (пауза ≥45с между коннектами) — все прошли**: `--check` (LOGIN 365 писем), `--test-idle` (SELECT → reconcile → IDLE 30с → 0 событий → DONE, EXIT=0), демон 150с (обрыв+reconnect) +- [x] **Исправлено live-тестами (питфолы, детали в WALKTHROUGH):** HERMES_REAL_HOME в imap_client (_load_credentials), `_cmd_ok` (хвост \r\n), колонка `references`→`refs` (SQLite), **UID-батчинг `1:1000` пуст** → `UID SEARCH ALL`/`UID :*` (иначе все письма ложно deleted), Exchange не шлёт `+ idling` → `+ IDLE accepted` +- [x] **Запушено в gitverse**: `5a81c86` (imap_stream + фикс токена TG + фильтр актуальности + HERMES_REAL_HOME) + +### Backlog urgent (10 писем) — закрыт ✅ +- [x] **Корень найден**: `email_handlers.py` при наличии python-dotenv шёл в ветку `if load_dotenv` и НЕ грузил `/opt/vesti/.env` → `VESTI_BOT_TOKEN` пуст → все уведомления падали. Фикс: vesti/.env грузится всегда +- [x] **5 сентябрьских писем доставлены в ЛС** (msg_id 62–64 и далее), помечены handled_urgent +- [x] **Фильтр актуальности (запрос пользователя)**: `URGENT_MAX_AGE_DAYS=3` + `is_urgent_recent()` — письма старше 3 дней НЕ шлются в Telegram (помечаются handled_urgent); 5 старых (июль/авг) пропущены корректно +- [x] Идемпотентность сохранена: дубли предотвращены (14229 помечен вручную) + +### Открыто на следующую сессию +- [ ] Запуск imap_stream.py как постоянного сервиса (systemd/cron) — код готов, не запущен навсегда +- [ ] Баг Radicale: `[task] PUT 400: Bad Request` (VTODO-задача, не относится к urgent) — не разобран +- [ ] Полный reconcile по всем подпапкам INBOX (сейчас Фаза 1 — только INBOX) + ### Задача 1: Веб-интерфейс ассистента ⬜ - [ ] FastAPI + SQLite FTS5: список писем (дата/адресант/тэги/папка) - [ ] Перемещение в папку; тэги diff --git a/TODO.md b/TODO.md index 9500b27..3f85252 100644 --- a/TODO.md +++ b/TODO.md @@ -59,8 +59,10 @@ | 2026-09-15 | Telegram-уведомления о важных письмах в ЛС (письмо 3216 → ЛС kpa39l, user подтвердил) | ✅ закрыта | `.env` проект (TELEGRAM_CHAT_ID=281328953) | | 2026-09-15 | Баг: крон mail-classify-handlers использовал python3 без httpx → уведомления не уходили (10 urgent-писем в backlog). Фикс: /opt/vesti/.venv/bin/python + --limit 2 | ✅ закрыта | scripts/mail-classify-handlers.sh | | 2026-09-15 | Формат даты в уведомлениях: `02.09.2026 10:49` (format_date_for_tg) + снятие YAML-кавычек (_unquote_yaml) | ✅ закрыта | scripts/email_handlers.py | -| 2026-09-15 | imap_stream.py (IDLE-цикл) — скелет + выбор библиотеки (aioimaplib не установлен; свой клиент поверх imap_connect) | 🔵 открыта | openspec/changes/imap-realtime-sync/tasks.md | -| 2026-09-15 | Backlog 10 urgent-писем (июль-сентябрь) — отправляются по 2/прогон крона в ЛС; часть просрочена (решить, пропускать ли) | 🔵 открыта | STATUS.md §2026-09-15 | +| 2026-09-15 | imap_stream.py (IDLE-цикл) — РЕАЛИЗОВАН (Фаза 1, задачи 3–5): IDLE-цикл с reconnect (45с), reconcile new/full, SQLite mailbox.db (mailbox_state, mailbox_events); --check/--test-idle/--status/демон | ✅ закрыта | @session:default/20260915_174925_ec1ca4; git 5a81c86 | +| 2026-09-15 | Backlog urgent: корень найден (vesti/.env не грузился при python-dotenv) → 5 сентябрьских доставлены; фильтр актуальности URGENT_MAX_AGE_DAYS=3 (старые не шлются) | ✅ закрыта | @session:default/20260915_174925_ec1ca4; git 5a81c86 | +| 2026-09-15 | Фильтр неактуальных уведомлений (запрос пользователя): is_urgent_recent() — письмо старше 3 дн не шлётся, помечается handled_urgent | ✅ закрыта | scripts/email_handlers.py; git 5a81c86 | +| 2026-09-15 | Live-тесты Exchange с паузой ≥45с — все прошли (--check, --test-idle 30с, демон 150с пережил обрыв+reconnect) | ✅ закрыта | @session:default/20260915_174925_ec1ca4 | | 2026-09-15 | Репозиторий: gitverse = источник истины (создан kpa39l/email-assistant, remote gitverse, master запушен) | ✅ закрыта | AGENT.md, STATUS.md | | 2026-09-15 | Gitea pull mirror: estorozhenko/email-assistant ← gitverse (12h, id 54) | ✅ закрыта | AGENT.md, STATUS.md | | 2026-09-15 | AGENT.md: добавить местоположение токенов (git-tokens.env, VESTI_BOT_TOKEN, TELEGRAM_CHAT_ID) | ✅ закрыта | AGENT.md | diff --git a/WALKTHROUGH.md b/WALKTHROUGH.md index b989507..586e6df 100644 --- a/WALKTHROUGH.md +++ b/WALKTHROUGH.md @@ -2,6 +2,83 @@ Воспроизводимость: хронология, команды, решения, ошибки и как чинили. +## 2026-09-15 (вечер, сессия @session:default/20260915_174925_ec1ca4) + +### imap_stream.py — Фаза 1 (IDLE-цикл) реализован + +**Цель:** постоянный IMAP-поток (задачи 3–5 change imap-realtime-sync). aioimaplib +не ставится → свой клиент на сыром socket поверх `imap_client.imap_connect`. + +**Архитектура `scripts/imap_stream.py`:** +- `ImapStream` — обёртка над сокетом: теги, `_cmd`, `_cmd_ok`, чтение до тега. +- IDLE-цикл: `idle_start` → `idle_wait(25с)` → `idle_done` → перевыпуск. +- Reconnect: при ошибке `CONNECT_PAUSE=45с` (rate-limit Exchange), повтор. +- `reconcile_new(folder)` — поиск UID > last_uid, архивация через mail_archive, + запись в `mailbox_state` + событие `mailbox_events`. +- `reconcile_full(folder)` — полное сравнение UID/флагов (каждый 5-й цикл). +- SQLite: `/opt/hermes/email/state/mailbox.db`, таблицы `mailbox_state` (uid, + folder, message_id, in_reply_to, refs, flags, has_attachment, archive_path, + last_seen, deleted) и `mailbox_events` (event/ts/folder/uid/detail). +- CLI: `--check` (авторизация), `--test-idle` (IDLE 30с), `--status`, `--metrics`, + без аргументов — демон. + +**Питфолы Exchange, найденные live-тестами (пауза ≥45с между коннектами):** +1. `_load_credentials` в imap_client.py читал `Path.home()` — в сессии Hermes + HOME≠реальный. Фикс: `HERMES_REAL_HOME` (env) → `/home/estorozhenko`. Cron + уже делает это в mail-archive.sh. +2. `_cmd_ok`: `resp.split(b"\r\n")[-1]` давал пустой элемент (хвост `\r\n`) — + команда считалась failed даже при OK. Фикс: разбор строк до тега. +3. Колонка `references` — зарезервированное слово SQLite → `refs`. +4. **UID-батчинг сломан:** `UID SEARCH UID 1:1000` пуст (UID — глобальный номер + ~14200+, не порядковый); первый пустой батч обрывал цикл, все письма ложно + помечались deleted. Фикс: `UID SEARCH ALL` / `UID SEARCH UID :*` + (проверено: 365 UID). +5. Exchange отвечает на IDLE `+ IDLE accepted, awaiting DONE command.` — + НЕ `+ idling`. Фикс: матч `+ IDLE` или `+ idling`. +6. **Exchange рвёт IDLE-соединение ~60с** (не 30 мин) — `IDLE_TIMEOUT=25с` + (перевыпуск до серверного лимита). Демон переживает обрыв: reconnect 45с. + +**Live-тесты (все с паузой ≥45с):** +- `--check`: connect+LOGIN+SELECT 365 писем → OK. +- `--test-idle`: SELECT → reconcile (0 новых, deleted=0, flags=18) → IDLE 30с → + 0 событий → DONE → EXIT=0. +- Демон 150с: stream_started → SELECT 365 → обрыв IDLE (~60с) → + reconnect_pause 45с → SELECT 365. Цикл переподключения работает. + +**Git:** 5a81c86 запушен в gitverse (defcf53..5a81c86). Файлы: scripts/imap_stream.py +(новый, ~690 строк), scripts/email_handlers.py (+фильтр актуальности), scripts/imap_client.py +(+HERMES_REAL_HOME). + +### Backlog urgent-pисем: корень найден + фильтр актуальности + +**Проблема:** 10 urgent-писем не уходили в Telegram. Причина: при наличии +python-dotenv `email_handlers.py` шёл в ветку `if load_dotenv` — грузил только +`BASE_DIR/.env` и `radicale/.env`, НО НЕ `/opt/vesti/.env` → `VESTI_BOT_TOKEN` +пуст → все обработки падали. Строка `✗ [urgent] ...: Нет токена Telegram`. + +**Фикс (email_handlers.py):** `/opt/vesti/.env` грузится всегда (до ветвления). + +**Запрос пользователя (mid-turn):** «перестань слать неактуальные срочные +уведомления; встроить проверку на актуальность — сравнивать дату письма и +текущую». Реализовано: +- `URGENT_MAX_AGE_DAYS = 3` — письма старше 3 дней не шлются. +- `is_urgent_recent(headers)` — парсит дату из frontmatter, сравнивает с now. +- Старое письмо: `✓ [urgent] ...: пропущено (актуальность истекла)`, помечается + `handled_urgent` (идемпотентность, не перебирается). + +**Результат backlog:** 5 сентябрьских доставлены в ЛС (msg_id 62–64 и далее), +5 старых (июль/авг) — пропущены фильтром корректно. Январьское «Сервер» — на +деле `classification: info` (не urgent, ошибка подсчёта). 14229 помечен вручную +(уже дважды уведомлён — предотвращён дубль). + +**Известный отдельный баг (не urgent):** `✗ [task] ...: PUT 400: Bad Request` +(Radicale VTODO-задача) — не разобран, открыт на следующую сессию. + +### Cron-заметка + +`mail-classify-handlers` (в venv /opt/vesti/.venv/bin/python, --limit 2/прогон) +работает; после фикса токена обрабатывает по 2 письма за прогон идемпотентно. + ## 2026-09-11 ### Фаза 1.7: динамическое обнаружение подпапок INBOX