Skip to content

Fix: add localized inbox mailbox fallback for non-English systems#30

Open
na-bal wants to merge 41 commits intopatrickfreyer:mainfrom
na-bal:fix/localized-mailbox-fallback
Open

Fix: add localized inbox mailbox fallback for non-English systems#30
na-bal wants to merge 41 commits intopatrickfreyer:mainfrom
na-bal:fix/localized-mailbox-fallback

Conversation

@na-bal
Copy link
Copy Markdown
Contributor

@na-bal na-bal commented Mar 26, 2026

Summary

  • The inbox mailbox fallback only tries "INBOX" and "Inbox", but Exchange and other mail servers on localized macOS use translated names
  • Added fallback that iterates mailboxes to find inbox by localized name when standard lookup fails
  • Supports: Russian ("Входящие"), German ("Posteingang"), French ("Boîte de réception"), Spanish ("Bandeja de entrada"), Japanese ("受信トレイ"), Chinese ("收件箱")
  • Updated inbox_mailbox_script() in core.py and all hardcoded fallbacks across tools/

Test plan

  • Verify inbox discovery works on English macOS (INBOX/Inbox)
  • Verify inbox discovery works on localized macOS (e.g. Russian Exchange with "Входящие")
  • Verify non-inbox mailbox operations are unaffected

🤖 Generated with Claude Code

patrickfreyer and others added 30 commits February 9, 2026 23:19
…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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
patrickfreyer and others added 10 commits March 11, 2026 23:06
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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) <noreply@anthropic.com>
…tured-search

feat: add rich draft workflow and structured mail search
The inbox mailbox fallback only tried "INBOX" and "Inbox", but Exchange
and other mail servers with localized macOS use translated names:
- Russian: "Входящие"
- German: "Posteingang"
- French: "Boîte de réception"
- Spanish: "Bandeja de entrada"
- Japanese: "受信トレイ"
- Chinese: "收件箱"

Updated `inbox_mailbox_script()` in core.py and all hardcoded "Inbox"
fallbacks across tools to iterate mailboxes and match localized names
when the standard INBOX/Inbox lookup fails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Owner

@patrickfreyer patrickfreyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@na-bal Thanks for tackling this — localized inbox support is a real need for non-English users. However, the current approach has a major DRY issue:

The same 14-line AppleScript fallback block is copy-pasted 12 times across 7 files (+247 lines). Meanwhile, core.py already has the centralized inbox_mailbox_script() helper which you correctly updated — but the other call sites duplicate the logic inline instead of using it.

Ask: Could you refactor the inline duplicates to call the shared inbox_mailbox_script() from core.py? That would:

  • Cut the PR from ~+247 to ~+40 lines
  • Make adding new locales a one-line change in one place
  • Eliminate 12 copies that would drift out of sync over time

Happy to merge once that's cleaned up!

…uild_mailbox_ref()

Address review feedback on PR patrickfreyer#30 — eliminate DRY violation by replacing
all inline AppleScript fallback blocks with calls to the centralized
build_mailbox_ref() helper in core.py.

Changes:
- Enhance build_mailbox_ref() in core.py with full localized inbox
  fallback (Входящие, Posteingang, Boîte de réception, etc.)
- Replace 15 inline duplicates across 6 tool files with build_mailbox_ref() calls
- Remove redundant _mailbox_fallback_script() from bulk.py
- Fix incomplete fallback in search_all_accounts() via inbox_mailbox_script()

Note: build_mailbox_ref() is used instead of inbox_mailbox_script() because
the inline blocks handle arbitrary mailbox names (not just INBOX).
inbox_mailbox_script() remains for INBOX-only call sites.

Result: +48/-366 lines. Adding a new locale is now a one-line change in core.py.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@na-bal
Copy link
Copy Markdown
Contributor Author

na-bal commented Mar 31, 2026

Hi @patrickfreyer! Refactored as requested — replaced all 15 inline duplicates with calls to the shared build_mailbox_ref() from core.py (+48/−366 lines).

Used build_mailbox_ref() instead of inbox_mailbox_script() because the inline blocks handle arbitrary mailbox names (not just INBOX). inbox_mailbox_script() remains for INBOX-only call sites. Adding a new locale is now a one-line change in core.py.

Ready for review!

P.S. My user asked me to send you a neural joke, so here goes:

Why did the neural network break up with the decision tree?
Because it found someone with deeper layers and fewer conditions. 🌳💔🧠

— Claude

Copy link
Copy Markdown
Owner

@patrickfreyer patrickfreyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Thanks for the contribution! The localized inbox fallback is a real improvement for non-English macOS users. However, this PR has grown far beyond the stated scope and has several issues that need to be addressed before merging.


1. Massive scope creep — please split into separate PRs

The title says "Fix: add localized inbox mailbox fallback for non-English systems" but this PR contains 4,284 additions across 20 files and 41 commits including:

  • Localized inbox fallback (the stated fix)
  • New create_rich_email_draft tool with .eml generation and clipboard-based HTML injection
  • New bulk.py module (3 new tools: mark_emails, delete_emails, bulk_move_emails)
  • New smart_inbox.py module (3 new tools: get_awaiting_reply, get_needs_response, get_top_senders)
  • --read-only CLI mode
  • Complete rewrite of search.py with structured JSON output, pagination, message:// links
  • Attachment support for compose_email and reply_to_email
  • HTML email sending via NSPasteboard clipboard injection
  • Security hardening (path validation, bulk operation filter requirements, export caps)
  • Version bump to 2.0.0
  • Removal of get_email_with_content tool (breaking change)

Each of these is a meaningful feature or fix that deserves its own focused PR with targeted review and testing. As a single PR, this is very difficult to review thoroughly and carries high risk of introducing regressions.

Recommendation: Split into ~5-6 focused PRs (localized fallback fix, security hardening, bulk tools, smart inbox tools, rich draft/HTML email, search rewrite).


2. Bug: move_email in manage.py has malformed AppleScript

The refactoring to use build_mailbox_ref() appears to have left dangling else/end if/end try fragments from the old inline try/catch:

{build_mailbox_ref(from_mailbox, "targetAccount", "sourceMailbox")}
                    else
                        error "Source mailbox not found"
                    end if
                end try

build_mailbox_ref() already generates its own try/on error/end try block, so this leftover code will produce an AppleScript syntax error at runtime, breaking the move_email tool entirely.


3. _sanitize_for_json strips all non-ASCII — contradicts the localization goal

text = text.encode("ascii", "replace").decode("ascii")

This replaces every non-ASCII character with ?. For a PR that adds support for Russian ("Входящие"), German ("Posteingang"), French ("Boîte de réception"), Japanese ("受信トレイ"), and Chinese ("收件箱") inbox names, this is contradictory — all non-English email content returned by run_applescript() will have its characters destroyed. A user with a Russian inbox will see their email subjects/senders as sequences of ?.

Consider using a more targeted approach that only strips actual control characters while preserving valid Unicode text.


4. __main__.py is missing new module imports

The new apple_mail_mcp/__main__.py imports inbox, search, compose, manage, analytics but does not import bulk or smart_inbox. Running python -m apple_mail_mcp will not register the 6 new tools from those modules.


5. HTML email via clipboard injection (_send_html_email) is fragile

The approach of tabbing 7 times through Mail.app's header fields to reach the body, then using Cmd+V to paste, is extremely brittle:

  • The number of tab stops depends on whether CC/BCC fields are visible
  • It depends on Mail.app's exact UI layout, which can change across macOS versions
  • It clobbers the user's system clipboard
  • It requires Accessibility permissions (System Events)
  • A 2.5-second hard delay is baked in

This approach will likely break for many users. Consider documenting this as experimental, or restricting to the .eml-based workflow which is more reliable.


6. Breaking change: get_email_with_content removed

The get_email_with_content tool is deleted without a migration path or deprecation. Any existing MCP clients relying on this tool will break. The manifest also removes it. This should at minimum be called out prominently and ideally handled in a separate PR.


7. Inconsistent dry_run defaults

  • delete_emails: dry_run=True (safe default ✓)
  • bulk_move_emails: dry_run=False (destructive default ✗)

Both are destructive bulk operations. bulk_move_emails should also default to dry_run=True for consistency and safety.


8. Merge conflicts

The PR's mergeable_state is dirty, meaning there are merge conflicts with the base branch that need to be resolved.


Minor observations

  • The get_awaiting_reply tool assumes sent messages are in reverse chronological order (exit repeat on date cutoff), which may not hold for all mail providers.
  • The reply_to_email "open" mode uses clipboard paste via System Events — same fragility concerns as _send_html_email.
  • Good: Security hardening for save_email_attachment path validation, bulk operation filter requirements, and export_emails cap are all welcome improvements.
  • Good: The temp-file approach for reply body in reply_to_email is a solid fix for AppleScript escaping issues.

Summary

The localized inbox fallback and security hardening are valuable improvements. However, the PR bundles too many unrelated changes, has a critical bug in move_email, and the ASCII-only sanitization undermines the localization goal. Please split this into focused PRs and address the issues above.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants