mirror of
https://gitverse.ru/kpa39l/email-assistant.git
synced 2026-09-29 21:25:10 +00:00
340 lines
33 KiB
Markdown
340 lines
33 KiB
Markdown
# Email Assistant — локальный архив и ассистент почты
|
||
|
||
**Дата:** 2026-09-15
|
||
**Фаза:** 1.5–1.7 + Портфель веб-UI (планирование) + Классификация/обработчики (в работе) + IMAP Realtime Sync (планирование)
|
||
|
||
**Стек:** Himalaya CLI → Python → SQLite → Ollama (Qwen3:8b) → Radicale (CalDAV) → Telegram
|
||
|
||
---
|
||
|
||
## Архитектура проекта
|
||
|
||
```
|
||
/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: Рефакторинг формата хранения ✅
|
||
- [x] Перейти с meta.json + body.md на один `email.md` с YAML-frontmatter
|
||
- [x] Полные заголовки в frontmatter (Message-ID, References, In-Reply-To, CC, Content-Type)
|
||
- [x] State файлы в `/opt/hermes/email/state/`
|
||
|
||
### Фаза 1: Локальный архив ✅
|
||
- [x] Структура `/opt/hermes/email-assistant/`
|
||
- [x] Himalaya (IMAP mail.corpoffice.tech:143 STARTTLS)
|
||
- [x] `mail_archive.py` — инкрементальный архиватор
|
||
- [x] Первый запуск: INBOX 585, Sent 515, Отправленные 510, Archive 373
|
||
- [x] systemd user timer + Hermes cron (every 5m)
|
||
|
||
### Фаза 1.5: Индексация, поиск и дайджесты ✅⬜
|
||
- [x] `mail_index.py` — SQLite-индекс всех email.md (FTS5 + трекинг контактов)
|
||
- [x] `sqlite_search.py` — CLI-поиск по FTS5 (поддержка фильтров from:/subject:/folder)
|
||
- [x] `digest.py` — еженедельный дайджест через LLM (Qwen3:8b)
|
||
- [ ] Поставить cron на `mail_index.py --incremental` (раз в 5-10 мин)
|
||
- [ ] Поставить cron на `digest.py` (раз в неделю)
|
||
|
||
### Фаза 1.7: Динамическое обнаружение всех подпапок INBOX ✅
|
||
- [x] `mail_archive.py` — список вложенных папок INBOX захардкожен (18 шт.), но на сервере их **137** (включая многоуровневые: INBOX/!Персонал/ОТ и ТБ, INBOX/Бюджет/Винный город/CAPEX 2025, INBOX/Контрагенты/iiko/Тихая гавань и т.д.)
|
||
- [x] `--all` сейчас использует тот же хардкод — не архивирует ~120 подпапок
|
||
- [x] Требуется: динамическое обнаружение IMAP-папок через `himalaya folder list`, рекурсивный обход всех подпапок INBOX (любой глубины), автоматическая архивация новых подпапок при их создании
|
||
- [x] `mail-archive-every-5min` cron должен обновлять список папок динамически, а не из хардкода
|
||
- [x] **Реализовано (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) ✅⬜
|
||
- [x] `contacts_extractor.py` — извлечение контактов из подписей через LLM
|
||
- [x] clean_body — удаление цитируемой переписки (Outlook/forwards/>)
|
||
- [x] SQLite-трекинг обработанных писем (contacts_extracted / contacts_skipped)
|
||
- [x] Инкрементальное сохранение каждые 5 писем
|
||
- [x] `--limit N` для дозированной обработки
|
||
- [x] vCard 4.0 генерация
|
||
- [x] Cron already set: `contacts-extractor-every-30m` (--limit 15)
|
||
- [ ] Проверить качество извлечения: сейчас 5 контактов найдено, 5 skipped
|
||
- [ ] Доделать парсинг темы письма (некоторые темы содержат вшитые заголовки)
|
||
|
||
### Фаза 2: Векторизация и поиск ⬜
|
||
- [ ] Выбор векторизатора (bge-m3 через Ollama — уже есть в Memory OS)
|
||
- [ ] Индексация body в Qdrant
|
||
- [ ] Поиск по письмам через агента
|
||
|
||
### Фаза 3: Граф знаний ⬜
|
||
- [ ] Извлечение связанных сущностей (отправители, темы, проекты)
|
||
|
||
---
|
||
|
||
## Портфель: Веб-интерфейс + Календарь/Задачи (2026-09-11)
|
||
|
||
План: `PLAN_WEBUI.md`. Порядок: Задача 4 → 2 → 3 → 1 → 6 → 7.
|
||
|
||
### Задача 4: Анализ ФС vs Maildir ✅ (закрыта 2026-09-11)
|
||
- [x] `STORAGE_ANALYSIS.md` — сравнение email.md/Maildir/MBOX/notmuch по 7 критериям
|
||
- [x] Рекомендация: **остаться на email.md** + добавить `tags: []` в frontmatter + опц. экспорт в Maildir
|
||
- [x] Ссылка в README; change `email-storage-analysis` заархивирован (openspec-lab)
|
||
- [x] Факты: 4884 письма, 76 МБ, 81 контакт
|
||
|
||
### Задача 2: Локальный календарь + трекер задач 🔵 (в работе)
|
||
- [x] Решение пользователя: **Radicale (CalDAV) + Vikunja (трекер)**, всё в Docker-контейнерах
|
||
- [x] **РЕШЕНИЕ 2026-09-13: Vikunja — ЛИШНЯЯ СУЩНОСТЬ, задачи через Radicale VTODO** (change `remove-vikunja-use-radicale-tasks`). Radicale из коробки умеет VTODO (календарь «Задачи»), jtx board читает их по CalDAV. Vikunja выводится из эксплуатации.
|
||
- [x] Change `local-calendar-tasks` создан и валиден — proposal/specs/design/tasks (Radicale-часть актуальна, Vikunja-часть — SUPERSEDED)<br>
|
||
- [x] **Radicale развёрнут и РАБОТАЕТ (2026-09-13)**: контейнер на :5232, PROPFIND 207 с паролем / 401 без. Коллекции Личный/Рабочий/Задачи на ФС.
|
||
- [x] **CardDAV-синк контактов (2026-09-13, change `contacts-caldav-server`)**: `scripts/contacts_caldav_sync.py` — двусторонний sync. PUSH: 81 контакт → vCard в Radicale. PULL: правки/создание/удаление карточек с телефона → contacts.json. Идемпотентно (162 unchanged, 0 PUT на повторе). Конфликты (412) — приоритет телефону, локальная версия в `caldav-sync.log`. Подробнее: `scripts/contacts_caldav_sync.py --help`, лог `/opt/hermes/email/contacts/caldav-sync.log`.
|
||
- [x] **Vikunja ВЫВЕДЕНА ИЗ ЭКСПЛУАТАЦИИ (2026-09-13)**: контейнеры vikunja + vikunja-db удалены (`docker compose down -v`), каталог `/opt/hermes/email-assistant/vikunja/` удалён, порт 3456 свободен. Tasks.nixg.ru закомментирован в Caddy (строки 114-120), Caddy перезагружен (бэкап Caddyfile.bak-vikunja-removed). |
|
||
- [x] **Caddy reverse proxy (cal.nixg.ru → 5232)** — РАБОТАЕТ (2026-09-13): PROPFIND 207 снаружи. tasks.nixg.ru закомментирован.
|
||
- [x] **«Обход в Глории»** — повторяющееся событие (Рабочий, VTIMEZONE Europe/Moscow, RRULE WEEKLY BYDAY=TU 11:00), подтверждено на телефоне (GMT+3 ✓)
|
||
|
||
### Задача 3: Нативная синхронизация с Android 🔵 (в работе)
|
||
- [x] **Контакты синхронизированы** (DAVx5 → Radicale «Контакты»; CardDAV-sync двусторонний, change `contacts-caldav-server`)
|
||
- [x] **Caddy reverse proxy (cal.nixg.ru → 5232)** — работает, PROPFIND 207 снаружи
|
||
- [x] **«Обход в Глории»** — VEVENT подтверждён на телефоне (GMT+3 ✓)
|
||
- [ ] **Проверить появление событий/задач в приложении** (тестовый VEVENT obhod-v-glorii-2026.ics в «Рабочий», тестовый VTODO test-vikunja-removal-2026 в «Задачи») — контакты синхронизируются, события/задачи на телефоне пока не проверены
|
||
- [ ] Двусторонняя синхронизация: событие/задача с телефона → bigbox → база
|
||
|
||
### Задача 8: Классификация писем и обработчики 🔵 (в работе, change `email-classification-handlers`)
|
||
- [x] **Вложения**: фикс бага `himalaya --dir` → `--downloads-dir`; вложения в `<msg_dir>/attachments/`; идемпотентно (2026-09-13 вечер, проверено на живом письме)
|
||
- [x] **СРОЧНЫЙ ПАТЧ (2026-09-14): архивация НЕ помечает письма «прочитанными»** (change `no-mark-seen-on-archive`): himalaya `message read`/`attachment download` используют IMAP `BODY[]`, что выставляет `\Seen` (RFC 3501; сервер — Microsoft Exchange). Исправлено: чтение — `himalaya message read --preview` (не ставит Seen); вложения — новая `fetch_attachments_imaplib()` (сырой IMAP stdlib socket+ssl, `UID FETCH ... (BODY.PEEK[])`, папки в modified UTF-7, литералы до 1.5 МБ, декодирование MIME-encoded слов, фолбэк himalaya + flag remove seen). Проверено живьём (UID 14200: флаги `()` → `()` при скачивании 1.1 МБ docx; кириллическая папка «Организация работы» — 4 docx, флаги не тронуты; UID 320 — без вложений, флаги не меняются)
|
||
- [x] **Бэкфилл вложений** (`--attachments-backfill`): докачка вложений для писем, заархивированных до фикса (пустой `attachments/` при `has_attachment: true`); корректно разбирает вложенные папки (`INBOX/!Битрикс`); письма, уже недоступные на IMAP, помечаются «пусто» без ошибки (2026-09-14, проверено живьём)
|
||
- [x] **Классификатор**: `scripts/email_classifier.py` — Qwen3:8b (Ollama localhost:11434) → теги info/urgent/task/meeting + `classification`/`classification_reason` в frontmatter; идемпотентно; прогон прошёл (письмо 422 → task,meeting)
|
||
- [x] **Обработчики**: `scripts/email_handlers.py` — urgent→Telegram (Bot API+SOCKS5), task→Radicale VTODO («Задачи»), meeting→Radicale VEVENT («Рабочий»), info→ничего; идемпотентно через `handled_*`
|
||
- [x] **Фикс секретов (2026-09-14)**: скрипт теперь сам читает `radicale/.env` (RADICALE_PASS) и `/opt/vesti/.env` (VESTI_BOT_TOKEN) — раньше без ручного export был 401; добавлен stdlib-парсер .env (python-dotenv в системе нет)
|
||
- [x] **Живой прогон (2026-09-14)**: письмо 2026/422 → VTODO «Задачи» (204) + VEVENT «Рабочий» (204); повтор — идемпотентно (0 дублей)
|
||
- [x] **Cron (2026-09-14)**: `mail-classify-handlers` (6e1e78ceedfd, every 5m) — классификатор (--limit 10) → обработчики; end-to-end проверено: 5 новых «meeting» → 5 VEVENT (201)
|
||
- [x] **Живое urgent-письмо → доставка в Telegram (2026-09-15)**: письмо с id 3216 («RE: Платежи Аврора», Archive/2026/08/3216) помечено `classification: urgent` → `email_handlers.py` с `TELEGRAM_CHAT_ID=281328953` отправил в ЛС (private чат kpa39l) через бота @dedinit_controller_bot — `msg_id=60`, пользователь подтвердил получение; `handled_urgent: true` записан (идемпотентно, повторной отправки нет)
|
||
- [x] **Telegram-секреты**: токен `VESTI_BOT_TOKEN` читается из /opt/vesti/.env; канал-дефолт `@dedinit_vesti` — **внимание**: для ЛС нужно явно `TELEGRAM_CHAT_ID=281328953` (private chat kpa39l); зафиксировать дефолт в ЛС — TODO при настройке stream-сервиса
|
||
|
||
---
|
||
|
||
## Сделано в сессии 2026-09-15 (IMAP realtime sync — планирование + Telegram-уведомления в ЛС)
|
||
|
||
- [x] **Выделена авторизация IMAP в отдельную функцию** — `scripts/imap_client.py` (новый): `imap_connect()` (socket+STARTTLS+LOGIN, re-try 5→60с, креды из config/himalaya-config.toml); `mail_archive.py` переведён на неё (sys.path-фикс для cron); проверено живьём: `OK: connected+LOGIN as e.storozhenko @ mail.corpoffice.tech`
|
||
- [x] **Логирование авторизации + метрики** (в imap_client.py): JSON-лог `/opt/hermes/email/logs/imap_client.log` (conn_ok/conn_error/auth_ok/auth_failed/session_started/session_ended); `imap_metrics()` (auth_success_rate, conn_ok/err, sessions_active); контекст-менеджер `imap_session`; `--metrics` CLI
|
||
- [x] **Telegram-уведомления о важных письмах — в ЛС** (kpa39l): создан `/opt/hermes/email-assistant/.env` с `TELEGRAM_CHAT_ID=281328953`; живой тест: письмо 3216 («RE: Платежи Аврора») помечено urgent → доставка в private chat 281328953 (bot @dedinit_controller_bot, msg_id=60), пользователь подтвердил
|
||
- [x] **Найден и исправлен баг доставки**: крон-скрипт `mail-classify-handlers.sh` использовал системный `python3` без httpx → все Telegram-уведомления молча падали (накопилось 10 urgent-писем). Фикс: `PY=/opt/vesti/.venv/bin/python` + `--limit 2` (дозированная отправка backlog, анти-спам)
|
||
- [x] **Формат даты в уведомлениях**: добавлены `format_date_for_tg()` (ISO → `02.09.2026 10:49`) и `_unquote_yaml()` (снятие YAML-кавычек в parse_email_md); строка `📅 ДД.ММ.ГГГГ ЧЧ:ММ` в handle_urgent
|
||
- [x] **IMAP realtime sync (проект)**: proposal.md + design.md + tasks.md + spec-дельты (imap-realtime-sync, email-attachments, email-classification) — openspec `change 'imap-realtime-sync' is valid`; диагноз rate-limit (не TLS-fingerprinting); 2 микросервиса (imap_stream.py IDLE + change_analyzer.py ChangeLog SQLite), systemd, soft-delete
|
||
- [x] **Backlog urgent-писем**: 10 писем в INBOX с `classification: urgent` без `handled_urgent` (июль 2026 — сентябрь 2026, включая срочное «переоформление договоров… отключение интернета до 20-го») — отправляются по 2 за крон-прогон в ЛС
|
||
- [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)
|
||
|
||
### Задача 1: Веб-интерфейс ассистента ⬜
|
||
- [ ] FastAPI + SQLite FTS5: список писем (дата/адресант/тэги/папка)
|
||
- [ ] Перемещение в папку; тэги
|
||
- [ ] Кнопка «Создать задачу» → Radicale VTODO (Задача 6, вместо Vikunja API)
|
||
- [ ] Страница авторизации (Задача 7)
|
||
|
||
---
|
||
|
||
## Решения и проблемы скриптов
|
||
|
||
### `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` (валидный сертификат)
|
||
- **Seen-фикс (2026-09-14):** himalaya `message read` и `attachment download` ставят `\Seen` (BODY[]). Чтение тела: `message read --preview` (не ставит). Вложения: `fetch_attachments_imaplib()` — сырой IMAP (socket+ssl, stdlib), `UID FETCH (BODY.PEEK[])`, имя папки в modified UTF-7 (`_imap_utf7_encode()`, кириллица), чтение литерала чанками по 64 КБ (письма до 1.5 МБ), имя файла через `email.header.decode_header` (MIME-encoded word `=?koi8-r?B?...?=`). Фолбэк: himalaya `attachment download` + `flag remove <uid> seen` (страховка).
|
||
|
||
### `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 не только для извлечения, но и для нахождения подписи
|
||
|
||
---
|
||
|
||
## Текущие метрики
|
||
|
||
| Папка | Писем (2026-09-11) |
|
||
|-------|---------------------|
|
||
| INBOX (вкл. подпапки) | 2674 |
|
||
| Archive | 876 |
|
||
| Отправленные | 790 |
|
||
| Sent | 544 |
|
||
| **Всего** | **4884** (76 МБ, mail_index.db 5.4 MB) |
|
||
|
||
Контакты: **81** в contacts.json (не «5» — устарело; проверено 2026-09-11).
|
||
|
||
---
|
||
|
||
## Cron-задачи (Hermes)
|
||
|
||
| ID | Имя | Расписание | Тип | Статус |
|
||
|----|-----|-----------|-----|--------|
|
||
| 5f2305b2bbf8 | mail-archive-every-5min | every 5m | no-agent (скрипт) | ✅ (Фаза 1.7: использует `--all --drain` с динамическим списком) |
|
||
| ea0fd1ab4f93 | contacts-extractor-every-30m | every 30m | скрипт (--limit 15) | ✅ |
|
||
| 6e1e78ceedfd | mail-classify-handlers | every 5m | скрипт (classifier --limit 10 → handlers) | ✅ (2026-09-14) |
|
||
| — | mail-index-incremental | not set | — | ❌ |
|
||
| — | digest-weekly | not set | — | ❌ |
|
||
|
||
---
|
||
|
||
## DAVx⁵ (Android: CalDAV/CardDAV-мост)
|
||
|
||
**Назначение:** DAVx⁵ — приложение-синхронизатор для Android, **не имеет собственного UI** для просмотра событий/контактов, а встраивается в стандартные системные приложения Android (Календарь, Контакты). Это стандарт де-факто для синхронизации с Radicale на Android.
|
||
|
||
- **Где взять:** F-Droid (бесплатно) или Google Play (платно, поддержка разработчиков).
|
||
- **Как работает:** добавляете аккаунт (URL сервера Radicale, логин, пароль) → DAVx⁵ сам находит доступные календари и адресные книги → выбираете, что синхронизировать с системой.
|
||
- **Для задач** пользователь поставил **jtx board** (не DAVx⁵).
|
||
|
||
### Настройка Caddy для Radicale
|
||
|
||
Проксирование Radicale через Caddy требует внимания к путям и заголовкам, иначе CalDAV/CardDAV-клиенты не найдут ресурсы.
|
||
|
||
**Рабочий пример для домена `dav.example.com` (Radicale в корне):**
|
||
```caddyfile
|
||
dav.example.com {
|
||
# Важно: сохраняем заголовок Authorization для Radicale
|
||
header_up Authorization {header.Authorization}
|
||
|
||
# Если Radicale в подпапке — используйте handle_path (см. ниже)
|
||
reverse_proxy localhost:5232
|
||
}
|
||
```
|
||
|
||
**Ключевые моменты:**
|
||
1. **Сохранение Authorization:** Caddy по умолчанию может удалять заголовки. `header_up Authorization {header.Authorization}` гарантирует, что Radicale получит логин/пароль.
|
||
2. **X-Script-Name (если в подпапке):** для размещения по `/radicale` нужен `handle_path` для удаления префикса пути + заголовок `X-Script-Name`, чтобы Radicale знал о своём расположении.
|
||
3. **Обязательный `handle_path` для подпапки:** простой `reverse_proxy` внутри `handle` может не сработать — Radicale ожидает запросы без префикса (получает его через `X-Script-Name`).
|
||
|
||
**Пример для подпапки `/radicale`:**
|
||
```caddyfile
|
||
dav.example.com {
|
||
handle_path /radicale/* {
|
||
header_up Authorization {header.Authorization}
|
||
header_up X-Script-Name /radicale
|
||
reverse_proxy localhost:5232
|
||
}
|
||
}
|
||
```
|
||
|
||
**В DAVx⁵ указывается базовый URL** (например, `https://dav.example.com` или `https://dav.example.com/radicale`); пути к календарям/контактам приложение определяет автоматически.
|
||
|
||
**Наш случай (следующая сессия):** поддомен `cal.nixg.ru` (Caddy на vps02, `reverse_proxy 10.8.0.2:5232` к bigbox) — Radicale слушает 127.0.0.1:5232, user `estorozhenko`, пароль `RADICALE_PASS` в `radicale/.env`. Адресная книга: `Контакты` (кириллица в URL — DAVx5 умеет). **Задачи: Radicale VTODO (календарь «Задачи») → jtx board** (Vikunja выведена, change `remove-vikunja-use-radicale-tasks`).
|
||
|
||
---
|
||
|
||
## Конфигурация
|
||
|
||
- **Репозиторий (источник истины):** `https://gitverse.ru/kpa39l/email-assistant` (private, remote `gitverse`, https с токеном из `/opt/hermes/.hermes/secrets/git-tokens.env`)
|
||
- **Gitea (зеркало):** `https://gitea.nixg.ru/estorozhenko/email-assistant` (pull mirror ← gitverse, интервал 12h, id 54). Старый `hermes/email-assistant` — обычный репо, не зеркало.
|
||
- **AGENT.md** — политика репозитория/токенов (см. файл)
|
||
- ~~Push mirror gitea → gitverse~~ ✅ **сделано 2026-09-15** (pull mirror наоборот: gitverse → gitea)
|
||
|
||
### Ресурсы проекта (расположение и доступ)
|
||
|
||
| Ресурс | Где живёт | Доступ |
|
||
|--------|-----------|--------|
|
||
| **Caddy (reverse proxy)** | vps02 = 87.242.100.206, контейнер `caddy` | SSH: `ssh vps02` (alias в `~/.ssh/config`, User estorozhenko, ключ cloudruVPS). Caddyfile: `/opt/caddy/Caddyfile` (root; правка через `sudo`). Reload: `sudo docker exec caddy caddy reload --config /etc/caddy/Caddyfile` |
|
||
| **Radicale (CalDAV)** | bigbox, контейнер `radicale`, порт 5232 | WGET: `http://127.0.0.1:5232` (с bigbox), наружу: `https://cal.nixg.ru`. Конфиг: `/opt/hermes/email-assistant/radicale/` (compose.yml, .env — пароль `RADICALE_PASS`). Коллекции на ФС: `/opt/hermes/email-assistant/radicale/data/collections/collection-root/estorozhenko/{Личный,Рабочий,Задачи}` |
|
||
| **Vikunja (трекер задач)** | ~~bigbox, compose в `/opt/hermes/email-assistant/vikunja/`~~ | ⛔ **ВЫВЕДЕНА** (change `remove-vikunja-use-radicale-tasks`). Задачи → Radicale VTODO (календарь «Задачи»). |
|
||
| **WG (сеть хостов)** | 10.8.0.0/24 | bigbox = 10.8.0.2 (цель reverse-proxy с vps02), vps01 = .1, vps03 = .3, vps02 = .4 |
|
||
| **SSH-ключи** | `/home/estorozhenko/.ssh/` | vps01_key (vps01), cloudruVPS (vps02), hostkeyVPS (vps03/root) |
|
||
|
||
**Домены (публичный DNS):** cal.nixg.ru → 87.242.100.206 (vps02/Caddy → bigbox Radicale 5232). **tasks.nixg.ru — НЕ используется** (Vikunja выведена).
|
||
|
||
- **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/`
|