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
This commit is contained in:
2026-09-14 08:08:48 +00:00
parent 8bff6f9aa4
commit 39df85b51c
12 changed files with 662 additions and 16 deletions
+53
View File
@@ -45,3 +45,56 @@
#### 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, пароль никуда не выводится