mail-app-cli: Scriptable Email for Emacs and AI Agents
Email clients have GUIs. AI agents need APIs. Emacs users need text interfaces. macOS Mail.app has neither.
mail-app-cli solves this by wrapping Mail.app in a scriptable command-line interface. Complete access to accounts, mailboxes, messages, and attachments. JSON output. No OAuth dance. No API tokens. If it’s in Mail.app, you can script it.
The Problem It Solves#
For Emacs users: Read and manage email without leaving your editor. mail-app-wrap provides the Emacs interface. mail-app-cli provides the engine.
For AI agents: Give your AI access to email without repeated authorization. Mail.app already has your credentials. The CLI uses them. AI calls the CLI. No tokens to manage.
For automation: Script email operations with shell commands and pipes. Archive read messages. Extract attachments. Send notifications. All without touching a GUI.
Basic Operations#
List accounts:
$ mail-app-cli accounts list
[
{
"name": "Gmail",
"emailAddress": "user@gmail.com",
"enabled": true
}
]
List mailboxes:
$ mail-app-cli mailboxes list --account "Gmail"
[
{
"account": "Gmail",
"name": "INBOX",
"unreadCount": 12
}
]
List messages:
$ mail-app-cli messages list -a "Gmail" -m "INBOX" --unread
[
{
"id": "12345",
"subject": "Q1 Planning",
"sender": "boss@company.com",
"dateSent": "2025-12-15T09:30:00Z",
"read": false
}
]
Read a message:
$ mail-app-cli messages show 12345 -a "Gmail" -m "INBOX"
{
"id": "12345",
"subject": "Q1 Planning",
"sender": "boss@company.com",
"content": "Let's meet to discuss...",
"attachments": ["budget.xlsx"]
}
AI Agent Integration#
AI agents can now interact with email without handling OAuth:
# AI agent code
import subprocess
import json
def check_urgent_email():
"""AI checks for urgent messages"""
result = subprocess.run(
['mail-app-cli', 'messages', 'list',
'-a', 'Gmail', '-m', 'INBOX',
'--unread', '--limit', '10'],
capture_output=True,
text=True
)
messages = json.loads(result.stdout)
for msg in messages:
if 'urgent' in msg['subject'].lower():
return msg
return None
def send_status_update(report):
"""AI sends automated status email"""
subprocess.run([
'mail-app-cli', 'send',
'-a', 'Gmail',
'-t', 'team@company.com',
'-s', 'Daily Status Report',
'--body', report
])
The AI uses the same Mail.app credentials you already configured. No separate API keys. No re-authorization. Just call the CLI.
Emacs Integration#
mail-app-wrap provides the Emacs interface:
;; Read email in Emacs
(mail-app-list-messages "Gmail" "INBOX")
;; Search across all mailboxes
(mail-app-search "project update")
;; Compose and send
(mail-app-send
:account "Gmail"
:to "colleague@company.com"
:subject "Re: Architecture Review"
:body "Looks good to me...")
All powered by mail-app-cli underneath. Emacs Lisp calls the CLI, parses JSON, renders in buffers.
Automation Examples#
Archive all read messages:
#!/bin/bash
mail-app-cli messages list -a "Gmail" -m "INBOX" | \
jq -r '.[] | select(.read==true) | .id' | \
while read -r msg_id; do
mail-app-cli messages archive "$msg_id" -a "Gmail" -m "INBOX"
done
Daily unread summary:
#!/bin/bash
echo "Unread Email:"
mail-app-cli mailboxes list | \
jq -r '.[] | select(.unreadCount > 0) | "\(.account)/\(.name): \(.unreadCount)"'
Save attachments from specific sender:
#!/bin/bash
SENDER="colleague@company.com"
mail-app-cli messages list -a "Gmail" -m "INBOX" | \
jq -r ".[] | select(.sender | contains(\"$SENDER\")) | .id" | \
while read -r msg_id; do
mail-app-cli attachments list "$msg_id" -a "Gmail" -m "INBOX" | \
jq -r '.[].name' | \
while read -r att_name; do
mail-app-cli attachments save "$msg_id" "$att_name" \
-a "Gmail" -m "INBOX" -o "~/Downloads/$att_name"
done
done
Flag messages from VIPs:
#!/bin/bash
VIPS=("ceo@company.com" "cto@company.com")
for vip in "${VIPS[@]}"; do
mail-app-cli messages list -a "Gmail" -m "INBOX" --unread | \
jq -r ".[] | select(.sender | contains(\"$vip\")) | .id" | \
while read -r msg_id; do
mail-app-cli messages flag "$msg_id" -a "Gmail" -m "INBOX" --flagged
done
done
Search and Filter#
Search across all mailboxes:
$ mail-app-cli search "budget review"
[
{
"account": "Gmail",
"mailbox": "Work",
"id": "67890",
"subject": "Budget Review Meeting"
}
]
Combine with jq for advanced filtering:
# Find messages with attachments
mail-app-cli messages list -a "Gmail" -m "INBOX" | \
jq '.[] | select(.attachments | length > 0)'
# Get messages from last week
mail-app-cli messages list -a "Gmail" -m "INBOX" \
--since "$(date -v-7d +%Y-%m-%d)"
JSON Output Format#
All commands output JSON for easy parsing:
{
"id": "12345",
"subject": "Meeting Tomorrow",
"sender": "colleague@company.com",
"dateSent": "2025-12-15T14:30:00Z",
"read": false,
"flagged": false,
"attachments": ["agenda.pdf"]
}
Pipe through jq for custom formatting:
# CSV output
mail-app-cli messages list -a "Gmail" -m "INBOX" | \
jq -r '.[] | [.sender, .subject, .dateSent] | @csv'
# Custom table format
mail-app-cli messages list -a "Gmail" -m "INBOX" | \
jq -r '.[] | "\(.sender)\t\(.subject)"'
AppleScript Under the Hood#
mail-app-cli uses AppleScript and JavaScript for Automation (JXA) to control Mail.app:
// Simplified example
func GetMessages(account, mailbox string) ([]Message, error) {
script := fmt.Sprintf(`
tell application "Mail"
set msgs to messages of mailbox "%s" of account "%s"
repeat with msg in msgs
-- Extract message details
end repeat
end tell
`, mailbox, account)
output := executeAppleScript(script)
return parseJSON(output)
}
This provides native integration with Mail.app’s data. No IMAP reimplementation. No protocol parsing. Direct access.
Why This Matters#
No separate credentials: Mail.app already has your email accounts configured. Use them.
No API rate limits: Direct access to local Mail.app database.
No network dependencies: Works offline with cached mail.
No authorization flow: AI agents don’t need OAuth. They call a CLI that uses existing credentials.
Full feature access: Everything Mail.app can do, you can script.
Implementation Details#
Built in Go with Cobra for CLI framework. 50K lines across:
- Command definitions (accounts, mailboxes, messages, send, search, attachments)
- AppleScript/JXA client for Mail.app interaction
- JSON output formatting
- Error handling and validation
Use Cases#
Emacs Email Client: mail-app-wrap provides the interface. Users read/write email in Emacs buffers.
AI Agent Email Access: Agents check for urgent messages, send status reports, save attachments - all without OAuth setup. Works particularly well with Clarity for generating automated status reports via email.
Email Automation: Archive old messages, flag VIPs, extract data, generate reports. Combine with classic Linux commands for powerful text processing pipelines on email data.
Backup Scripts: Export messages to text/JSON for archival.
Integration: Connect email to other workflows (Jira tickets from emails, calendar events from invites, etc.)
Quick Reference#
# Accounts
mail-app-cli accounts list
mail-app-cli accounts show "Gmail"
# Mailboxes
mail-app-cli mailboxes list --account "Gmail"
# Messages
mail-app-cli messages list -a "Gmail" -m "INBOX"
mail-app-cli messages list -a "Gmail" -m "INBOX" --unread
mail-app-cli messages show <id> -a "Gmail" -m "INBOX"
mail-app-cli messages mark <id> -a "Gmail" -m "INBOX" --read
mail-app-cli messages flag <id> -a "Gmail" -m "INBOX" --flagged
mail-app-cli messages archive <id> -a "Gmail" -m "INBOX"
mail-app-cli messages move <id> "Archive" -a "Gmail" -m "INBOX"
mail-app-cli messages delete <id> -a "Gmail" -m "INBOX"
# Search
mail-app-cli search "query" --limit 20
# Send
mail-app-cli send -a "Gmail" -t user@example.com -s "Subject" --body "Content"
# Attachments
mail-app-cli attachments list <id> -a "Gmail" -m "INBOX"
mail-app-cli attachments save <id> "file.pdf" -a "Gmail" -m "INBOX"
Available at github.com/robertmeta/mail-app-cli under MIT license.