All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
.mcp.json failed to load as a project-scope config — the entrypoint was ${CLAUDE_PLUGIN_ROOT}/build/index.js, but CLAUDE_PLUGIN_ROOT is only set when the server is launched from a marketplace plugin install. Loaded as a project-scope .mcp.json (Claude Code run from inside a clone — the local-dev and contributor workflow), the variable is unset and, with no default, Claude Code fails to parse the config and the server never loads. Now uses the documented dual-context form ${CLAUDE_PLUGIN_ROOT:-.}/build/index.js: plugin installs still resolve against the plugin root, while project-scope use falls back to ./build/index.js relative to the repo. (#15)search-messages returned zero results whenever dateFrom/dateTo was set on non-English system locales — the date bounds were compiled into AppleScript as date "May 30, 2026" string coercion, which Mail.app parses using the system locale. On a non-English locale (e.g. pt_PT) the English month name throws Invalid date and time (-30720); because the comparison runs inside the per-message try block, the error was silently swallowed and every message was skipped, so date-filtered searches returned nothing even when matching messages existed. The comparison date is now built from numeric components (set year/month/day/…) in a new locale-independent buildAppleScriptDate() helper, so date filters work regardless of system locale. A date-only dateTo is now treated as end-of-day so the upper bound includes messages received later that same day. (#15)move-message / batch-move-messages failed for nested destinations — moving to a nested mailbox (e.g. an Exchange Moore subfolder) was silently failing because the destination was resolved via mailbox "X" of account "Y", which only finds certain top-level mailboxes. Destination resolution now walks the flat mailboxes of account list (which already enumerates nested mailboxes by path, e.g. Processed/Vendors) and matches by exact name, using the matched reference directly. Source-message lookup also benefits — it now reaches messages in nested mailboxes. batch-move-messages now propagates distinct per-message errors (not found vs ambiguous vs message not found) instead of a generic "Failed to move message." Move timeout bumped 60s → 90s. (#14 by @kevinmay-scoutsolutions)move-message refuses to guess when the destination name is ambiguous within an account — if the destination name matches more than one mailbox (e.g. two Drafts mailboxes in the same Gmail account), the move now fails with a clear ambiguous (N matches) error instead of silently moving the message into an arbitrary match. This is a behavior change: previously some such moves succeeded, but you couldn't tell which mailbox received the message. Pass a full path (e.g. Parent/Drafts) to disambiguate.search-messages silently ignored from, subject, isRead, isFlagged filters — these four parameters were declared in the tool's input schema but the handler never forwarded them to searchMessages(), so callers (and LLM agents) reasonably assumed they worked while results came back unfiltered. The handler now passes all four through, and searchMessages() builds them into the AppleScript whose clause as AND filters. The existing query (subject-OR-sender) is parenthesized so precedence holds when combined. Clause-building logic is extracted to a pure buildSearchCondition() with 9 unit tests covering the isRead:false / isFlagged:false not-dropped regression, OR-grouping, and quote/backslash escaping. (#13 by @kevinmay-scoutsolutions)search-messages schema descriptions for from and subject clarify substring-match semantics — from is a substring match against the full Display Name <addr> string (not an exact-address match), and subject is a substring match.build/ not produced on marketplace install — Claude Code installs path-source plugins by git clone only; it does not run npm install, so the prepare hook never fired and the cached plugin directory was missing build/index.js. The MCP server failed to start until the user manually ran npm run build inside the cached plugin dir. Build artifacts in build/ are now committed to the repository so they are present immediately after marketplace clone. A pre-commit hook keeps build/ in sync with src/, and CI fails if the committed build/ is stale. (#10).mcp.json plugin entrypoint resolved against user cwd — args referenced build/index.js as a relative path, so the MCP server only started when Claude Code was launched from inside a clone of this repo. Now uses ${CLAUDE_PLUGIN_ROOT}/build/index.js so the path resolves against the plugin's install location. (#10 by @natekettles)prepare script no longer rebuilds. It now runs husky only. Build artifacts are committed and kept current by the pre-commit hook; rebuilding on every npm install was producing churn in git status for daily development.list-attachments and save-attachment returned empty across all account types (iCloud, Google, Exchange) when attachments were embedded as MIME parts in the message source (a known limitation of Mail.app's AppleScript bridge). Both tools now use a two-attempt pattern: AppleScript first, then fall back to parsing the raw MIME source of the message. Path-traversal protection is preserved on the fallback save path. (#8 by @kevinmay-scoutsolutions)hasAttachments accuracy — get-message now detects MIME-embedded attachments via raw source scan when AppleScript reports zero. list-messages uses the fast AppleScript count path only (may false-negative on MIME-embedded; use get-message or list-attachments for authoritative info).multipart/alternative (text+html) or multipart/related (inline images) containers are now discovered by recursive descent.quoted-printable and 7bit/8bit/binary attachments in addition to base64.src/utils/mimeParse.ts — standalone MIME parser (zero dependencies) with 18 unit tests covering single/multi attachments, nested multipart, inline dispositions, and all supported transfer encodings.reply msg with opening window creates a GUI compose window that doesn't fully initialize from non-interactive processes. Switched to without opening window, which makes set content work immediately and reliably. In-Reply-To and References headers are still set correctly by Mail.app. (#7)forward msg with opening window → without opening window.& content of theReply / & content of theForward appended to the body was always empty (the quoted content lives in Mail.app's HTML layer, not the plaintext content property). Removed the dead concatenation.search-messages and list-messages now search across all mailboxes in an account when no mailbox is specified, instead of defaulting to INBOX. This dramatically improves results for Gmail accounts where messages live in labels rather than INBOX. Deduplication ensures each message appears only once.list-messages now iterates all accounts when no account is specified, matching the existing behavior of search-messages.dateFrom and dateTo now reject non-parseable date strings (e.g., "31", "abc") with a clear error message instead of crashing AppleScript. The existing regex security filter is preserved; a semantic .refine() check is added on top./^\d+$/) to prevent injection attacksescapeForAppleScript() call is applied before interpolationsave-attachment uses path.resolve and restricts save paths to the user's home directory, /tmp, /private/tmp, and /Volumes; attachment names containing /, \, null bytes, or .. are rejectedNumber(id) as an extra safeguardsend-email and create-draft enforce a maximum of 20 file attachmentssrc/security.test.ts with unit tests for all input validation schemas and path traversal preventiontest/integration.test.ts for live Mail.app testingtest:integration and test:all for running integration and combined test suites{{placeholder}} token support (max 100 recipients per batch) (PR #3 by @michaelhenze)send-email and create-draft now accept an optional attachments parameter (array of absolute file paths) (PR #2 by @michaelhenze)send-email and create-draft, preventing failures when Mail.app is slow to establish SMTP connectionssend-serial-email uses spawnSync("sleep") instead of CPU-burning busy-wait between sendssend-serial-email enforces safety limits: max 100 recipients, max 10s delay between sendsbatch-mark-as-unread, batch-flag-messages, batch-unflag-messagescreate-mailbox, delete-mailbox, rename-mailboxlist-rules, enable-rule, disable-rulesearch-contacts (Contacts.app integration)save-template, list-template, get-template, delete-template, use-templatepreferHtml option in get-messagefrom, offset) for list-messagesdateFrom, dateTo) for search-messagesunflag-message tool (was implemented but not wired up)First stable release with full Apple Mail integration.
Initial release - project skeleton.