mail-app-cli: Scriptable Email for Emacs and AI Agents

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.