Files
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

81 lines
5.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design: не помечать письма прочитанными при архивации
## Контекст
- Сервер: Microsoft Exchange IMAP4 (`mail.corpoffice.tech:143`, STARTTLS).
- himalaya v1.2.0.
- Проверено на живом письме (UID 320, INBOX):
- `himalaya message read` — ставит `\Seen` (документировано; есть флаг `--preview`).
- `himalaya attachment download` — флага против Seen НЕТ (help проверен).
- Raw `FETCH ... BODY.PEEK[]` (сырой IMAP) — НЕ ставит `\Seen` (проверено:
флаги `()` до и после чтения тела 16430 байт).
## Решение
### 1. Чтение тела письма (get_email_content, mail_archive.py)
Было:
```python
HIMALAYA_CMD + ["message", "read", str(uid), "--folder", folder] + header_args
```
Стало:
```python
HIMALAYA_CMD + ["message", "read", str(uid), "--folder", folder, "--preview"] + header_args
```
`--preview` документирован: «Read the message **without** applying the "seen" flag».
### 2. Скачивание вложений (get_attachments, mail_archive.py)
`himalaya attachment download` ставит Seen, а `--preview` у него нет. Обходные
варианты:
**A. Сырой IMAP через stdlib `imaplib` (выбрано).**
Реализовать `fetch_attachments_imaplib(uid, folder, dest_dir)`:
1. Читать `account`/`password` из конфига himalaya (`~/.config/himalaya/config.toml`,
секция `[accounts.<default>]`, поля `backend.*`, пароль — `backend.auth.raw`).
2. `imaplib.IMAP4(host, 143)` + `starttls()` + `login()`.
3. `SELECT folder` (НЕ readonly — у Exchange readonly-режим может помешать
корректному FETCH, проверить; PEEK работает в любом режиме).
4. `UID FETCH <uid> (BODY.PEEK[])` — uid = номер письма **в папке** (как у нас
в структуре архива — он и есть UID, см. ниже).
5. Парсинг `email.message_from_bytes`, сбор частей с `get_filename()` или
`content-disposition: attachment`, запись в `dest_dir`.
Преимущества: убирает himalaya из критического пути (лечит и зависания),
гарантированно не ставит Seen. Недостатки: дублируется логика himalaya
(пароль в конфиге, parsing) — но конфиг-формат стабилен, парсётся stdlib tomllib.
**Б. himalaya + выставление Seen обратно после скачивания.**
`himalaya attachment download`, затем `himalaya flag remove <uid> --folder <folder> --flag seen`.
Минусы: на время скачивания письмо становится прочитанным (мгновенно, но
заметно на стороне Exchange-уведомлений); двойное обращение к IMAP; если
скачивание упадёт — письмо останется Seen.
Выбрано **А** (сырой IMAP): единственный вариант, который вообще не трогает
флаги. При этом `--preview` для тела — совместимость с himalaya-чтением.
### 3. Мелочи
- Оба места правятся в `mail_archive.py`; `email_classifier.py` и
`email_handlers.py` не трогаем (они читают локальные `email.md`, не IMAP).
- Пароль: НЕ логировать, НЕ выводить. Имя переменной — `imap_password`.
- Фолбэк: если `fetch_attachments_imaplib` падает (сервер не отдаёт PEEK) —
fallback на старый `himalaya attachment download` + `flag remove` (вариант Б).
## Проверка
1. `python3 -m py_compile scripts/mail_archive.py`
2. На живом письме UID 320 (непрочитанное, INBOX):
- `himalaya message read 320 --preview` → флаги остаются `()`.
- `fetch_attachments_imaplib(...)` → вложение скачано, флаги остаются `()`.
3. Прогнать `scripts/mail_archive.py --limit 2` на INBOX — флаги у обработанных
писем не меняются (сравнить флаги в frontmatter email.md до/после).
4. Включить cron обратно; наблюдать 1 цикл — новых `Seen` в frontmatter
у свежих писем нет.
## Rollback
1. `git checkout -- scripts/mail_archive.py` (если не закоммичено) или revert коммита.
2. Вернуть `--preview`/`fetch_attachments_imaplib` → исходные вызовы himalaya.
3. Флаги писем, уже помеченных Seen этим багом, НЕ восстанавливаются
(вне scope; отдельная задача при необходимости).