Ver Fonte

fix: use 'without opening window' for reply/forward to fix empty body from background processes (#7)

reply-to-message and forward-message sent empty body text when the MCP
server ran as a background process. Switched AppleScript from
'with opening window' to 'without opening window' so set content works
immediately without a GUI compose window. Bumps version to 1.4.0.
Robert Sweet há 2 meses atrás
pai
commit
ea767f7a81
5 ficheiros alterados com 59 adições e 5 exclusões
  1. 7 0
      CHANGELOG.md
  2. 37 0
      CLAUDE.md
  3. 10 0
      README.md
  4. 1 1
      package.json
  5. 4 4
      src/services/appleMailManager.ts

+ 7 - 0
CHANGELOG.md

@@ -5,6 +5,13 @@ 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.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

+ 37 - 0
CLAUDE.md

@@ -100,12 +100,14 @@ The `to`, `cc`, and `bcc` parameters must always be arrays:
 - Set `replyAll: true` to reply to all recipients
 - Set `send: false` to save as draft instead of sending immediately
 - Default behavior: reply to sender only, send immediately
+- Uses `without opening window` internally — no Mail.app compose window is opened, which ensures reliable body delivery from background processes (see [Known Issues](#known-issue-resolved-reply--forward-empty-body-from-background-processes) below)
 
 ### forward-message
 
 - Requires message `id` and `to` array
 - Optional `body` to prepend a message
 - Set `send: false` to save as draft
+- Uses `without opening window` internally — same background-process fix as reply-to-message
 
 ### Multi-account
 
@@ -258,6 +260,41 @@ The `to`, `cc`, and `bcc` parameters must always be arrays:
 2. get-mail-stats → see total/unread counts and recently received counts
 ```
 
+## Known Issue (Resolved): Reply / Forward Empty Body from Background Processes
+
+### The Problem
+
+Prior to v1.4.0, `reply-to-message` and `forward-message` would send replies/forwards with **empty body text** when the MCP server was running as a background process (e.g., spawned via `execSync` from a Node.js MCP server, which is how Claude Code invokes it).
+
+The root cause was the AppleScript `reply msg with opening window` command. This creates a GUI compose window asynchronously. When `set content` runs immediately after, the window may not be ready yet, and the content assignment is **silently ignored**. Even adding delays (`delay 1`, `delay 2`) was unreliable — the compose window's readiness depends on system load, Mail.app state, and whether the process has GUI access.
+
+A secondary issue: the old code appended `& content of theReply` (the original quoted message) to the body. This was always a no-op — the quoted content lives in the HTML layer of the compose window, not the plaintext `content` property.
+
+### The Fix
+
+Replaced `with opening window` with `without opening window` for both `reply` and `forward` commands. With this approach:
+
+- `set content` works **immediately** — no delay needed
+- Works reliably from background processes (Node.js `execSync`, MCP stdio transport)
+- `In-Reply-To` and `References` headers are still set correctly by Mail.app (the `reply` command knows which message it's replying to)
+- No GUI compose window is opened (better for a server process)
+- `reply to all` and `send` both work as expected
+
+### Approaches That Were Tested and Failed
+
+| Approach | Result |
+|----------|--------|
+| `delay 1` / `delay 2` before `set content` | Body still empty from background process (works interactively) |
+| `reply msg without opening window` (old attempt) | Previously dismissed, but actually works — `set content` is reliable without the window |
+| `set html content` on reply object | AppleScript error — not a valid property |
+| System Events UI scripting (keystroke) | Blocked: "osascript is not allowed to send keystrokes" from background process |
+| `make new outgoing message` with same subject | Body arrives, but no `In-Reply-To`/`References` headers (can't set `reply id` on outgoing messages) |
+| Manual headers on `outgoing message` | Not possible — Mail.app's `outgoing message` class doesn't expose a `headers` property |
+
+### References
+
+- GitHub Issue: [#7 — reply-to-message sends empty body when called from background process](https://github.com/sweetrb/apple-mail-mcp/issues/7)
+
 ## Testing Your Understanding
 
 Before sending emails with paths or special characters, verify escaping:

+ 10 - 0
README.md

@@ -724,6 +724,16 @@ If installed from source, use this configuration:
 | Attachment save path restrictions | `save-attachment` only allows saving to home directory, `/tmp`, `/private/tmp`, and `/Volumes`; path traversal is blocked |
 | Attachment count limit | `send-email` and `create-draft` accept a maximum of 20 file attachments |
 
+### Reply / Forward from Background Processes (Fixed in v1.4.0)
+
+Prior to v1.4.0, `reply-to-message` and `forward-message` would send messages with **empty body text** when the MCP server ran as a background process (e.g., spawned via `execSync` from Node.js, which is how Claude Code invokes it).
+
+**Root cause:** The AppleScript `reply msg with opening window` command creates a GUI compose window asynchronously. When `set content` runs immediately after, the window may not be ready, and the content assignment is silently ignored. Delays (`delay 1`, `delay 2`) were unreliable — the compose window's readiness depends on system load, Mail.app state, and whether the process has GUI access.
+
+**Fix:** Replaced `with opening window` with `without opening window` for both `reply` and `forward` commands. With this approach, `set content` works immediately and reliably from background processes. `In-Reply-To` and `References` headers are still set correctly by Mail.app, and no GUI compose window is opened.
+
+See [#7](https://github.com/sweetrb/apple-mail-mcp/issues/7) for full details and the list of approaches that were tested.
+
 ### Backslash Escaping (Important for AI Agents)
 
 When sending content containing backslashes (`\`) to this MCP server, **you must escape them as `\\`** in the JSON parameters.

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "apple-mail-mcp",
-  "version": "1.3.0",
+  "version": "1.4.0",
   "description": "MCP server for Apple Mail - read, search, send, and manage emails via Claude",
   "type": "module",
   "main": "build/index.js",

+ 4 - 4
src/services/appleMailManager.ts

@@ -968,8 +968,8 @@ export class AppleMailManager {
               set matchingMsgs to (messages of mb whose id is ${Number(id)})
               if (count of matchingMsgs) > 0 then
                 set msg to item 1 of matchingMsgs
-                set theReply to reply msg with opening window${replyAllClause}
-                set content of theReply to "${safeBody}" & return & return & content of theReply
+                set theReply to reply msg without opening window${replyAllClause}
+                set content of theReply to "${safeBody}"
                 ${sendAction}
                 return "ok"
               end if
@@ -1019,9 +1019,9 @@ export class AppleMailManager {
               set matchingMsgs to (messages of mb whose id is ${Number(id)})
               if (count of matchingMsgs) > 0 then
                 set msg to item 1 of matchingMsgs
-                set theForward to forward msg with opening window
+                set theForward to forward msg without opening window
                 ${recipientCommands}
-                ${safeBody ? `set content of theForward to "${safeBody}" & return & return & content of theForward` : ""}
+                ${safeBody ? `set content of theForward to "${safeBody}"` : ""}
                 ${sendAction}
                 return "ok"
               end if