v0.9.0 — Mail depth & outbound
Mail depth & outbound — gated sending, the drafts lifecycle, and an indexed read plane over Mail's own Envelope Index.
Outbound, behind a gate a human flips
send_mail, reply_all and forward_mail exist — but the tool is absent unless the operator runs macos-apps-mcp allow-send mail. That's a CLI command and deliberately not an MCP tool: the model must not be able to grant itself sending. dry_run still defaults to True even once enabled, and a dry run makes no native call at all.
sent: True means Mail accepted the message, not that it was delivered — so every send also reports outbox_pending.
Indexed Mail reads (#70, #75, #76, #77)
Reads now go through a read-only sqlite plane over ~/Library/Mail/V*/MailData/Envelope Index — fast, and it never launches Mail.
mail_search— subject/from/to/mailbox/account/date-range/unread/flagged/has-attachments, across all mailboxes, plus opt-in full-text body search via an FTS sidecar in our own state dir.mail_overview— every mailbox with total and unread counts, unread-first.mail_thread— a whole conversation from any message id, oldest-first, including the messages you sent.mail_needs_response/mail_awaiting_reply— triage over real In-Reply-To/References threading.
Search returns one result per message. A real mailbox stores one RFC822 Message-ID in several places — Gmail shows a message under both a label and All Mail, migrations leave copies on two servers, every reply makes a Sent-plus-folder pair. On the development machine 36,112 non-deleted rows resolved to just 22,223 distinct messages, and search was returning some of them up to eight times. Dedup now happens in SQL, preferring a live INBOX copy over a filed one over Archive/Trash/Junk.
has_attachments means a real document. Mail records inline signature and newsletter images as attachments, so the naive predicate matched 4,474 messages where only 2,223 carried an actual document.
Counts are computed live. mailboxes.unread_count is trigger-maintained and goes stale.
Safety and self-report
- Three capability tiers gated at registration — read → write → outbound. A gated-off tool is absent, never registered-and-erroring.
doctor()reports the version actually serving the call, so a stale daemon can't hide.- The version is enforced identical across
pyproject.tomland the app bundle'sInfo.plist— 0.8.0 shipped a bundle claiming the wrong version for a full cycle.
Known gaps
- Body search only covers messages already downloaded and indexed; ~37% of
.emlxare full on a typical IMAP account. Forcing the rest is #119, deliberately scoped as a CLI command. - Smart mailboxes are saved searches in a plist and are not visible to the index plane.
- The
0.9.0milestone remains open — mailbox management, status writes, trash and attachment export land in 0.9.x. Milestones and releases don't have to line up.
Full history: v0.8.0...v0.9.0