Причина: 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
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, пароль никуда не выводится