mirror of
https://gitverse.ru/kpa39l/email-assistant.git
synced 2026-09-29 09:15:09 +00:00
39df85b51c
Причина: 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
101 lines
6.7 KiB
Markdown
101 lines
6.7 KiB
Markdown
# 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, пароль никуда не выводится
|