Skip to main content
Glama
jxnl
by jxnl

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;
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure. It mentions three operations but omits three others listed in the schema (mailboxes, accounts, latest). It doesn't describe authentication requirements, rate limits, side effects, or what happens when operations fail. For a multi-operation tool with 10 parameters, this is insufficient behavioral context.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately concise with a single sentence that front-loads the core purpose. It efficiently communicates the tool's scope without unnecessary elaboration. Every word earns its place, though it could be slightly more structured by explicitly listing all operations.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex tool with 10 parameters, 6 operations, and no annotations or output schema, the description is incomplete. It omits three operations entirely and provides no behavioral context about authentication, side effects, or error handling. The agent would struggle to use this tool effectively without trial and error.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, providing comprehensive parameter documentation. The description adds minimal value beyond the schema by listing three main operations, but doesn't explain parameter dependencies or operational semantics. The baseline of 3 is appropriate since the schema does the heavy lifting, though the description could have better explained how operations relate to parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose as interacting with Apple Mail app with specific operations (read unread emails, search emails, send emails). It distinguishes from sibling tools like calendar or contacts by focusing on email functionality. However, it doesn't explicitly differentiate from potential email-related tools that might exist elsewhere.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention prerequisites, appropriate contexts for different operations, or when other tools might be more suitable. The agent must infer usage solely from the operation names and parameter descriptions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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