Files
apple-mcp/apps/mail.py
T
Eric Jungbauer 7e619d0454 Initial commit — Apple Apps MCP server with native macOS config app
Python MCP server (FastMCP) providing Claude Code/CoWork access to Apple
Reminders, Calendar, Mail, Contacts, Find My, and Maps via AppleScript.
Includes a native SwiftUI config/installer app and a compiled EventKit
helper for fast reminder queries on large databases (8,000+ items).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:32:24 -06:00

271 lines
9.6 KiB
Python

"""Apple Mail tools."""
from helpers import run_applescript, safe_applescript_string, parse_applescript_date
def list_mailboxes() -> list[dict]:
"""List all mail accounts and their mailboxes."""
script = '''
tell application "Mail"
set output to ""
repeat with acct in accounts
set acctName to name of acct
repeat with mb in mailboxes of acct
set mbName to name of mb
set msgCount to count of messages of mb
set output to output & acctName & ">>>" & mbName & "|||" & (msgCount as string) & linefeed
end repeat
end repeat
return output
end tell'''
raw = run_applescript(script, timeout=30)
results = []
for line in raw.strip().split("\n"):
if not line.strip():
continue
parts = line.split("|||")
if len(parts) < 2:
continue
name_part = parts[0]
account = None
if ">>>" in name_part:
account, name_part = name_part.split(">>>", 1)
results.append({
"account": account.strip() if account else None,
"mailbox": name_part.strip(),
"message_count": int(parts[1].strip()) if parts[1].strip().isdigit() else 0,
})
return results
def get_messages(mailbox_name: str = "INBOX", account_name: str | None = None,
count: int = 20) -> list[dict]:
"""Get recent messages from a mailbox.
Args:
mailbox_name: Mailbox name (default INBOX).
account_name: Specific account. If None, uses first account.
count: Number of recent messages to fetch.
"""
safe_mb = safe_applescript_string(mailbox_name)
if account_name:
safe_acct = safe_applescript_string(account_name)
mb_ref = f'mailbox "{safe_mb}" of account "{safe_acct}"'
else:
mb_ref = f'inbox'
if mailbox_name != "INBOX":
mb_ref = f'mailbox "{safe_mb}" of first account'
script = f'''
tell application "Mail"
set output to ""
set theMB to {mb_ref}
set msgList to messages 1 through {count} of theMB
repeat with m in msgList
set mSubject to subject of m
set mFrom to sender of m
set mDate to (date received of m) as string
set mRead to read status of m as string
set mId to message id of m
set output to output & mSubject & "|||" & mFrom & "|||" & mDate & "|||" & mRead & "|||" & mId & linefeed
end repeat
return output
end tell'''
raw = run_applescript(script, timeout=30)
results = []
for line in raw.strip().split("\n"):
if not line.strip():
continue
parts = line.split("|||")
if len(parts) < 5:
continue
results.append({
"subject": parts[0].strip(),
"from": parts[1].strip(),
"date": parse_applescript_date(parts[2].strip()),
"read": parts[3].strip() == "true",
"message_id": parts[4].strip(),
})
return results
def get_message_content(subject: str, mailbox_name: str = "INBOX",
account_name: str | None = None) -> dict:
"""Get full content of a specific message by subject."""
safe_subject = safe_applescript_string(subject)
if account_name:
safe_acct = safe_applescript_string(account_name)
safe_mb = safe_applescript_string(mailbox_name)
mb_ref = f'mailbox "{safe_mb}" of account "{safe_acct}"'
else:
mb_ref = "inbox" if mailbox_name == "INBOX" else f'mailbox "{safe_applescript_string(mailbox_name)}" of first account'
script = f'''
tell application "Mail"
set theMB to {mb_ref}
set msgs to (messages of theMB whose subject contains "{safe_subject}")
if (count of msgs) > 0 then
set m to item 1 of msgs
set mSubject to subject of m
set mFrom to sender of m
set mTo to ""
try
set recipList to to recipients of m
set mTo to ""
repeat with r in recipList
set mTo to mTo & (address of r) & ", "
end repeat
end try
set mDate to (date received of m) as string
set mContent to content of m
set mRead to read status of m as string
return mSubject & "|||" & mFrom & "|||" & mTo & "|||" & mDate & "|||" & mRead & "|||" & mContent
else
return "NOT_FOUND"
end if
end tell'''
raw = run_applescript(script, timeout=30)
if raw == "NOT_FOUND":
raise RuntimeError(f"Message with subject containing '{subject}' not found")
parts = raw.split("|||", 5)
if len(parts) < 6:
raise RuntimeError("Failed to parse message")
return {
"subject": parts[0].strip(),
"from": parts[1].strip(),
"to": parts[2].strip().rstrip(", ") or None,
"date": parse_applescript_date(parts[3].strip()),
"read": parts[4].strip() == "true",
"content": parts[5].strip(),
}
def search_mail(query: str, mailbox_name: str = "INBOX", count: int = 20) -> list[dict]:
"""Search mail by subject or sender."""
safe_query = safe_applescript_string(query)
mb_ref = "inbox" if mailbox_name == "INBOX" else f'mailbox "{safe_applescript_string(mailbox_name)}" of first account'
script = f'''
tell application "Mail"
set output to ""
set theMB to {mb_ref}
set msgs to (messages of theMB whose subject contains "{safe_query}" or sender contains "{safe_query}")
set maxCount to {count}
set i to 0
repeat with m in msgs
if i >= maxCount then exit repeat
set mSubject to subject of m
set mFrom to sender of m
set mDate to (date received of m) as string
set mRead to read status of m as string
set output to output & mSubject & "|||" & mFrom & "|||" & mDate & "|||" & mRead & linefeed
set i to i + 1
end repeat
return output
end tell'''
raw = run_applescript(script, timeout=60)
results = []
for line in raw.strip().split("\n"):
if not line.strip():
continue
parts = line.split("|||")
if len(parts) < 4:
continue
results.append({
"subject": parts[0].strip(),
"from": parts[1].strip(),
"date": parse_applescript_date(parts[2].strip()),
"read": parts[3].strip() == "true",
})
return results
def send_mail(to: str, subject: str, body: str, cc: str | None = None,
from_account: str | None = None) -> dict:
"""Send an email."""
safe_to = safe_applescript_string(to)
safe_subject = safe_applescript_string(subject)
safe_body = safe_applescript_string(body)
cc_line = ""
if cc:
cc_addrs = [a.strip() for a in cc.split(",")]
cc_lines = "\n".join([f'make new cc recipient at end of cc recipients with properties {{address:"{safe_applescript_string(a)}"}}' for a in cc_addrs])
cc_line = cc_lines
account_line = ""
if from_account:
account_line = f', sender:"{safe_applescript_string(from_account)}"'
to_addrs = [a.strip() for a in to.split(",")]
to_lines = "\n".join([f'make new to recipient at end of to recipients with properties {{address:"{safe_applescript_string(a)}"}}' for a in to_addrs])
script = f'''
tell application "Mail"
set newMsg to make new outgoing message with properties {{subject:"{safe_subject}", content:"{safe_body}", visible:false{account_line}}}
tell newMsg
{to_lines}
{cc_line}
end tell
send newMsg
return "sent"
end tell'''
run_applescript(script, timeout=30)
return {"to": to, "subject": subject, "sent": True}
def mark_read(subject: str, mailbox_name: str = "INBOX", read: bool = True) -> dict:
"""Mark a message as read or unread."""
safe_subject = safe_applescript_string(subject)
mb_ref = "inbox" if mailbox_name == "INBOX" else f'mailbox "{safe_applescript_string(mailbox_name)}" of first account'
read_val = "true" if read else "false"
script = f'''
tell application "Mail"
set theMB to {mb_ref}
set msgs to (messages of theMB whose subject contains "{safe_subject}")
if (count of msgs) > 0 then
set read status of item 1 of msgs to {read_val}
return "done"
else
return "not_found"
end if
end tell'''
result = run_applescript(script)
if result == "done":
return {"subject": subject, "read": read}
raise RuntimeError(f"Message '{subject}' not found")
def move_message(subject: str, from_mailbox: str, to_mailbox: str,
account_name: str | None = None) -> dict:
"""Move a message to a different mailbox."""
safe_subject = safe_applescript_string(subject)
safe_from = safe_applescript_string(from_mailbox)
safe_to = safe_applescript_string(to_mailbox)
if account_name:
safe_acct = safe_applescript_string(account_name)
from_ref = f'mailbox "{safe_from}" of account "{safe_acct}"'
to_ref = f'mailbox "{safe_to}" of account "{safe_acct}"'
else:
from_ref = "inbox" if from_mailbox == "INBOX" else f'mailbox "{safe_from}" of first account'
to_ref = f'mailbox "{safe_to}" of first account'
script = f'''
tell application "Mail"
set fromMB to {from_ref}
set toMB to {to_ref}
set msgs to (messages of fromMB whose subject contains "{safe_subject}")
if (count of msgs) > 0 then
move item 1 of msgs to toMB
return "moved"
else
return "not_found"
end if
end tell'''
result = run_applescript(script)
if result == "moved":
return {"subject": subject, "from": from_mailbox, "to": to_mailbox, "moved": True}
raise RuntimeError(f"Message '{subject}' not found in {from_mailbox}")