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;
    }

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