"""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}")