mirror of
https://gitverse.ru/kpa39l/email-assistant.git
synced 2026-09-29 21:25:10 +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
81 lines
5.0 KiB
Markdown
81 lines
5.0 KiB
Markdown
# 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; отдельная задача при необходимости). |