Files
email-assistant/openspec/specs/email-attachments/spec.md
T
hermes 39df85b51c fix: архивация/вложения больше не помечают письма прочитанными (Seen)
Причина: himalaya message read и attachment download используют IMAP BODY[],
который по RFC 3501 выставляет \Seen на сервере (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 words, фолбэк himalaya + flag remove seen

Проверено живьём: UID 14200 (INBOX, docx 1.1МБ) флаги ()->() — Seen не выставлен.
Openspec: change no-mark-seen-on-archive заархивирован (2026-09-14-no-mark-seen-on-archive), 7/7 validate OK
2026-09-14 08:08:48 +00:00

6.7 KiB

email-attachments Specification

Purpose

Скачивание вложений письма в каталог этого письма. Сейчас mail_archive.py вызывает himalaya attachment download --dir, но правильный флаг в Himalaya — --downloads-dir, из-за чего команда падает (exit 2), ошибка молча глотается except: pass, и папка attachments/ всегда пустая. Вложения теряются.

Requirements

Requirement: Вложения сохраняются в каталог письма

Для каждого письма с вложениями (флаг has_attachment: true в frontmatter) вложения MUST быть сохранены в подкаталог attachments/ каталога письма (/opt/hermes/email/<folder>/YYYY/MM/<uid>/attachments/).

Scenario: Письмо с вложением архивировано

  • WHEN mail_archive.py заархивировал письмо с has_attachment: true
  • THEN файлы вложений лежат в <msg_dir>/attachments/ и совпадают с вложениями на IMAP-сервере

Requirement: Правильный флаг Himalaya

Скачивание вложений MUST использовать флаг --downloads-dir (а не несуществующий --dir) команды himalaya attachment download, и передавать ему каталог письма.

Scenario: Вызов himalaya с корректным флагом

  • WHEN get_attachments() выполняется для письма
  • THEN используется himalaya attachment download --folder <folder> --downloads-dir <msg_dir>/attachments <uid>, exit code 0 при успехе

Requirement: Учёт отсутствия вложений

Если письмо не имеет вложений (has_attachment: false или команда вернула «нет вложений»), скрипт MUST NOT создавать пустую папку attachments/ и MUST NOT считать это ошибкой.

Scenario: Письмо без вложений

  • WHEN mail_archive.py обрабатывает письмо без вложений
  • THEN каталог attachments/ не создаётся, ошибка не логируется

Requirement: Повторная обработка существующих писем

Повторный запуск mail_archive.py MUST NOT повторно качать уже сохранённые вложения (проверка по наличию каталога/файлов).

Scenario: Повторный запуск

  • WHEN mail_archive.py запущен повторно на письме с уже скачанными вложениями
  • THEN вложения не скачиваются повторно (идемпотентность)

Requirement: Бэкфилл вложений для ранее заархивированных писем

Письма, заархивированные до внедрения --downloads-dir (пустой attachments/ при has_attachment: true), MUST поддерживать докачку вложений через флаг --attachments-backfill. Бэкфилл MUST NOT трогать письма, где вложения уже скачаны, и MUST корректно определять IMAP-папку из пути (/<folder>/YYYY/MM/<uid>/, папка может быть вложенной, например INBOX/!Битрикс).

Scenario: Запуск бэкфилла

  • WHEN mail_archive.py --attachments-backfill запущен на архиве с письмами, у которых has_attachment: true, но пустой attachments/
  • THEN вложения скачиваются в эти папки; письма с уже скачанными вложениями пропускаются

Scenario: Нестандартная структура пути

  • WHEN бэкфилл встречает путь, не соответствующий /<folder>/YYYY/MM/<uid>/email.md
  • THEN письмо пропускается без ошибки

Requirement: Скачивание вложений не помечает письмо прочитанным

Скачивание вложений MUST NOT выставлять IMAP-флаг \Seen (письмо не должно становиться «прочитанным» в почтовом ящике).

Способ: himalaya attachment download ставит \Seen (использует BODY[]), и флага --preview у него нет. Поэтому get_attachments() MUST использовать сырой IMAP-запрос BODY.PEEK[] через stdlib imaplib (не ставит \Seen на Microsoft Exchange, проверено) и распаковку MIME через stdlib email.

Scenario: Скачивание вложения у непрочитанного письма

  • GIVEN письмо в INBOX с флагами () (непрочитанное)
  • WHEN get_attachments() скачивает его вложения
  • THEN файлы вложений сохранены в attachments/, а флаги письма на IMAP остаются () (флаг \Seen не выставлен)

Scenario: Фолбэк при сбое сырого IMAP

  • WHEN fetch_attachments_imaplib() не может получить письмо (ошибка IMAP)
  • THEN вложения скачиваются через himalaya attachment download, после чего флаг \Seen снимается через himalaya flag remove (письмо временно помечается, но восстанавливается) ИЛИ операция помечается как недоступная — письмо НЕ остаётся прочитанным навсегда

Requirement: Пароль IMAP для скачивания вложений

fetch_attachments_imaplib() MUST брать учётные данные IMAP (host, port, login, пароль) из конфига himalaya (~/.config/himalaya/config.toml, секция [accounts.<default>], backend.*, пароль — backend.auth.raw) и MUST NOT логировать или выводить пароль.

Scenario: Доступ к конфигу

  • WHEN get_attachments() запускается для письма
  • THEN подключение к IMAP выполняется с учётными данными из конфига himalaya, пароль никуда не выводится