Evaluated OAuth2 (SPEC §10) and chose not to build it this phase. A self-built,
unverified OAuth app suffers Google's 7-day refresh-token expiry in Testing
status (or the unverified-warning + restricted-scope verification cost in
Production). For a single-user personal tool, a Gmail App Password (2FA) is
strictly simpler and reuses the IMAP/SMTP password auth from Phases 1–2.
Validated live against a real Gmail account over app-password auth: list/get/
search, send, and a full SMTP-out → IMAP-in round-trip. No code changes were
required; the speculative OAuth store fields started mid-session were reverted.
OAuth2 remains a clean future addition (schema columns already present).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
If header/body parsing errored mid-fetch we returned without draining the
message channel, so the UidFetch goroutine could block on a full channel.
Both fetch paths now break, drain remaining messages, then read the done
error. Verified with the race detector.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
list and search now fetch BODY.PEEK[HEADER] + BODYSTRUCTURE instead of the
whole RFC822 message, so listing a large mailbox no longer downloads every
message body and attachment. Header parsing reuses the same go-message path
(RFC2047 decoding/formatting preserved); has_attachments is derived from the
BODYSTRUCTURE tree. FetchFull keeps fetching the full message for get.
Validated end-to-end against a live IMAP account: list/search/get output
identical to the prior full-fetch behaviour, has_attachments correct.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pass 0 (unlimited) to m.Search so the mail layer returns all matching
headers; the existing post-filter loop already caps at the caller's
limit, mirroring ListCmd. Add TestSearchLimitCountsVisibleOnly to prove
filtering happens before the cap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Thread uidv through setup's return value (new uint32 before the cleanup
func) so AckCmd no longer makes a redundant SelectFolder round-trip that
silently returned 0 on failure and recorded acks under the wrong
UID-validity epoch. All four callers updated; read-only callers ignore
the value with _.
- Cap search results to limit (keep most-recent UIDs)
- Propagate io.ReadAll errors from body reads in fetchByUIDSet
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SQLite PRAGMAs are connection-scoped, but database/sql uses a connection
pool. Without pinning to one connection, new pooled connections won't have
foreign_keys enabled, breaking ON DELETE CASCADE enforcement.
Also mark modernc.org/sqlite as a direct dependency in go.mod.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TDD task-by-task plan for the read-only emcli: crypto, encrypted store,
seen-set read state, policy filtering, IMAP read, and the agent
list/get/search/ack commands with flag-based admin. Phases 2-4 (send,
OAuth2, TUI) to follow as their own plans.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reading state is now a per-(account,folder) floor plus an acked set of
UIDs above it, instead of a single monotonic pointer. This makes
acknowledgement per-message and order-independent so concurrent
subagents can process and ack out of order. Internal compaction collapses
contiguous acked runs into the floor to bound storage. Adds stateless
search and ack commands; reads no longer mutate state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
emcli is a Go CLI that mediates an AI agent's email access, enforcing
per-account read/send restrictions so credentials never reach the agent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>