Skip to content

v0.9.0 — Mail depth & outbound

Choose a tag to compare

@elfensky elfensky released this 29 Jul 09:48
· 85 commits to main since this release

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.toml and the app bundle's Info.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 .emlx are 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.0 milestone 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