Skip to main content
Glama

mail

Read unread emails, search email content, and send messages directly from the Apple Mail app. Manage mailboxes and accounts through automated operations.

Instructions

Interact with Apple Mail app - read unread emails, search emails, and send emails

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
operationYesOperation to perform: 'unread', 'search', 'send', 'mailboxes', 'accounts', or 'latest'
accountNoEmail account to use (optional - if not provided, searches across all accounts)
mailboxNoMailbox to use (optional - if not provided, uses inbox or searches across all mailboxes)
limitNoNumber of emails to retrieve (optional, for unread, search, and latest operations)
searchTermNoText to search for in emails (required for search operation)
toNoRecipient email address (required for send operation)
subjectNoEmail subject (required for send operation)
bodyNoEmail body content (required for send operation)
ccNoCC email address (optional for send operation)
bccNoBCC email address (optional for send operation)

Implementation Reference

  • Main execution handler for the 'mail' MCP tool. Loads utils/mail module and handles operations: unread (getUnreadMails), search (searchMails), send (sendMail), mailboxes/accounts (getMailboxes etc.). Includes custom AppleScript for account-specific unread emails.
    case "mail": { if (!isMailArgs(args)) { throw new Error("Invalid arguments for mail tool"); } try { const mailModule = await loadModule('mail'); switch (args.operation) { case "unread": { // If an account is specified, we'll try to search specifically in that account let emails; if (args.account) { console.error(`Getting unread emails for account: ${args.account}`); // Use AppleScript to get unread emails from specific account const script = ` tell application "Mail" set resultList to {} try set targetAccount to first account whose name is "${args.account.replace(/"/g, '\\"')}" -- Get mailboxes for this account set acctMailboxes to every mailbox of targetAccount -- If mailbox is specified, only search in that mailbox set mailboxesToSearch to acctMailboxes ${args.mailbox ? ` set mailboxesToSearch to {} repeat with mb in acctMailboxes if name of mb is "${args.mailbox.replace(/"/g, '\\"')}" then set mailboxesToSearch to {mb} exit repeat end if end repeat ` : ''} -- Search specified mailboxes repeat with mb in mailboxesToSearch try set unreadMessages to (messages of mb whose read status is false) if (count of unreadMessages) > 0 then set msgLimit to ${args.limit || 10} if (count of unreadMessages) < msgLimit then set msgLimit to (count of unreadMessages) end if repeat with i from 1 to msgLimit try set currentMsg to item i of unreadMessages set msgData to {subject:(subject of currentMsg), sender:(sender of currentMsg), ¬ date:(date sent of currentMsg) as string, mailbox:(name of mb)} -- Try to get content if possible try set msgContent to content of currentMsg if length of msgContent > 500 then set msgContent to (text 1 thru 500 of msgContent) & "..." end if set msgData to msgData & {content:msgContent} on error set msgData to msgData & {content:"[Content not available]"} end try set end of resultList to msgData on error -- Skip problematic messages end try end repeat if (count of resultList) ≥ ${args.limit || 10} then exit repeat end if on error -- Skip problematic mailboxes end try end repeat on error errMsg return "Error: " & errMsg end try return resultList end tell`; try { const asResult = await runAppleScript(script); if (asResult && asResult.startsWith('Error:')) { throw new Error(asResult); } // Parse the results - similar to general getUnreadMails const emailData = []; const matches = asResult.match(/\{([^}]+)\}/g); if (matches && matches.length > 0) { for (const match of matches) { try { const props = match.substring(1, match.length - 1).split(','); const email: any = {}; props.forEach(prop => { const parts = prop.split(':'); if (parts.length >= 2) { const key = parts[0].trim(); const value = parts.slice(1).join(':').trim(); email[key] = value; } }); if (email.subject || email.sender) { emailData.push({ subject: email.subject || "No subject", sender: email.sender || "Unknown sender", dateSent: email.date || new Date().toString(), content: email.content || "[Content not available]", isRead: false, mailbox: `${args.account} - ${email.mailbox || "Unknown"}` }); } } catch (parseError) { console.error('Error parsing email match:', parseError); } } } emails = emailData; } catch (error) { console.error('Error getting account-specific emails:', error); // Fallback to general method if specific account fails emails = await mailModule.getUnreadMails(args.limit); } } else { // No account specified, use the general method emails = await mailModule.getUnreadMails(args.limit); } return { content: [{ type: "text", text: emails.length > 0 ? `Found ${emails.length} unread email(s)${args.account ? ` in account "${args.account}"` : ''}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ''}:\n\n` + emails.map((email: any) => `[${email.dateSent}] From: ${email.sender}\nMailbox: ${email.mailbox}\nSubject: ${email.subject}\n${email.content.substring(0, 500)}${email.content.length > 500 ? '...' : ''}` ).join("\n\n") : `No unread emails found${args.account ? ` in account "${args.account}"` : ''}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ''}` }], isError: false }; } case "search": { if (!args.searchTerm) { throw new Error("Search term is required for search operation"); } const emails = await mailModule.searchMails(args.searchTerm, args.limit); return { content: [{ type: "text", text: emails.length > 0 ? `Found ${emails.length} email(s) for "${args.searchTerm}"${args.account ? ` in account "${args.account}"` : ''}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ''}:\n\n` + emails.map((email: any) => `[${email.dateSent}] From: ${email.sender}\nMailbox: ${email.mailbox}\nSubject: ${email.subject}\n${email.content.substring(0, 200)}${email.content.length > 200 ? '...' : ''}` ).join("\n\n") : `No emails found for "${args.searchTerm}"${args.account ? ` in account "${args.account}"` : ''}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ''}` }], isError: false }; } case "send": { if (!args.to || !args.subject || !args.body) { throw new Error("Recipient (to), subject, and body are required for send operation"); } const result = await mailModule.sendMail(args.to, args.subject, args.body, args.cc, args.bcc); return { content: [{ type: "text", text: result }], isError: false }; } case "mailboxes": { if (args.account) { const mailboxes = await mailModule.getMailboxesForAccount(args.account); return { content: [{ type: "text", text: mailboxes.length > 0 ? `Found ${mailboxes.length} mailboxes for account "${args.account}":\n\n${mailboxes.join("\n")}` : `No mailboxes found for account "${args.account}". Make sure the account name is correct.` }], isError: false }; } else { const mailboxes = await mailModule.getMailboxes(); return { content: [{ type: "text", text: mailboxes.length > 0 ? `Found ${mailboxes.length} mailboxes:\n\n${mailboxes.join("\n")}` : "No mailboxes found. Make sure Mail app is running and properly configured." }], isError: false }; } } case "accounts": { const accounts = await mailModule.getAccounts(); return { content: [{ type: "text", text: accounts.length > 0 ? `Found ${accounts.length} email accounts:\n\n${accounts.join("\n")}` : "No email accounts found. Make sure Mail app is configured with at least one account." }], isError: false }; } default: throw new Error(`Unknown operation: ${args.operation}`); } } catch (error) { return { content: [{ type: "text", text: `Error with mail operation: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }
  • Input schema and metadata for the 'mail' tool, defining parameters and operations.
    const MAIL_TOOL: Tool = { name: "mail", description: "Interact with Apple Mail app - read unread emails, search emails, and send emails", inputSchema: { type: "object", properties: { operation: { type: "string", description: "Operation to perform: 'unread', 'search', 'send', 'mailboxes', or 'accounts'", enum: ["unread", "search", "send", "mailboxes", "accounts"] }, account: { type: "string", description: "Email account to use (optional - if not provided, searches across all accounts)" }, mailbox: { type: "string", description: "Mailbox to use (optional - if not provided, uses inbox or searches across all mailboxes)" }, limit: { type: "number", description: "Number of emails to retrieve (optional, for unread and search operations)" }, searchTerm: { type: "string", description: "Text to search for in emails (required for search operation)" }, to: { type: "string", description: "Recipient email address (required for send operation)" }, subject: { type: "string", description: "Email subject (required for send operation)" }, body: { type: "string", description: "Email body content (required for send operation)" }, cc: { type: "string", description: "CC email address (optional for send operation)" }, bcc: { type: "string", description: "BCC email address (optional for send operation)" } }, required: ["operation"] } };
  • tools.ts:307-309 (registration)
    Registers the MAIL_TOOL in the exported tools array for MCP server list tools response.
    const tools = [CONTACTS_TOOL, NOTES_TOOL, MESSAGES_TOOL, MAIL_TOOL, REMINDERS_TOOL, WEB_SEARCH_TOOL, CALENDAR_TOOL, MAPS_TOOL];
  • Default export of mail helper functions invoked by the 'mail' tool handler.
    export default { getUnreadMails, searchMails, sendMail, getMailboxes, getAccounts, getMailboxesForAccount, };
  • Runtime input argument validation for 'mail' tool calls, enforcing schema constraints.
    function isMailArgs(args: unknown): args is { operation: "unread" | "search" | "send" | "mailboxes" | "accounts"; account?: string; mailbox?: string; limit?: number; searchTerm?: string; to?: string; subject?: string; body?: string; cc?: string; bcc?: string; } { if (typeof args !== "object" || args === null) return false; const { operation, account, mailbox, limit, searchTerm, to, subject, body, cc, bcc } = args as any; if (!operation || !["unread", "search", "send", "mailboxes", "accounts"].includes(operation)) { return false; } // Validate required fields based on operation switch (operation) { case "search": if (!searchTerm || typeof searchTerm !== "string") return false; break; case "send": if (!to || typeof to !== "string" || !subject || typeof subject !== "string" || !body || typeof body !== "string") return false; break; case "unread": case "mailboxes": case "accounts": // No additional required fields break; } // Validate field types if present if (account && typeof account !== "string") return false; if (mailbox && typeof mailbox !== "string") return false; if (limit && typeof limit !== "number") return false; if (cc && typeof cc !== "string") return false; if (bcc && typeof bcc !== "string") return false; return true; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jxnl/apple-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server