Phase B + folders: mail_folder_list, mail_search, mail_thread, mail_move
Phase B per the spec (multi-account already supported via the
account arg) + full folder support:
- mail_folder_list : enumerate IMAP folders. Returns {name, delimiter,
attributes, selectable}. selectable=false flags
\Noselect mailboxes (parent-of-children only).
- mail_search : raw IMAP SEARCH passthrough against any folder.
ALL/OR/NOT combinators supported. CR/LF in the
query rejected (anti-injection).
- mail_thread : seed Message-ID -> matches the seed itself plus
any message whose References or In-Reply-To
contains the seed. Oldest-first ordering (root
-> leaves). Brackets on the seed are optional.
- mail_move : UID MOVE (RFC 6851) with COPY + STORE +FLAGS
\Deleted + UID EXPUNGE fallback. Destination
folder must already exist.
Refactor: shared fetch_summaries() helper used by search + thread.
Smoke verified 2026-05-21 against kayos@sulkta.com:
- 6 folders listed (DMARC, Drafts, INBOX, Junk, Sent, Trash)
- SEARCH "SUBJECT \"mail-mcp smoke\"" finds uid 27
- THREAD on uid 27's Message-ID returns 1 msg (the seed)
- MOVE INBOX(27) -> Junk(2) -> INBOX(28) round trip clean
Build gotcha: async-imap's uid_store and uid_expunge return non-Unpin
streams (unlike uid_fetch). Pinned via Box::pin inside scoped blocks.