Files
email-assistant/STATUS.md
T
hermes c7430d1b8a feat: динамическое обнаружение подпапок INBOX (Фаза 1.7)
- get_inbox_subfolders(): парсит 'himalaya folder list', находит 136
  подпапок INBOX (вместо хардкода 18), fallback на INBOX_SUBFOLDERS
- --all использует FOLDERS + get_inbox_subfolders()
- EXCLUDED_FOLDERS: Trash/Drafts/RSS/Junk/Spam/Archive не архивируются
- ENVELOPE_TIMEOUT=180: INBOX (14k писем) перечисляется >60с
- run_cmd() ловит FileNotFoundError (himalaya вне PATH)
- mail-archive.sh: HOME=/home/estorozhenko, цикл переписан на --all --drain
- STATUS.md: Фаза 1.7 ✅
2026-09-11 11:58:27 +00:00

15 KiB
Raw Blame History

Email Assistant — локальный архив и ассистент почты

Дата: 2026-07-19 Фаза: 1.5 — Индексация, поиск, дайджесты + Адресная книга (в работе)

Стек: Himalaya CLI → Python → SQLite → Ollama (Qwen3:8b) → Yandex Disk


Архитектура проекта

/opt/hermes/email-assistant/
├── STATUS.md                              # этот файл
├── config/
│   ├── himalaya-config.toml              # Himalaya IMAP-конфиг
│   └── contacts-cron.sh                  # обёртка для cron контактов
├── scripts/
│   ├── mail_archive.py                   # инкрементальный архиватор писем с IMAP
│   ├── mail-archive.sh                   # shell-обёртка для systemd/cron
│   ├── migrate_to_email_md.py            # конвертер meta.json→email.md (deprecated)
│   ├── mail_index.py                     # SQLite FTS5-индекс всех писем
│   ├── contacts_extractor.py             # извлечение контактов через LLM
│   ├── sqlite_search.py                  # FTS5-поиск по архиву
│   ├── digest.py                         # еженедельный дайджест почты
│   └── mail_archive.py                   # основной архиватор
│
├── context/
│   ├── CONTEXT.md                        # архитектура и план развития
│   ├── SKILL.md                          # навык для Hermes (email-local-archive)
│   └── CONTACTS.md                       # описание contacts extractor

/opt/hermes/email/                          # архив писем (локальный диск)
├── INBOX/
│   └── YYYY/MM/UID/email.md              # YAML-frontmatter + тело
├── Sent/
├── Отправленные/
├── Archive/
├── state/                                 # mail-archive-last-*.json (last_uid per folder)
├── contacts/                              # адресная книга
│   ├── contacts.json                      # полная база контактов
│   ├── index.json                         # email → contact_id
│   ├── contacts.vcf                       # vCard 4.0 для импорта
│   └── last_scan.json                     # трекинг обработанных
├── digests/                               # еженедельные дайджесты
│   └── digest-YYYY-MM-DD.md
└── mail_index.db                          # SQLite + FTS5 (~5.4 MB)

Hermes cron:
  - mail-archive-every-5min   (no-agent, скрипт)
  - contacts-extractor-every-30m (скрипт, --limit 15)
  - digest: пока не поставлен
  - mail_index --incremental: пока не поставлен

Статус задач

Фаза 0.5: Рефакторинг формата хранения ✅

  • Перейти с meta.json + body.md на один email.md с YAML-frontmatter
  • Полные заголовки в frontmatter (Message-ID, References, In-Reply-To, CC, Content-Type)
  • State файлы в /opt/hermes/email/state/

Фаза 1: Локальный архив ✅

  • Структура /opt/hermes/email-assistant/
  • Himalaya (IMAP mail.corpoffice.tech:143 STARTTLS)
  • mail_archive.py — инкрементальный архиватор
  • Первый запуск: INBOX 585, Sent 515, Отправленные 510, Archive 373
  • systemd user timer + Hermes cron (every 5m)

Фаза 1.5: Индексация, поиск и дайджесты ✅⬜

  • mail_index.py — SQLite-индекс всех email.md (FTS5 + трекинг контактов)
  • sqlite_search.py — CLI-поиск по FTS5 (поддержка фильтров from:/subject:/folder)
  • digest.py — еженедельный дайджест через LLM (Qwen3:8b)
  • Поставить cron на mail_index.py --incremental (раз в 5-10 мин)
  • Поставить cron на digest.py (раз в неделю)

Фаза 1.7: Динамическое обнаружение всех подпапок INBOX ✅

  • mail_archive.py — список вложенных папок INBOX захардкожен (18 шт.), но на сервере их 137 (включая многоуровневые: INBOX/!Персонал/ОТ и ТБ, INBOX/Бюджет/Винный город/CAPEX 2025, INBOX/Контрагенты/iiko/Тихая гавань и т.д.)
  • --all сейчас использует тот же хардкод — не архивирует ~120 подпапок
  • Требуется: динамическое обнаружение IMAP-папок через himalaya folder list, рекурсивный обход всех подпапок INBOX (любой глубины), автоматическая архивация новых подпапок при их создании
  • mail-archive-every-5min cron должен обновлять список папок динамически, а не из хардкода
  • Реализовано (2026-09-11): get_inbox_subfolders() через himalaya folder list — динамически находит 136 подпапок INBOX (глубина до 3), fallback на хардкод при ошибке. --all использует FOLDERS + get_inbox_subfolders(). Проверено: 140 папок в списке, smoke-тест на реальном запуске. Таймаут envelope list поднят до 180с (INBOX 14k писем >60с).

Фаза 1.6: Адресная книга (Contacts Extractor) ✅⬜

  • contacts_extractor.py — извлечение контактов из подписей через LLM
  • clean_body — удаление цитируемой переписки (Outlook/forwards/>)
  • SQLite-трекинг обработанных писем (contacts_extracted / contacts_skipped)
  • Инкрементальное сохранение каждые 5 писем
  • --limit N для дозированной обработки
  • vCard 4.0 генерация
  • Cron already set: contacts-extractor-every-30m (--limit 15)
  • Проверить качество извлечения: сейчас 5 контактов найдено, 5 skipped
  • Доделать парсинг темы письма (некоторые темы содержат вшитые заголовки)

Фаза 2: Векторизация и поиск ⬜

  • Выбор векторизатора (bge-m3 через Ollama — уже есть в Memory OS)
  • Индексация body в Qdrant
  • Поиск по письмам через агента

Фаза 3: Граф знаний ⬜

  • Извлечение связанных сущностей (отправители, темы, проекты)

Решения и проблемы скриптов

mail_archive.py — Инкрементальный архиватор

Задача: Качать письма с IMAP, сохранять в email.md с YAML-frontmatter.

Решение:

  • Для каждой папки хранится last_uid в /opt/hermes/email/state/mail-archive-last-<folder>.json
  • --limit N — максимум писем за проход (по умолчанию 200)
  • --drain — скачивать ВСЮ почту до конца: повторять проходы по каждой папке, пока за проход не обработано 0 писем. Нужен, когда новых писем накопилось больше батча (--limit) — скрипт сам себя повторяет до полного осущения папки, а не оставляет хвост до следующего запуска. Предохранитель от бесконечного цикла (10 000 проходов).
  • himalaya envelope --page-size 500 для быстрой загрузки списка писем
  • Каждое письмо: himalaya get <uid> | email-to-md.py → email.md
  • Инкрементально: добавляет все uid > last_uid, обновляет last_uid
  • Проблема: Himalaya v1.2.0 не поддерживает danger_accept_invalid_certs — используем mail.corpoffice.tech (валидный сертификат)

mail_index.py — SQLite-индекс

Задача: Быстрый полнотекстовый поиск по архиву, трекинг обработки контактов.

Решение:

  • SQLite с FTS5 (unicode61 tokenizer) — 4 таблицы: emails, email_fts, триггеры синхронизации
  • Индексирует: path, uid, folder, date, from, to, subject, body_preview (первые 500 символов)
  • Поля contacts_extracted / contacts_skipped для совместной работы с contacts_extractor
  • Режимы: полная переиндексация (--incremental игнорирует mtime), поиск (--search), статистика (--stats)
  • Инкрементальный режим: проверяет file_mtime — пропускает неизменённые файлы

contacts_extractor.py — Извлечение контактов

Задача: Найти в подписи письма имя, должность, телефон, компанию отправителя.

Решение:

  • Берёт необработанные письма из SQLite (WHERE contacts_extracted=0 AND contacts_skipped=0)
  • clean_body(): удаляет HTML-теги, трекинг-ссылки, цитируемую переписку (Outlook-заголовки От:, From:, Sent:; forwarded; > quotes)
  • Отдаёт очищенный текст Qwen3:8b (Ollama, temperature=0.1)
  • LLM возвращает JSON: full_name, email, phone, position, company, address, raw_signature
  • Дедупликация по email: при повторной встрече обновляет поля
  • Инкрементальное сохранение: каждые 5 писем пишет contacts.json + contacts.vcf
  • Текущая проблема: clean_body может вырезать подпись вместе с цитатами (см. ниже)

sqlite_search.py — FTS5-поиск

Задача: Быстрый поиск по архиву писем из консоли.

Решение:

  • FTS5-запрос к mail_index.db через SQL MATCH
  • Поддержка синтаксиса: "точная фраза", OR, -исключение, префикс*
  • Пользовательские префиксы: from:user@mail, subject:отчёт → LIKE-фильтр в WHERE
  • --folder INBOX/!Отчеты — фильтр по папке
  • --body — включает тело письма в поиск (медленнее, но полнее)
  • Вывод: дата, папка, отправитель, тема, полный путь к файлу

digest.py — Еженедельный дайджест

Задача: Сгенерировать краткое резюме всех писем за N дней для руководителя.

Решение:

  • SQLite-запрос: письма за последние N дней (по дате из frontmatter)
  • Группировка по папкам (INBOX/!ВГ Чек листы → "ВГ Чек листы")
  • Вызов Qwen3:8b с промптом: "напиши краткий дайджест на русском для руководителя"
  • LLM выделяет: общую статистику, темы по папкам, важные отправители
  • Сохраняет в /opt/hermes/email/digests/digest-YYYY-MM-DD.md
  • Режимы: stdout, file, both

Проблема: clean_body вырезает подпись вместе с цитатой

Корень: В письмах с цепочкой ответов (Outlook forwarding) подпись отправителя часто находится после маркера От: Стороженко... Отправлено:..., но до конца цитаты. clean_body отрезает всё от первого найденного маркера, теряя подпись.

Текущее решение (итерация):

  1. Ищем самый ранний маркер цитирования среди всех паттернов (не первый совпавший)
  2. Fallback: если ни один маркер не сработал — ищем От: / From: / Subject: в последних 500 символах

Что ещё можно сделать:

  • Двухпроходная очистка: сначала отрезать цепочки forward-заголовков, потом отделять подпись от тела
  • Определять границу подписи по паттернам С уважением, / Best regards, / — — она ближе к концу
  • Использовать LLM не только для извлечения, но и для нахождения подписи

Текущие метрики

Папка Писем Контакты извл.
INBOX 585 —
INBOX подпапки (18 хардкодных) ~180 —
Неархивируемые подпапки INBOX ~120 папок не синхронизируются —
Sent 515 —
Отправленные 510 —
Archive 373 —
Всего 2073 5

Индекс: 2073 письма, 5.4 MB SQLite. Контакты: 5 найдено (clean_body отрезает подпись в большинстве forwarded-писем).


Cron-задачи (Hermes)

ID Имя Расписание Тип Статус
22c5beb891cc mail-archive-every-5min every 5m no-agent (скрипт) ✅
8e181a988392 contacts-extractor-every-30m every 30m скрипт (--limit 15) ✅
— mail-index-incremental not set — ❌
— digest-weekly not set — ❌

Конфигурация

  • Репозиторий: https://gitea.nixg.ru/hermes/email-assistant

  • Push mirror на gitverse.ru: ❌ не настроен

    • Настроить push mirror из gitea.nixg.ru в gitverse.ru
    • Требуется: создать репозиторий на gitverse.ru, получить токен, настроить mirror в настройках gitea (Settings → Git Hooks/Mirrors → Add Push Mirror)
  • Himalaya: ~/.config/himalaya/config.toml

  • Аккаунт: vinogorod, IMAP mail.corpoffice.tech:143 (STARTTLS)

  • Почта: e.storozhenko@vinogorod.ru

  • LLM: Qwen3:8b (Ollama localhost:11434)

  • SMTP: не настроен

  • Файлы state: /opt/hermes/email/state/