14 KiB
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-5mincron должен обновлять список папок динамически, а не из хардкода
Фаза 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 - Himalaya читает envelope (from, to, subject, date, message-id) → YAML frontmatter
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 отрезает всё от первого найденного маркера, теряя подпись.
Текущее решение (итерация):
- Ищем самый ранний маркер цитирования среди всех паттернов (не первый совпавший)
- 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 | — | ❌ |
Конфигурация
- Himalaya:
~/.config/himalaya/config.toml - Аккаунт:
vinogorod, IMAPmail.corpoffice.tech:143(STARTTLS) - Почта:
e.storozhenko@vinogorod.ru - LLM: Qwen3:8b (Ollama localhost:11434)
- SMTP: не настроен
- Файлы state:
/opt/hermes/email/state/