v0.10.0 — keep secrets out of prompts, seal the AI cache per user
Privacy release. The AI features send your mail to a third-party model; this is about what they're allowed to take with them.
Credentials are stripped before prompts leave the server
Mail is full of secrets nobody thought of as secrets — a reset mail quoting the temporary password, a colleague pasting an API key, a signup confirmation with a one-time code. Summarising or scanning that mail shipped it verbatim to whoever runs the model, and none of those features need the secret to do their job.
Outbound prompts are now scrubbed with deterministic patterns — no model involved, so it can't be talked out of it: vendor key formats, JWTs, private key blocks, password: …-style assignments, credentials embedded in URLs, and card numbers validated with Luhn so order references survive.
Deliberately biased toward missing an unusual secret over mangling ordinary prose. "Please reset your password before Friday" is left exactly alone. A redactor that eats real text gets switched off, and then it protects nothing. LLM_SCRUB_SECRETS=false opts out.
Verified against production: asking the model to echo back a password and an API key returns my password is [redacted] and the key is [redacted:api-key].
The AI cache is sealed per user
Cached completions quote your own mail back at you, so the cache is as sensitive as the mailbox. Rows are now encrypted with a key derived from the server key and the user's own password — ai-cache.db is inert without both, and the server can only read a row while that user is authenticated.
The password is bound into the lookup as well, so a row written under an old password is never even a candidate rather than being found and failing to open. Rows written before this still read back.
Optional decoy requests
LLM_DECOY_COUNT (0–5, default 0) sends variants of each prompt with names, addresses and numbers randomised, so the provider's stored transcript is mostly conversations that never happened. Fire-and-forget — it cannot delay or alter your answer.
Honestly stated, because the trade-off is real: it multiplies token spend by (1 + count), and it does not hide that the account uses the service, when, or roughly how much text.
Also
has:attachment now matches attachments (#14) — it was mapped to an IMAP keyword Dovecot never sets, so it returned zero results everywhere. Decided from MIME structure using the same function that draws the paperclip, with attachment flags cached as you browse: cold 27s → warm 0.4s on a 2900-message folder.
Mailbox passwords sealed at rest, every sqlite file kept on the persistent volume, Drive buckets provisioned with CORS, and a clear error when mailcow has Sieve disabled for a mailbox — see v0.9.x.
341 tests, 340 passing, 1 skipped.