--- name: mail-automation description: Use when the user asks to "automate Apple Mail", "read emails programmatically", "create email drafts", "check unread count", "send email from Mail app", "access mailboxes", "search inbox", "filter messages", or mentions Mail.app automation, email scripting, or macOS email workflows. metadata: version: "1.0.0" --- # Apple Mail automation Automate Apple Mail using JXA (JavaScript for Automation) for reading, composing, searching, and managing email programmatically. ## Overview Apple Mail provides a complete scripting dictionary accessible via JXA, enabling full programmatic control of email operations. Use this skill to build email automation, daily briefings, email triage systems, and integration with other applications. ## Prerequisites - **Mail.app** configured with at least one email account - **Automation permissions** granted in System Settings → Privacy & Security → Automation - **Mail.app running** (most operations require the app to be open) - **macos-automation-core** loaded for `runJXA()` pattern ## Core capabilities | Category | Operations | JXA Support | |----------|------------|-------------| | Reading | Access inbox/mailboxes, read properties, check status | Yes Full | | Composing | Create drafts, set recipients, add content | Yes Full | | Sending | Send messages programmatically | Yes Full | | Searching | Filter by date, status, sender, subject | Yes Full | | Accounts | List accounts, access properties | Yes Full | | Attachments | Read attachment metadata, access files | Yes Full | ## Quick reference ### Common operations | Task | JXA Pattern | |------|-------------| | Get unread count | `Mail.inbox().unreadCount()` | | Get all messages | `Mail.inbox().messages()` | | Filter unread | `messages.filter(m => !m.readStatus())` | | Get subject | `message.subject()` | | Get sender | `message.sender()` | | Get date | `message.dateReceived()` | | Get content | `message.content()` | | Create draft | `Mail.OutgoingMessage({...})` | | Add recipient | `msg.toRecipients.push(...)` | | Send message | `msg.send()` | ### Mailbox access ```javascript const Mail = Application('Mail') // Access inbox const inbox = Mail.inbox() // Access mailbox by name const mailbox = Mail.mailboxes.byName('Archive') // List all mailboxes const allMailboxes = Mail.mailboxes() ``` ## TypeScript integration Recommended pattern using type-safe wrapper: ```typescript import { runJXA } from './jxa-runner' interface Email { id: string subject: string sender: string date: string content: string read: boolean } async function getRecentEmails(limit: number = 10): Promise { const script = ` const Mail = Application('Mail') const messages = Mail.inbox().messages() return JSON.stringify( messages.slice(0, ${limit}).map(msg => ({ id: msg.messageId() || msg.id().toString(), subject: msg.subject() || '(No subject)', sender: msg.sender(), date: msg.dateReceived().toISOString(), content: msg.content() || '', read: msg.readStatus() })) ) ` return await runJXA(script) } ``` ## Common patterns ### Pattern 1: check unread count ```typescript async function getUnreadCount(): Promise { const script = ` const Mail = Application('Mail') return Mail.inbox().unreadCount() ` return await runJXA(script) } ``` ### Pattern 2: read recent messages ```typescript interface MessageSummary { subject: string sender: string date: string read: boolean } async function getRecentMessages(limit: number = 10): Promise { const script = ` const Mail = Application('Mail') const messages = Mail.inbox().messages() return JSON.stringify( messages.slice(0, ${limit}).map(msg => ({ subject: msg.subject(), sender: msg.sender(), date: msg.dateReceived().toISOString(), read: msg.readStatus() })) ) ` return await runJXA(script) } ``` ### Pattern 3: filter by criteria ```typescript async function getUnreadMessages(limit: number = 10): Promise { const script = ` const Mail = Application('Mail') const allMessages = Mail.inbox().messages() const unreadMessages = allMessages.filter(msg => !msg.readStatus()) return JSON.stringify( unreadMessages.slice(0, ${limit}).map(msg => ({ id: msg.messageId(), subject: msg.subject(), sender: msg.sender(), date: msg.dateReceived().toISOString() })) ) ` return await runJXA(script) } ``` ### Pattern 4: filter by date range ```typescript async function getRecentEmailsFromHours(hours: number = 24): Promise { const script = ` const Mail = Application('Mail') const cutoffDate = new Date(Date.now() - ${hours} * 60 * 60 * 1000) const messages = Mail.inbox().messages() return JSON.stringify( messages .filter(msg => msg.dateReceived() > cutoffDate) .map(msg => ({ subject: msg.subject(), sender: msg.sender(), date: msg.dateReceived().toISOString() })) ) ` return await runJXA(script) } ``` ### Pattern 5: create draft email ```typescript async function createDraft( to: string, subject: string, body: string ): Promise { // Escape special characters const escapedSubject = subject.replace(/"/g, '\\"').replace(/\n/g, '\\n') const escapedBody = body.replace(/"/g, '\\"').replace(/\n/g, '\\n') const script = ` const Mail = Application('Mail') const msg = Mail.OutgoingMessage({ subject: "${escapedSubject}", content: "${escapedBody}", visible: true }) Mail.outgoingMessages.push(msg) msg.toRecipients.push(Mail.Recipient({ address: "${to}" })) return "Draft created" ` await runJXA(script) } ``` ### Pattern 6: search by sender ```typescript async function getEmailsFromSender(senderEmail: string): Promise { const script = ` const Mail = Application('Mail') const messages = Mail.inbox().messages() return JSON.stringify( messages .filter(msg => msg.sender().includes("${senderEmail}")) .slice(0, 20) .map(msg => ({ subject: msg.subject(), sender: msg.sender(), date: msg.dateReceived().toISOString() })) ) ` return await runJXA(script) } ``` ## Error handling ### Common errors **"Application isn't running":** ```typescript try { const count = await getUnreadCount() } catch (error) { if (error.message.includes("isn't running")) { throw new Error('Apple Mail is not running. Please open Mail.app first.') } throw error } ``` **Permission denied:** ``` Error: Not authorized to send Apple events to Mail. ``` **Solution:** Grant automation permission: 1. System Settings → Privacy & Security → Automation 2. Enable checkbox for Mail under your terminal app **Mailbox not found:** ```typescript // Check if mailbox exists first const script = ` const Mail = Application('Mail') const mailboxNames = Mail.mailboxes().map(m => m.name()) return JSON.stringify(mailboxNames) ` const mailboxes = await runJXA(script) if (!mailboxes.includes('Archive')) { throw new Error('Mailbox "Archive" not found') } ``` ## Performance considerations ### DO: minimize property access ```javascript // Good - access only needed properties const messages = Mail.inbox().messages() for (const msg of messages) { const subject = msg.subject() const sender = msg.sender() } ``` ### DON'T: access all properties ```javascript // Slow - gets everything const messages = Mail.inbox().messages() for (const msg of messages) { const props = msg.properties() // Avoid this } ``` ### Batch operations ```typescript // Process large message sets in batches async function processMessagesInBatches(batchSize: number = 50) { // Get total count first const countScript = ` const Mail = Application('Mail') return Mail.inbox().messages().length ` const total = await runJXA(countScript) // Process in batches for (let offset = 0; offset < total; offset += batchSize) { const script = ` const Mail = Application('Mail') const messages = Mail.inbox().messages() return JSON.stringify( messages.slice(${offset}, ${offset + batchSize}).map(msg => ({ subject: msg.subject(), sender: msg.sender() })) ) ` const batch = await runJXA(script) // Process batch } } ``` ## Limitations ### Cannot do: - No Access Mail without Mail.app running - No Bypass Mail.app (requires Mail.app installed and configured) - No Direct IMAP/POP3 access (use mail client libraries instead) - No Modify Mail.app preferences programmatically (very limited support) - No Access mail while account is not configured ### Workarounds: **For direct IMAP/POP3:** Use libraries like `imapflow` or `nodemailer` for server access without Mail.app **For mail without Mail.app:** Consider IMAP/SMTP libraries instead of JXA **For advanced filtering:** Mail Rules in Mail.app or server-side filters ## Application client pattern Full TypeScript client implementation (see `examples/mail-client.ts`): ```typescript export class MailClient { async getUnreadCount(): Promise { /* ... */ } async getRecentMessages(limit: number): Promise { /* ... */ } async createDraft(to: string, subject: string, body: string): Promise { /* ... */ } async isMailRunning(): Promise { /* ... */ } } // Usage const mail = new MailClient() if (await mail.isMailRunning()) { const unread = await mail.getUnreadCount() console.log(`Unread: ${unread}`) } ``` ## Additional resources - **mail-scripting-dictionary.md** - Complete Mail.app scripting API reference - **imap-vs-native.md** - When to use IMAP libraries vs JXA ## Examples - **mail-client.ts** - Full TypeScript Mail client implementation - **read-inbox.jxa** - Standalone inbox reading script - **create-draft.jxa** - Draft creation script - **search-messages.jxa** - Message search patterns ## Next steps Combine with other automation skills: - **calendar-automation** - Create emails based on calendar events - **notes-automation** - Email notes to yourself - **reminders-automation** - Email reminders summary