Read unread emails, search messages, and send emails directly from the Apple Mail app. Manage mailboxes, accounts, and retrieve latest emails with customizable options for efficient email handling.
Instructions
Interact with Apple Mail app - read unread emails, search emails, and send emails
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| account | No | Email account to use (optional - if not provided, searches across all accounts) | |
| bcc | No | BCC email address (optional for send operation) | |
| body | No | Email body content (required for send operation) | |
| cc | No | CC email address (optional for send operation) | |
| limit | No | Number of emails to retrieve (optional, for unread, search, and latest operations) | |
| mailbox | No | Mailbox to use (optional - if not provided, uses inbox or searches across all mailboxes) | |
| operation | Yes | Operation to perform: 'unread', 'search', 'send', 'mailboxes', 'accounts', or 'latest' | |
| searchTerm | No | Text to search for in emails (required for search operation) | |
| subject | No | Email subject (required for send operation) | |
| to | No | Recipient email address (required for send operation) |
Input Schema (JSON Schema)
{
"properties": {
"account": {
"description": "Email account to use (optional - if not provided, searches across all accounts)",
"type": "string"
},
"bcc": {
"description": "BCC email address (optional for send operation)",
"type": "string"
},
"body": {
"description": "Email body content (required for send operation)",
"type": "string"
},
"cc": {
"description": "CC email address (optional for send operation)",
"type": "string"
},
"limit": {
"description": "Number of emails to retrieve (optional, for unread, search, and latest operations)",
"type": "number"
},
"mailbox": {
"description": "Mailbox to use (optional - if not provided, uses inbox or searches across all mailboxes)",
"type": "string"
},
"operation": {
"description": "Operation to perform: 'unread', 'search', 'send', 'mailboxes', 'accounts', or 'latest'",
"enum": [
"unread",
"search",
"send",
"mailboxes",
"accounts",
"latest"
],
"type": "string"
},
"searchTerm": {
"description": "Text to search for in emails (required for search operation)",
"type": "string"
},
"subject": {
"description": "Email subject (required for send operation)",
"type": "string"
},
"to": {
"description": "Recipient email address (required for send operation)",
"type": "string"
}
},
"required": [
"operation"
],
"type": "object"
}
Implementation Reference
- tools.ts:81-131 (schema)Input schema definition for the 'mail' MCP tool, detailing parameters for operations: unread, search, send, mailboxes, accounts.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:308-310 (registration)Registration of the 'mail' tool (as MAIL_TOOL) in the array of MCP tools that is exported for use in the server.const tools = [CONTACTS_TOOL, NOTES_TOOL, MESSAGES_TOOL, MAIL_TOOL, REMINDERS_TOOL, WEB_SEARCH_TOOL, CALENDAR_TOOL, MAPS_TOOL]; export default tools;
- utils/mail.ts:68-348 (helper)Helper function to retrieve unread emails from Apple Mail, used for 'unread' operation in mail tool.async function getUnreadMails(limit: number = 10): Promise<EmailMessage[]> { try { if (!(await checkMailAccess())) { return []; } // First, try with AppleScript which might be more reliable for this case try { const script = ` tell application "Mail" set allMailboxes to every mailbox set resultList to {} -- First find account mailboxes with unread messages repeat with m in allMailboxes try set unreadMessages to (messages of m whose read status is false) if (count of unreadMessages) > 0 then set msgLimit to ${limit} 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 -- Basic email info set msgData to {subject:(subject of currentMsg), sender:(sender of currentMsg), ¬ date:(date sent of currentMsg) as string, mailbox:(name of m)} -- 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) ≥ ${limit} then exit repeat end if on error -- Skip problematic mailboxes end try end repeat return resultList end tell`; const asResult = await runAppleScript(script); // If we got results, parse them if (asResult && asResult.toString().trim().length > 0) { try { // Try to parse as JSON if the result looks like JSON if (asResult.startsWith("{") || asResult.startsWith("[")) { const parsedResults = JSON.parse(asResult); if (Array.isArray(parsedResults) && parsedResults.length > 0) { return parsedResults.map((msg) => ({ subject: msg.subject || "No subject", sender: msg.sender || "Unknown sender", dateSent: msg.date || new Date().toString(), content: msg.content || "[Content not available]", isRead: false, // These are all unread by definition mailbox: msg.mailbox || "Unknown mailbox", })); } } // If it's not in JSON format, try to parse the plist/record format const parsedEmails: EmailMessage[] = []; // Very simple parsing for the record format that AppleScript might return // This is a best-effort attempt and might not be perfect const matches = asResult.match(/\{([^}]+)\}/g); if (matches && matches.length > 0) { matches.forEach((match) => { try { // Parse key-value pairs const props = match.substring(1, match.length - 1).split(","); const emailData: any = {}; props.forEach((prop) => { const parts = prop.split(":"); if (parts.length >= 2) { const key = parts[0].trim(); const value = parts.slice(1).join(":").trim(); emailData[key] = value; } }); if (emailData.subject || emailData.sender) { parsedEmails.push({ subject: emailData.subject || "No subject", sender: emailData.sender || "Unknown sender", dateSent: emailData.date || new Date().toString(), content: emailData.content || "[Content not available]", isRead: false, mailbox: emailData.mailbox || "Unknown mailbox", }); } } catch (parseError) { console.error("Error parsing email match:", parseError); } }); } if (parsedEmails.length > 0) { return parsedEmails; } } catch (parseError) { console.error("Error parsing AppleScript result:", parseError); // If parsing failed, continue to the JXA approach } } // If the raw result contains useful info but parsing failed if ( asResult && asResult.includes("subject") && asResult.includes("sender") ) { console.error("Returning raw AppleScript result for debugging"); return [ { subject: "Raw AppleScript Output", sender: "Mail System", dateSent: new Date().toString(), content: `Could not parse Mail data properly. Raw output: ${asResult}`, isRead: false, mailbox: "Debug", }, ]; } } catch (asError) { // Continue to JXA approach as fallback } console.error("Trying JXA approach for unread emails..."); // Check Mail accounts as a different approach const accounts = await runAppleScript(` tell application "Mail" set accts to {} repeat with a in accounts set end of accts to name of a end repeat return accts end tell`); console.error("Available accounts:", accounts); // Try using direct AppleScript to check for unread messages across all accounts const unreadInfo = await runAppleScript(` tell application "Mail" set unreadInfo to {} repeat with m in every mailbox try set unreadCount to count (messages of m whose read status is false) if unreadCount > 0 then set end of unreadInfo to {name of m, unreadCount} end if on error -- Skip error mailboxes end try end repeat return unreadInfo end tell`); console.error("Mailboxes with unread messages:", unreadInfo); // Fallback to JXA approach const unreadMails: EmailMessage[] = await run((limit: number) => { const Mail = Application("Mail"); const results = []; try { // Get all accounts first const accounts = Mail.accounts(); console.error(`Found ${accounts.length} accounts`); // Go through all accounts to find mailboxes for (const account of accounts) { try { const accountName = account.name(); console.error(`Processing account: ${accountName}`); // Try to get mailboxes for this account try { const accountMailboxes = account.mailboxes(); console.error( `Account ${accountName} has ${accountMailboxes.length} mailboxes`, ); // Process each mailbox for (const mailbox of accountMailboxes) { try { const boxName = mailbox.name(); console.error(`Checking ${boxName} in ${accountName}`); // Try to get unread messages let unreadMessages; try { unreadMessages = mailbox.messages.whose({ readStatus: false, })(); console.error( `Found ${unreadMessages.length} unread in ${boxName}`, ); // Process unread messages const count = Math.min( unreadMessages.length, limit - results.length, ); for (let i = 0; i < count; i++) { try { const msg = unreadMessages[i]; results.push({ subject: msg.subject(), sender: msg.sender(), dateSent: msg.dateSent().toString(), content: msg.content() ? msg.content().substring(0, 500) : "[No content]", isRead: false, mailbox: `${accountName} - ${boxName}`, }); } catch (msgError) { console.error(`Error with message: ${msgError}`); } } } catch (unreadError) { console.error( `Error getting unread for ${boxName}: ${unreadError}`, ); } } catch (boxError) { console.error(`Error with mailbox: ${boxError}`); } // If we have enough messages, stop if (results.length >= limit) { break; } } } catch (mbError) { console.error( `Error getting mailboxes for ${accountName}: ${mbError}`, ); } // If we have enough messages, stop if (results.length >= limit) { break; } } catch (accError) { console.error(`Error with account: ${accError}`); } } } catch (error) { console.error(`General error: ${error}`); } console.error(`Returning ${results.length} unread messages`); return results; }, limit); return unreadMails; } catch (error) { console.error("Error in getUnreadMails:", error); throw new Error( `Error accessing mail: ${error instanceof Error ? error.message : String(error)}`, ); } }
- utils/mail.ts:350-512 (helper)Helper function to search emails in Apple Mail by search term, used for 'search' operation.async function searchMails( searchTerm: string, limit: number = 10, ): Promise<EmailMessage[]> { try { if (!(await checkMailAccess())) { return []; } // Ensure Mail app is running await runAppleScript(` if application "Mail" is not running then tell application "Mail" to activate delay 2 end if`); // First try the AppleScript approach which might be more reliable try { const script = ` tell application "Mail" set searchString to "${searchTerm.replace(/"/g, '\\"')}" set foundMsgs to {} set allBoxes to every mailbox repeat with currentBox in allBoxes try set boxMsgs to (messages of currentBox whose (subject contains searchString) or (content contains searchString)) set foundMsgs to foundMsgs & boxMsgs if (count of foundMsgs) ≥ ${limit} then exit repeat on error -- Skip mailboxes that error out end try end repeat set resultList to {} set msgCount to (count of foundMsgs) if msgCount > ${limit} then set msgCount to ${limit} repeat with i from 1 to msgCount try set currentMsg to item i of foundMsgs set msgInfo to {subject:subject of currentMsg, sender:sender of currentMsg, ¬ date:(date sent of currentMsg) as string, isRead:read status of currentMsg, ¬ boxName:name of (mailbox of currentMsg)} set end of resultList to msgInfo on error -- Skip messages that error out end try end repeat return resultList end tell`; const asResult = await runAppleScript(script); // If we got results, parse them if (asResult && asResult.length > 0) { try { const parsedResults = JSON.parse(asResult); if (Array.isArray(parsedResults) && parsedResults.length > 0) { return parsedResults.map((msg) => ({ subject: msg.subject || "No subject", sender: msg.sender || "Unknown sender", dateSent: msg.date || new Date().toString(), content: "[Content not available through AppleScript method]", isRead: msg.isRead || false, mailbox: msg.boxName || "Unknown mailbox", })); } } catch (parseError) { console.error("Error parsing AppleScript result:", parseError); // Continue to JXA approach if parsing fails } } } catch (asError) { // Continue to JXA approach } // JXA approach as fallback const searchResults: EmailMessage[] = await run( (searchTerm: string, limit: number) => { const Mail = Application("Mail"); const results = []; console.error(`Searching for "${searchTerm}" in mailboxes...`); // Search in the most common mailboxes try { const mailboxes = Mail.mailboxes(); console.error(`Found ${mailboxes.length} mailboxes to search`); for (const mailbox of mailboxes) { try { console.error(`Searching in mailbox: ${mailbox.name()}`); // Try to find messages with the search term in subject or content let messages; try { messages = mailbox.messages.whose({ _or: [ { subject: { _contains: searchTerm } }, { content: { _contains: searchTerm } }, ], })(); console.error( `Found ${messages.length} matching messages in ${mailbox.name()}`, ); } catch (queryError) { console.error( `Error querying messages in ${mailbox.name()}:`, queryError, ); continue; } // Take only the most recent messages up to the limit const count = Math.min(messages.length, limit); for (let i = 0; i < count; i++) { try { const msg = messages[i]; results.push({ subject: msg.subject(), sender: msg.sender(), dateSent: msg.dateSent().toString(), content: msg.content() ? msg.content().substring(0, 500) : "[No content]", // Limit content length isRead: msg.readStatus(), mailbox: mailbox.name(), }); } catch (msgError) { console.error("Error processing message:", msgError); } } if (results.length >= limit) { break; } } catch (boxError) { console.error(`Error with mailbox ${mailbox.name()}:`, boxError); } } } catch (mbError) { console.error("Error getting mailboxes:", mbError); } console.error(`Returning ${results.length} search results`); return results.slice(0, limit); }, searchTerm, limit, ); return searchResults; } catch (error) { console.error("Error in searchMails:", error); throw new Error( `Error searching mail: ${error instanceof Error ? error.message : String(error)}`, ); } }
- utils/mail.ts:514-622 (helper)Helper function to send an email via Apple Mail, used for 'send' operation.async function sendMail( to: string, subject: string, body: string, cc?: string, bcc?: string, ): Promise<string | undefined> { try { if (!(await checkMailAccess())) { throw new Error("Could not access Mail app"); } // Ensure Mail app is running await runAppleScript(` if application "Mail" is not running then tell application "Mail" to activate delay 2 end if`); // Escape special characters in strings for AppleScript const escapedTo = to.replace(/"/g, '\\"'); const escapedSubject = subject.replace(/"/g, '\\"'); const escapedBody = body.replace(/"/g, '\\"'); const escapedCc = cc ? cc.replace(/"/g, '\\"') : ""; const escapedBcc = bcc ? bcc.replace(/"/g, '\\"') : ""; let script = ` tell application "Mail" set newMessage to make new outgoing message with properties {subject:"${escapedSubject}", content:"${escapedBody}", visible:true} tell newMessage make new to recipient with properties {address:"${escapedTo}"} `; if (cc) { script += ` make new cc recipient with properties {address:"${escapedCc}"}\n`; } if (bcc) { script += ` make new bcc recipient with properties {address:"${escapedBcc}"}\n`; } script += ` end tell send newMessage return "success" end tell `; try { const result = await runAppleScript(script); if (result === "success") { return `Email sent to ${to} with subject "${subject}"`; } else { } } catch (asError) { console.error("Error in AppleScript send:", asError); const jxaResult: string = await run( (to, subject, body, cc, bcc) => { try { const Mail = Application("Mail"); const msg = Mail.OutgoingMessage().make(); msg.subject = subject; msg.content = body; msg.visible = true; // Add recipients const toRecipient = Mail.ToRecipient().make(); toRecipient.address = to; msg.toRecipients.push(toRecipient); if (cc) { const ccRecipient = Mail.CcRecipient().make(); ccRecipient.address = cc; msg.ccRecipients.push(ccRecipient); } if (bcc) { const bccRecipient = Mail.BccRecipient().make(); bccRecipient.address = bcc; msg.bccRecipients.push(bccRecipient); } msg.send(); return "JXA send completed"; } catch (error) { return `JXA error: ${error}`; } }, to, subject, body, cc, bcc, ); if (jxaResult.startsWith("JXA error:")) { throw new Error(jxaResult); } return `Email sent to ${to} with subject "${subject}"`; } } catch (error) { console.error("Error in sendMail:", error); throw new Error( `Error sending mail: ${error instanceof Error ? error.message : String(error)}`, ); } }