# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.5.5] - 2026-06-01 ### Fixed - **`.mcp.json` parsed but never *connected* as a project-scope config (incomplete 1.5.4 fix)** — 1.5.4 switched the entrypoint to `${CLAUDE_PLUGIN_ROOT:-.}/build/index.js`, which fixed the *parse* error but not the actual failure: in a project-scope clone `CLAUDE_PLUGIN_ROOT` is unset, so the path fell back to `.`, which Claude Code resolves against the launching process's working directory — **not** the repo root — so the server still failed to connect. A single entrypoint string cannot serve both contexts: plugin installs require `${CLAUDE_PLUGIN_ROOT}` (in a plugin, `CLAUDE_PROJECT_DIR` points at the *user's* project, not the plugin dir), clones require `${CLAUDE_PROJECT_DIR:-.}`, and Claude Code does not support nested defaults (`${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PROJECT_DIR:-.}}` does not expand). The two distribution paths are now **decoupled**: the root `.mcp.json` uses `${CLAUDE_PROJECT_DIR:-.}/build/index.js` for the clone/contributor workflow, and the plugin carries its own MCP config in `.claude-plugin/plugin.json` using `${CLAUDE_PLUGIN_ROOT}/build/index.js`. Because `plugin.json` now declares `mcpServers`, the plugin no longer auto-loads the root `.mcp.json`, so there is no double-registration. See the README's [Running from a clone](README.md#running-from-a-clone-in-claude-code-project-scope-mcpjson) section. ([#15](https://github.com/sweetrb/apple-mail-mcp/issues/15)) ## [1.5.4] - 2026-06-01 ### Fixed - **`.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](https://github.com/sweetrb/apple-mail-mcp/issues/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](https://github.com/sweetrb/apple-mail-mcp/issues/15)) ## [1.5.3] - 2026-05-27 ### Fixed - **`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](https://github.com/sweetrb/apple-mail-mcp/pull/14) by @kevinmay-scoutsolutions) ### Changed - **`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. ## [1.5.2] - 2026-05-27 ### Fixed - **`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](https://github.com/sweetrb/apple-mail-mcp/pull/13) by @kevinmay-scoutsolutions) ### Changed - **`search-messages` schema descriptions for `from` and `subject` clarify substring-match semantics** — `from` is a substring match against the full `Display Name ` string (not an exact-address match), and `subject` is a substring match. ## [1.5.1] - 2026-04-28 ### Fixed - **Plugin install: `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](https://github.com/sweetrb/apple-mail-mcp/pull/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](https://github.com/sweetrb/apple-mail-mcp/pull/10) by @natekettles) ### Changed - **`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. ## [1.5.0] - 2026-04-20 ### Fixed - **Attachments invisible to AppleScript** — `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](https://github.com/sweetrb/apple-mail-mcp/pull/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). ### Added - **Nested multipart support in MIME parser** — attachments nested inside `multipart/alternative` (text+html) or `multipart/related` (inline images) containers are now discovered by recursive descent. - **Non-base64 transfer encoding decoding** — MIME fallback now supports `quoted-printable` and `7bit`/`8bit`/`binary` attachments in addition to base64. - **New module `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. ## [1.4.0] - 2026-04-03 ### Fixed - **reply-to-message empty body from background processes** — Replies (and forwards) sent via the MCP server had empty body text because `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](https://github.com/sweetrb/apple-mail-mcp/issues/7)) - **forward-message empty body from background processes** — Same root cause and fix as reply-to-message. `forward msg with opening window` → `without opening window`. - **Removed no-op quoted content concatenation** — The old `& 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. ## [1.3.0] - 2026-04-01 ### Changed - **Search all mailboxes by default** - `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. - **Multi-account listing** - `list-messages` now iterates all accounts when no account is specified, matching the existing behavior of `search-messages`. ### Fixed - **Date filter validation** - `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. ## [1.2.1] - 2026-03-27 ### Security - **Message ID validation** - Message IDs are now validated as numeric-only (`/^\d+$/`) to prevent injection attacks - **Batch size cap** - Batch operations are limited to a maximum of 100 messages per request - **Date filter validation** - Date filters are validated to allow only alphanumeric characters and safe punctuation; an additional belt-and-suspenders `escapeForAppleScript()` call is applied before interpolation - **Attachment save path traversal prevention** - `save-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 rejected - **Defense-in-depth ID coercion** - All AppleScript message ID interpolations now use `Number(id)` as an extra safeguard - **Attachment count limit** - `send-email` and `create-draft` enforce a maximum of 20 file attachments ### Added - **Security test suite** - `src/security.test.ts` with unit tests for all input validation schemas and path traversal prevention - **Integration test suite** - `test/integration.test.ts` for live Mail.app testing - **New npm scripts** - `test:integration` and `test:all` for running integration and combined test suites ## [1.2.0] - 2026-03-14 ### Added - **send-serial-email** - Mail merge tool: send personalized emails to multiple recipients with `{{placeholder}}` token support (max 100 recipients per batch) (PR #3 by @michaelhenze) - **File attachments** - `send-email` and `create-draft` now accept an optional `attachments` parameter (array of absolute file paths) (PR #2 by @michaelhenze) ### Fixed - **Locale-independent date parsing** - Dates now display correctly on non-English macOS systems (e.g., German). Previously, locale-dependent date strings could cause all emails to show the current date instead of actual received date (PR #4 by @michaelhenze) - **Send/draft timeout resilience** - Increased timeout from 30s to 60s and enabled automatic retry with exponential backoff for `send-email` and `create-draft`, preventing failures when Mail.app is slow to establish SMTP connections ### Improved - Attachment paths are validated (must be absolute, must exist) before sending — provides clear error messages instead of cryptic AppleScript failures - `send-serial-email` uses `spawnSync("sleep")` instead of CPU-burning busy-wait between sends - `send-serial-email` enforces safety limits: max 100 recipients, max 10s delay between sends ## [1.1.1] - 2026-03-10 ### Fixed - TTL cache for account and mailbox name resolution to reduce redundant AppleScript calls ## [1.1.0] - 2026-03-09 ### Added - **Batch operations** - `batch-mark-as-unread`, `batch-flag-messages`, `batch-unflag-messages` - **Mailbox management** - `create-mailbox`, `delete-mailbox`, `rename-mailbox` - **Mail rules** - `list-rules`, `enable-rule`, `disable-rule` - **Contacts** - `search-contacts` (Contacts.app integration) - **Email templates** - `save-template`, `list-template`, `get-template`, `delete-template`, `use-template` - **save-attachment** - Download attachments to disk - **HTML content** - `preferHtml` option in `get-message` - Date received in search/list output - Sender filter and pagination (`from`, `offset`) for `list-messages` - Date range filtering (`dateFrom`, `dateTo`) for `search-messages` - Cross-account search when no account specified - Exposed `unflag-message` tool (was implemented but not wired up) ### Fixed - Use Mail.app's configured default send account instead of hardcoded fallback (PR #1 by @Leewonchan14) - Add message ID to search and list results (PR #1 by @Leewonchan14) ## [1.0.0] - 2026-01-06 First stable release with full Apple Mail integration. ### Features #### Message Operations - **search-messages** - Search messages by query, sender, subject with filtering options - **list-messages** - List messages in any mailbox with pagination - **get-message** - Retrieve full message content (subject, body, metadata) - **send-email** - Send emails with To, CC, BCC recipients from any account - **create-draft** - Save emails to Drafts folder without sending - **reply-to-message** - Reply to messages with reply-all support, send or save as draft - **forward-message** - Forward messages to new recipients with optional body - **mark-as-read** / **mark-as-unread** - Toggle message read status - **flag-message** / **unflag-message** - Toggle message flagged status - **delete-message** - Move messages to Trash - **move-message** - Organize messages into mailboxes #### Mailbox Operations - **list-mailboxes** - List all mailboxes/folders with unread counts - **get-unread-count** - Get unread count for specific mailbox or all accounts #### Account Operations - **list-accounts** - List all configured Mail accounts #### Diagnostics - **health-check** - Verify Mail.app connectivity and permissions - **get-mail-stats** - Get message and unread counts per account ### Technical - Full AppleScript integration with proper escaping and error handling - Retry logic with exponential backoff for transient failures - User-friendly error messages with actionable suggestions - Debug logging support (set DEBUG=1 or VERBOSE=1) - 60-second timeout for message search operations - Message ID lookup across all mailboxes for reliable operations ## [0.1.0] - 2026-01-06 Initial release - project skeleton. ### Added - Initial project structure forked from apple-notes-mcp - MCP server skeleton with tool definitions - TypeScript types for Mail data models - AppleScript utilities with error handling