Причина: 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
5.0 KiB
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)
Было:
HIMALAYA_CMD + ["message", "read", str(uid), "--folder", folder] + header_args
Стало:
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):
- Читать
account/passwordиз конфига himalaya (~/.config/himalaya/config.toml, секция[accounts.<default>], поляbackend.*, пароль —backend.auth.raw). imaplib.IMAP4(host, 143)+starttls()+login().SELECT folder(НЕ readonly — у Exchange readonly-режим может помешать корректному FETCH, проверить; PEEK работает в любом режиме).UID FETCH <uid> (BODY.PEEK[])— uid = номер письма в папке (как у нас в структуре архива — он и есть UID, см. ниже).- Парсинг
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(вариант Б).
Проверка
python3 -m py_compile scripts/mail_archive.py- На живом письме UID 320 (непрочитанное, INBOX):
himalaya message read 320 --preview→ флаги остаются().fetch_attachments_imaplib(...)→ вложение скачано, флаги остаются().
- Прогнать
scripts/mail_archive.py --limit 2на INBOX — флаги у обработанных писем не меняются (сравнить флаги в frontmatter email.md до/после). - Включить cron обратно; наблюдать 1 цикл — новых
Seenв frontmatter у свежих писем нет.
Rollback
git checkout -- scripts/mail_archive.py(если не закоммичено) или revert коммита.- Вернуть
--preview/fetch_attachments_imaplib→ исходные вызовы himalaya. - Флаги писем, уже помеченных Seen этим багом, НЕ восстанавливаются (вне scope; отдельная задача при необходимости).