b4646155b4
internal/email is the reading half of the email reader: a ~200-line IMAP client (LOGIN, EXAMINE, UID SEARCH SINCE, UID FETCH BODY.PEEK, LOGOUT), a MIME-to-plaintext converter, and a header-only junk filter. Two protocol choices are the design, not shortcuts. EXAMINE instead of SELECT means the session is read-only at the protocol level, so no command in it can flip a flag or expunge anything by mistake. BODY.PEEK instead of BODY means reading a message does not mark it \Seen — Maven reads his mail and leaves no trace of having done so, and the unread state in his own client stays his. Hand-rolled rather than go-imap because this is the one path that holds his mailbox credential and reads his private mail: five commands with no dependencies is auditable in a sitting. No IDLE and no cleartext/STARTTLS either — an option to send his password over a plain socket is an option to get it wrong once. Junk is decided by headers alone, before any model is involved: List-Unsubscribe/List-Id, Precedence: bulk, Auto-Submitted, the spam headers, and Gmail's own category labels. Sender lists and subject keywords are deliberately absent — they age badly and they would put his contacts in a config file. A junk verdict only means "do not spend the model on this"; nothing is deleted and no server flag is touched. Nothing here logs a body, a subject or an address, the junk reason names a header rather than content, and an undecodable charset degrades to headers-only instead of feeding the model mojibake. Verified against recorded .eml fixtures and an in-process fake IMAP server.