From macos-automation
This skill should be used 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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/macos-automation:mail-automationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Automate Apple Mail using JXA (JavaScript for Automation) for reading, composing, searching, and managing email programmatically.
Automate Apple Mail using JXA (JavaScript for Automation) for reading, composing, searching, and managing email programmatically.
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.
runJXA<T>() pattern| Category | Operations | JXA Support |
|---|---|---|
| Reading | Access inbox/mailboxes, read properties, check status | ✅ Full |
| Composing | Create drafts, set recipients, add content | ✅ Full |
| Sending | Send messages programmatically | ✅ Full |
| Searching | Filter by date, status, sender, subject | ✅ Full |
| Accounts | List accounts, access properties | ✅ Full |
| Attachments | Read attachment metadata, access files | ✅ Full |
| 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() |
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()
Recommended pattern using type-safe wrapper:
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<Email[]> {
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<Email[]>(script)
}
async function getUnreadCount(): Promise<number> {
const script = `
const Mail = Application('Mail')
return Mail.inbox().unreadCount()
`
return await runJXA<number>(script)
}
interface MessageSummary {
subject: string
sender: string
date: string
read: boolean
}
async function getRecentMessages(limit: number = 10): Promise<MessageSummary[]> {
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<MessageSummary[]>(script)
}
async function getUnreadMessages(limit: number = 10): Promise<Email[]> {
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<Email[]>(script)
}
async function getRecentEmailsFromHours(hours: number = 24): Promise<Email[]> {
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<Email[]>(script)
}
async function createDraft(
to: string,
subject: string,
body: string
): Promise<void> {
// 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<string>(script)
}
async function getEmailsFromSender(senderEmail: string): Promise<Email[]> {
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<Email[]>(script)
}
"Application isn't running":
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:
Mailbox not found:
// 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<string[]>(script)
if (!mailboxes.includes('Archive')) {
throw new Error('Mailbox "Archive" not found')
}
// Good - access only needed properties
const messages = Mail.inbox().messages()
for (const msg of messages) {
const subject = msg.subject()
const sender = msg.sender()
}
// Slow - gets everything
const messages = Mail.inbox().messages()
for (const msg of messages) {
const props = msg.properties() // Avoid this
}
// 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<number>(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<MessageSummary[]>(script)
// Process batch
}
}
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
Full TypeScript client implementation (see examples/mail-client.ts):
export class MailClient {
async getUnreadCount(): Promise<number> { /* ... */ }
async getRecentMessages(limit: number): Promise<Email[]> { /* ... */ }
async createDraft(to: string, subject: string, body: string): Promise<void> { /* ... */ }
async isMailRunning(): Promise<boolean> { /* ... */ }
}
// Usage
const mail = new MailClient()
if (await mail.isMailRunning()) {
const unread = await mail.getUnreadCount()
console.log(`Unread: ${unread}`)
}
Combine with other automation skills:
Guides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.
npx claudepluginhub mattwag05/mw-plugins --plugin macos-automation