fix: remove Cmd+A before HTML paste to preserve attachments#33
fix: remove Cmd+A before HTML paste to preserve attachments#33gerryke wants to merge 52 commits into
Conversation
…ar-mcp-personal refactor: Split monolithic apple_mail_mcp.py into modular package
When send=False, the reply is saved as a draft instead of being sent immediately. This allows users to review and edit replies before sending. The reply is still created via Mail's `reply` command so it threads properly with In-Reply-To/References headers, but instead of calling `send`, the compose window is closed with `saving yes` which saves to the Drafts folder. Co-Authored-By: Claude Opus 4.6 <[email protected]>
When saving a reply as draft (send=False), Mail.app's `close window saving yes` saves the content property literally. But after `reply`, the content property is empty (quoted text lives in the HTML layer). For drafts, manually read the original message content and build the quoted text so the saved draft includes both reply body and quoted original. The send path is unaffected since `send` handles HTML quoting. Also adds small delays after reply creation and content setting to let Mail.app sync state before closing the window. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Email content from AppleScript can contain characters that break the MCP stdio JSON-RPC transport, causing "CLI output was not valid JSON" errors in Claude Desktop. Changes: - Add _sanitize_for_json() that normalizes line endings (\r → \n), forces ASCII-safe output, and strips control characters - Switch run_applescript() to bytes mode with explicit UTF-8 decoding (errors='replace') instead of text=True which uses locale encoding - Add __main__.py entry point to eliminate RuntimeWarning on stderr when running via `python -m apple_mail_mcp` Co-Authored-By: Claude Opus 4.6 <[email protected]>
…plescript-output-for-json fix: sanitize AppleScript output to prevent JSON serialization errors
…draft feat: add save-as-draft support to reply_to_email
Prevents AppleScript syntax errors when user input contains \n, \r, or \t characters by escaping them in the output string. Addresses issue patrickfreyer#19 (security audit item 1). Co-Authored-By: Claude Opus 4.6 <[email protected]>
- empty_trash now requires confirm_empty=True and respects max_deletes - manage_trash and update_email_status require at least one filter (subject_keyword or sender) or explicit apply_to_all=True - save_email_attachment validates paths: must be under home dir, blocks writes to ~/.ssh, ~/.aws, ~/Library/LaunchAgents, etc. Addresses issue patrickfreyer#19 (security audit items 2, 3, 4). Co-Authored-By: Claude Opus 4.6 <[email protected]>
Adds max_emails parameter (default: 1000) to export_emails to prevent unbounded exports when scope="entire_mailbox". Addresses issue patrickfreyer#19 (security audit item 5). Co-Authored-By: Claude Opus 4.6 <[email protected]>
Pin fastmcp==3.1.0 and mcp-ui-server==1.0.0 instead of open-ended >=0.1.0 ranges to ensure reproducible builds. Addresses issue patrickfreyer#19 (security audit item 6). Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
…ssues-19-v2 fix: Address security issues from audit (patrickfreyer#19)
…atrickfreyer#10) Add build_mailbox_ref(), build_filter_condition(), build_date_filter(), and build_email_fields_script() to core.py. Update manage.py to use build_filter_condition and build_mailbox_ref, reducing duplicated mailbox resolution and condition-building code. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add output_format="json" parameter to list_inbox_emails, get_recent_emails, and search_emails. When set to "json", returns structured email data as a JSON array instead of formatted text, making it easier for LLMs and downstream tools to parse results. Co-Authored-By: Claude Opus 4.6 <[email protected]>
…atrickfreyer#8, patrickfreyer#11, patrickfreyer#12) - create_mailbox: create new mailboxes with nested path support and name validation (patrickfreyer#8) - search_emails_advanced: unified search with filters for subject, body, sender, dates, read/flagged/attachment status (patrickfreyer#11) - archive_emails: safely move matching emails to Archive with dry_run default, filter requirement, and max_archive cap (patrickfreyer#12) - Update __init__.py tool counts Co-Authored-By: Claude Opus 4.6 <[email protected]>
…cture-improvements feat: Infrastructure improvements (patrickfreyer#8, patrickfreyer#9, patrickfreyer#10, patrickfreyer#11, patrickfreyer#12)
Add three new MCP tools for batch email management: - mark_emails: batch mark as read/unread/flagged/unflagged with filters - delete_emails: soft-delete with dry_run=True default and safety limits - bulk_move_emails: batch move with nested mailbox support All tools require at least one filter and enforce max_emails safety limits to prevent accidental mass operations. Closes patrickfreyer#2, closes patrickfreyer#3, closes patrickfreyer#4 Co-Authored-By: Claude Opus 4.6 <[email protected]>
…ations feat: Add bulk email operations (patrickfreyer#2, patrickfreyer#3, patrickfreyer#4)
…n, and sender analytics Add three new tools in smart_inbox.py module: - get_awaiting_reply (patrickfreyer#5): finds sent emails without replies by cross-referencing Sent and Inbox - get_needs_response (patrickfreyer#6): identifies unread emails needing action, filtering out newsletters/automated - get_top_senders (patrickfreyer#7): ranks most frequent senders with optional domain grouping Co-Authored-By: Claude Opus 4.6 <[email protected]>
feat: Add smart inbox tools (patrickfreyer#5, patrickfreyer#6, patrickfreyer#7)
Port attachment functionality from PR patrickfreyer#15 with security hardening: - Add `attachments` parameter (comma-separated file paths) to both functions - Validate paths: expand tilde, resolve symlinks, require home dir, block sensitive directories (.ssh, .gnupg, .aws, .config, .claude, Keychains, etc.) - Check file existence with os.path.isfile() before passing to AppleScript - Use consistent POSIX file syntax and delay 1 in attachment loops - Return early with descriptive error if any path fails validation - Extract shared validation into _validate_attachment_paths() helper Co-Authored-By: Claude Opus 4.6 <[email protected]>
…t-support feat: add attachment support with security validation
…tering Port performance improvements from PR patrickfreyer#13 into the modular codebase. Five tools now use whose clause filtering at the Mail.app level instead of iterating every message in AppleScript loops: - search_emails: whose clause for subject, sender, read status, and programmatic date objects (locale-independent) - search_by_sender: whose clause for sender + date, remove lowercase() handler - get_recent_from_sender: whose clause for sender + date, remove lowercase() - get_newsletters: whose date received pre-filter to avoid full mailbox scan - get_statistics: whose clause pre-filtering for account_overview and sender_stats scopes, per-mailbox try/on error, skip system folders, division-by-zero guards has_attachments kept as post-filter (not supported in whose clauses). Newsletter pattern matching kept as post-filter (too complex for whose). Co-Authored-By: Claude Opus 4.6 <[email protected]>
…use-perf perf: whose clause filtering for 5 search/analytics tools
Adds a new delivery mode that opens emails in a visible Mail.app
compose window for user review before sending, instead of sending
immediately or saving silently to Drafts.
- compose_email: new `mode` param ("send"/"draft"/"open")
- reply_to_email: new `mode` param (overrides legacy `send` bool)
- manage_drafts: new "open" action to open existing drafts
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Open mode now lets Mail.app handle the quoted original natively (with proper HTML formatting and blue quote bar) instead of manually building plain-text quoted content. Co-Authored-By: Claude Opus 4.6 <[email protected]>
set content was replacing the entire message including the quoted original. Now prepends the reply body to the existing content so the native quoted original is preserved below. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Mail.app loads quoted original content asynchronously into the HTML layer. Setting content via AppleScript overwrites it. Using keystroke to type the reply preserves the native quoted original formatting. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Adds body_html parameter to compose_email for rich formatting (bold, headings, links, colors). Uses AppleScriptObjC to place HTML on the clipboard and paste into the compose window. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Reply body text was embedded directly into AppleScript source via f-string interpolation, causing syntax errors when the body contained special characters (em dashes, curly quotes, colons, slashes). Now writes the body to a temp file and reads it in AppleScript via `do shell script`, completely avoiding string escaping issues. Also switches open mode from fragile per-char keystroke to clipboard paste, and adds Unicode line separator handling to escape_applescript. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Body search was running as a post-filter, reading content of every email in a loop — causing timeouts on large mailboxes. AppleScript's contains operator is already case-insensitive, so body search can use the native whose clause (content contains "...") for Mail.app-level filtering, which is dramatically faster. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Manifest now reflects 27 tools (down from 35) after consolidating 9 search tools into one unified search_emails tool. Updated import comment. Co-Authored-By: Claude Opus 4.6 <[email protected]>
The `reply` command with multiple `with` parameters requires `and` between them. Without it, `reply_to_all: true` caused a syntax error (-2741) for all delivery modes (send, draft, open). Co-Authored-By: Claude Opus 4.6 <[email protected]>
When enabled, removes compose_email, reply_to_email, and forward_email from the tool registry. Draft management (list, create, delete) remains available but the draft "send" action is blocked. Co-Authored-By: Claude Opus 4.6 <[email protected]>
feat: add --read-only flag to disable email sending tools
GitHub's markdown renderer blocks external SVG images. Using the <picture> element with <source> tags is the recommended approach for star-history.com charts in GitHub READMEs. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
export_emails() accepted any save_directory without validation, unlike save_email_attachment() and compose attachment handling which already enforce home-directory bounds and block sensitive dirs. This adds the same path validation: realpath resolution, home-directory check, and sensitive directory blocking. Co-Authored-By: Claude Opus 4.6 <[email protected]>
…tured-search feat: add rich draft workflow and structured mail search
The `_sanitize_for_json` function was encoding all text to ASCII with
`text.encode("ascii", "replace")`, replacing every non-ASCII character
with "?". This broke output for all non-Latin scripts: Cyrillic, CJK,
Arabic, Hebrew, etc.
Replace with a filter that strips control characters while preserving
all printable Unicode.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…tion-to-export-emails Good security fix — consistent with existing path validation patterns in save_email_attachment.
Clean fix — ASCII restriction was overly cautious, UTF-8 works fine over stdio.
Release notes are maintained via GitHub Releases instead. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
reply_to_email (send/draft modes) and forward_email were using `set content of` in AppleScript, which overwrites Mail.app's HTML layer and destroys the quoted original / email thread history. Now all modes use NSPasteboard HTML clipboard injection to paste the reply body, preserving Mail's native quoted original below the reply. Also adds body_html parameter to reply_to_email for rich-text replies (HTML rendering still needs work — tracked in a separate issue). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…erves-email-history fix: preserve email thread history in reply and forward
…il_thread) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…nread_count - Add include_content param to list_inbox_emails (replaces get_recent_emails) - Add summary_only param to get_mailbox_unread_counts (replaces get_unread_count) - Remove get_recent_emails, _get_recent_emails_json, and get_unread_count - Update inbox_dashboard in analytics.py to use get_mailbox_unread_counts(summary_only=True) - Update __init__.py tool count comment (6 → 5) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…o manage.py Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
When composing an HTML email with attachments, the AppleScript was selecting all body content (Cmd+A) before pasting the HTML clipboard. Since Mail embeds attachments inline in the body, this caused the paste to overwrite and wipe out any attachments that had already been added to the compose window. Removing the select-all keystroke fixes the issue — the body starts empty so there is nothing to clear, and attachments survive the paste. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
Hi @gerryke — thanks for spotting this. The Cmd+A wipe is a real bug and the fix is correct. Two issues with merging this PR as-is:
Could you open a small focused PR with only the Happy to close this once the focused PR is up. |
|
Closing — the headline 2-line fix is correct (I'll cherry-pick it directly), but this PR bundles a tool-consolidation refactor (4,185 LOC across 21 files), an accidentally-committed If you'd like to re-PR the tool-consolidation work as a focused change against the current |
Problem
When calling
compose_emailwith bothbody_htmlandattachments, the attachments are added to the compose window first, then the AppleScript doesCmd+A(select all) before pasting the HTML from clipboard.Since Apple Mail embeds attachments inline in the message body,
Cmd+Aselects the attachments along with any body text. The subsequentCmd+Vpaste then replaces everything — including the attachments — leaving the final email with HTML content but no attachments.Root Cause
In
_send_html_email, Step 4 of the AppleScript:Fix
Remove the
Cmd+Akeystroke. The compose window body is initialized as empty (content:""), so there is nothing to clear before pasting. Attachments added in Step 2 are preserved.Verification
Tested with
compose_email(body_html=..., attachments=..., mode="open")— the resulting compose window now correctly shows both the rendered HTML content and the attachment side by side.