Initial backup 2026-02-17
This commit is contained in:
7
skills/apple-mail/.clawhub/origin.json
Normal file
7
skills/apple-mail/.clawhub/origin.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"registry": "https://clawhub.ai",
|
||||
"slug": "apple-mail",
|
||||
"installedVersion": "1.2.0",
|
||||
"installedAt": 1770184142890
|
||||
}
|
||||
165
skills/apple-mail/SKILL.md
Normal file
165
skills/apple-mail/SKILL.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
name: apple-mail
|
||||
description: Apple Mail.app integration for macOS. Read inbox, search emails, send emails, reply, and manage messages with fast direct access (no enumeration).
|
||||
metadata: {"clawdbot":{"emoji":"📧","os":["darwin"],"requires":{"bins":["sqlite3"]}}}
|
||||
---
|
||||
|
||||
# Apple Mail
|
||||
|
||||
Interact with Mail.app via AppleScript and SQLite. Run scripts from: `cd {baseDir}`
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Usage |
|
||||
|---------|-------|
|
||||
| **Refresh** | `scripts/mail-refresh.sh [account] [wait_seconds]` |
|
||||
| List recent | `scripts/mail-list.sh [mailbox] [account] [limit]` |
|
||||
| Search | `scripts/mail-search.sh "query" [mailbox] [limit]` |
|
||||
| Fast search | `scripts/mail-fast-search.sh "query" [limit]` |
|
||||
| Read email | `scripts/mail-read.sh <message-id> [message-id...]` |
|
||||
| Delete | `scripts/mail-delete.sh <message-id> [message-id...]` |
|
||||
| Mark read | `scripts/mail-mark-read.sh <message-id> [message-id...]` |
|
||||
| Mark unread | `scripts/mail-mark-unread.sh <message-id> [message-id...]` |
|
||||
| Send | `scripts/mail-send.sh "to@email.com" "Subject" "Body" [from-account] [attachment]` ¹ |
|
||||
| Reply | `scripts/mail-reply.sh <message-id> "body" [reply-all]` |
|
||||
| List accounts | `scripts/mail-accounts.sh` |
|
||||
| List mailboxes | `scripts/mail-mailboxes.sh [account]` |
|
||||
|
||||
## Refreshing Mail
|
||||
|
||||
Force Mail.app to check for new messages:
|
||||
|
||||
```bash
|
||||
scripts/mail-refresh.sh # All accounts, wait up to 10s
|
||||
scripts/mail-refresh.sh Google # Specific account only
|
||||
scripts/mail-refresh.sh "" 5 # All accounts, max 5 seconds
|
||||
scripts/mail-refresh.sh Google 0 # Google account, no wait
|
||||
```
|
||||
|
||||
**Smart sync detection:**
|
||||
- Script monitors database message count
|
||||
- Returns early when sync completes (no changes for 2s)
|
||||
- Reports new message count: `Sync complete in 2s (+3 messages)`
|
||||
|
||||
**Notes:**
|
||||
- Mail.app must be running (script will error if not)
|
||||
- `mail-list.sh` does NOT auto-refresh — call `mail-refresh.sh` first if you need fresh data
|
||||
|
||||
## Output Format
|
||||
|
||||
List/search returns: `ID | ReadStatus | Date | Sender | Subject`
|
||||
- `●` = unread, blank = read
|
||||
|
||||
## Gmail Mailboxes
|
||||
|
||||
⚠️ Gmail special folders need `[Gmail]/` prefix:
|
||||
|
||||
| Shows as | Use |
|
||||
|----------|-----|
|
||||
| `Spam` | `[Gmail]/Spam` |
|
||||
| `Sent Mail` | `[Gmail]/Sent Mail` |
|
||||
| `All Mail` | `[Gmail]/All Mail` |
|
||||
| `Trash` | `[Gmail]/Trash` |
|
||||
|
||||
Custom labels work without prefix.
|
||||
|
||||
## Fast Search (SQLite)
|
||||
|
||||
✨ **Now safe even if Mail.app is running** — copies database to temp file first.
|
||||
|
||||
```bash
|
||||
scripts/mail-fast-search.sh "query" [limit] # ~50ms vs minutes
|
||||
```
|
||||
|
||||
Previously required Mail.app to be quit. Now works anytime by copying the database to a temp file before querying.
|
||||
|
||||
## Performance Notes
|
||||
|
||||
**Speed by operation:**
|
||||
| Operation | Speed | Notes |
|
||||
|-----------|-------|-------|
|
||||
| `mail-fast-search.sh` | ~50ms | SQLite query, fastest |
|
||||
| `mail-accounts.sh` | <1s | Simple AppleScript |
|
||||
| `mail-list.sh` | 1-3s | AppleScript, direct mailbox access |
|
||||
| `mail-send.sh` | 1-2s | Creates and sends message |
|
||||
| `mail-read.sh` | ~2s | Position-optimized lookup |
|
||||
| `mail-delete.sh` | ~0.5s | Position-optimized lookup |
|
||||
| `mail-mark-*.sh` | ~1.5s | Position-optimized lookup |
|
||||
|
||||
**Optimization technique:**
|
||||
SQLite provides account UUID and approximate message position. AppleScript jumps directly to that position instead of iterating from the start.
|
||||
|
||||
**Batch operations supported:**
|
||||
- `mail-read.sh 123 456 789` - Read multiple (separator between each)
|
||||
- `mail-delete.sh 123 456 789` - Delete multiple
|
||||
- `mail-mark-read.sh 123 456` - Mark multiple as read
|
||||
- `mail-mark-unread.sh 123 456` - Mark multiple as unread
|
||||
|
||||
**⚠️ No auto-refresh:** Scripts read cached data. Call `mail-refresh.sh` first if you need latest emails.
|
||||
|
||||
## Managing Emails
|
||||
|
||||
**Delete emails:**
|
||||
```bash
|
||||
scripts/mail-delete.sh 12345 # Delete one
|
||||
scripts/mail-delete.sh 12345 12346 12347 # Delete multiple
|
||||
```
|
||||
|
||||
**Mark as read/unread:**
|
||||
```bash
|
||||
scripts/mail-mark-read.sh 12345 12346 # Mark as read
|
||||
scripts/mail-mark-unread.sh 12345 # Mark as unread
|
||||
```
|
||||
|
||||
**Bulk operations example:**
|
||||
```bash
|
||||
# Find spam emails
|
||||
scripts/mail-fast-search.sh "spam" 50 > spam.txt
|
||||
|
||||
# Extract IDs and delete them
|
||||
grep "^[0-9]" spam.txt | cut -d'|' -f1 | xargs scripts/mail-delete.sh
|
||||
```
|
||||
|
||||
## Reading Email Bodies
|
||||
|
||||
```bash
|
||||
scripts/mail-read.sh 12345 # Single email
|
||||
scripts/mail-read.sh 12345 12346 12347 # Multiple emails (separated output)
|
||||
```
|
||||
|
||||
Uses position-optimized lookup (~2s per message). Multiple emails are separated by `========` with a summary at the end.
|
||||
|
||||
## Errors
|
||||
|
||||
| Error | Cause |
|
||||
|-------|-------|
|
||||
| `Mail.app is not running` | Open Mail.app before running scripts |
|
||||
| `Account not found` | Invalid account — check mail-accounts.sh |
|
||||
| `Message not found` | Invalid/deleted ID — get fresh from mail-list.sh |
|
||||
| `Can't get mailbox` | Invalid name — check mail-mailboxes.sh |
|
||||
| `Mail database not found` | SQLite DB missing — check ~/Library/Mail/V{9,10,11}/MailData/ |
|
||||
|
||||
## Technical Details
|
||||
|
||||
**Database:** `~/Library/Mail/V{9,10,11}/MailData/Envelope Index`
|
||||
|
||||
**Message lookup method (optimized):**
|
||||
1. Query SQLite for account UUID, mailbox path, and approximate position
|
||||
2. AppleScript accesses the specific account directly (no iteration)
|
||||
3. Search starts at the approximate position (±5 messages buffer)
|
||||
4. Falls back to full mailbox search only if position hint fails
|
||||
|
||||
**Safety:**
|
||||
- Fast search copies database to temp file before querying
|
||||
- Safe to use even if Mail.app is running
|
||||
- Delete/read/mark operations query live database but access is minimal
|
||||
|
||||
## Notes
|
||||
|
||||
- Message IDs are internal, get fresh ones from list/search
|
||||
- Confirm recipient before sending
|
||||
- AppleScript search is slow but comprehensive; SQLite is fast for metadata
|
||||
- Delete/mark operations support bulk actions (pass multiple IDs)
|
||||
- Always refresh before listing if you need the absolute latest emails
|
||||
|
||||
¹ **Known limitation:** Mail.app adds a leading blank line to sent emails. This is an AppleScript/Mail.app behavior that cannot be bypassed.
|
||||
22
skills/apple-mail/scripts/mail-accounts.sh
Normal file
22
skills/apple-mail/scripts/mail-accounts.sh
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# List configured email accounts
|
||||
# Usage: mail-accounts.sh
|
||||
|
||||
osascript <<EOF
|
||||
tell application "Mail"
|
||||
set output to ""
|
||||
repeat with acct in every account
|
||||
set acctName to name of acct
|
||||
set acctType to account type of acct as string
|
||||
set acctEmail to ""
|
||||
try
|
||||
set acctEmail to email addresses of acct
|
||||
if class of acctEmail is list then
|
||||
set acctEmail to item 1 of acctEmail
|
||||
end if
|
||||
end try
|
||||
set output to output & acctName & " (" & acctType & ") - " & acctEmail & linefeed
|
||||
end repeat
|
||||
return output
|
||||
end tell
|
||||
EOF
|
||||
110
skills/apple-mail/scripts/mail-delete.sh
Normal file
110
skills/apple-mail/scripts/mail-delete.sh
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
# Delete emails by message ID (optimized with position hints)
|
||||
# Usage: mail-delete.sh <message-id> [message-id...]
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "Usage: mail-delete.sh <message-id> [message-id...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find the Mail database
|
||||
find_db() {
|
||||
local db
|
||||
for v in 11 10 9; do
|
||||
db="$HOME/Library/Mail/V$v/MailData/Envelope Index"
|
||||
if [[ -f "$db" ]]; then
|
||||
echo "$db"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
DB_PATH=$(find_db)
|
||||
|
||||
if [[ -z "$DB_PATH" ]]; then
|
||||
echo "Error: Mail database not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DELETED=0
|
||||
FAILED=0
|
||||
|
||||
for MSG_ID in "$@"; do
|
||||
# Get account UUID, mailbox path, and approximate position
|
||||
MSG_INFO=$(sqlite3 "$DB_PATH" "
|
||||
SELECT
|
||||
substr(mb.url, 8, instr(substr(mb.url, 8), '/') - 1) as account_uuid,
|
||||
replace(replace(substr(mb.url, 8 + instr(substr(mb.url, 8), '/')), '%5B', '['), '%5D', ']') as mailbox_path,
|
||||
(SELECT COUNT(*) FROM messages m2 WHERE m2.mailbox = m.mailbox AND m2.date_received >= m.date_received) as approx_pos
|
||||
FROM messages m
|
||||
JOIN mailboxes mb ON m.mailbox = mb.ROWID
|
||||
WHERE m.ROWID = $MSG_ID;" 2>/dev/null)
|
||||
|
||||
if [[ -z "$MSG_INFO" ]]; then
|
||||
echo "Message $MSG_ID not found in database" >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
IFS='|' read -r ACCOUNT_UUID MAILBOX_PATH APPROX_POS <<< "$MSG_INFO"
|
||||
MAILBOX_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$MAILBOX_PATH'))")
|
||||
|
||||
START_POS=$((APPROX_POS > 5 ? APPROX_POS - 5 : 1))
|
||||
END_POS=$((APPROX_POS + 20))
|
||||
|
||||
RESULT=$(osascript << EOF
|
||||
tell application "Mail"
|
||||
try
|
||||
set targetId to $MSG_ID
|
||||
set targetAccount to first account whose id is "$ACCOUNT_UUID"
|
||||
set mbx to mailbox "$MAILBOX_PATH" of targetAccount
|
||||
set msgCount to count of messages of mbx
|
||||
|
||||
if $END_POS > msgCount then
|
||||
set endPos to msgCount
|
||||
else
|
||||
set endPos to $END_POS
|
||||
end if
|
||||
|
||||
-- Search in expected range first
|
||||
repeat with i from $START_POS to endPos
|
||||
try
|
||||
set msg to message i of mbx
|
||||
if id of msg = targetId then
|
||||
delete msg
|
||||
return "OK"
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
|
||||
-- Expand search if not found
|
||||
repeat with i from 1 to msgCount
|
||||
try
|
||||
set msg to message i of mbx
|
||||
if id of msg = targetId then
|
||||
delete msg
|
||||
return "OK"
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
|
||||
return "ERROR: Message not found"
|
||||
on error errMsg
|
||||
return "ERROR: " & errMsg
|
||||
end try
|
||||
end tell
|
||||
EOF
|
||||
)
|
||||
|
||||
if [[ "$RESULT" == "OK" ]]; then
|
||||
echo "Deleted message $MSG_ID"
|
||||
DELETED=$((DELETED + 1))
|
||||
else
|
||||
echo "Failed to delete message $MSG_ID: $RESULT" >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Summary: $DELETED deleted, $FAILED failed"
|
||||
59
skills/apple-mail/scripts/mail-fast-search.sh
Normal file
59
skills/apple-mail/scripts/mail-fast-search.sh
Normal file
@@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
# Fast SQLite-based email search (~50ms vs minutes with AppleScript)
|
||||
# Safe to use even if Mail.app is running (copies DB to temp file)
|
||||
# Usage: mail-fast-search.sh <query> [limit]
|
||||
|
||||
set -e
|
||||
|
||||
QUERY="${1:?Usage: mail-fast-search.sh <query> [limit]}"
|
||||
LIMIT="${2:-20}"
|
||||
|
||||
# Find the Mail envelope index database
|
||||
find_db() {
|
||||
local db
|
||||
for v in 11 10 9; do
|
||||
db="$HOME/Library/Mail/V$v/MailData/Envelope Index"
|
||||
if [[ -f "$db" ]]; then
|
||||
# Verify this DB has the messages table
|
||||
if sqlite3 "$db" "SELECT 1 FROM messages LIMIT 1" &>/dev/null; then
|
||||
echo "$db"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
SOURCE_DB=$(find_db)
|
||||
|
||||
if [[ -z "$SOURCE_DB" ]]; then
|
||||
echo "Error: Mail database not found or schema incompatible" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy to temp file to avoid corrupting the live DB while Mail.app is running
|
||||
TEMP_DB=$(mktemp -t mail-search.XXXXXX)
|
||||
cleanup() {
|
||||
rm -f "$TEMP_DB" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
cp "$SOURCE_DB" "$TEMP_DB"
|
||||
|
||||
# Search by subject, sender address, or sender name
|
||||
sqlite3 -header -separator ' | ' "$TEMP_DB" "
|
||||
SELECT
|
||||
m.ROWID as id,
|
||||
CASE WHEN (m.flags & 1) = 0 THEN '●' ELSE ' ' END as unread,
|
||||
datetime(m.date_sent, 'unixepoch', 'localtime') as date,
|
||||
COALESCE(a.comment, a.address, 'Unknown') as sender,
|
||||
COALESCE(s.subject, '(no subject)') as subject
|
||||
FROM messages m
|
||||
LEFT JOIN subjects s ON m.subject = s.ROWID
|
||||
LEFT JOIN addresses a ON m.sender = a.ROWID
|
||||
WHERE s.subject LIKE '%${QUERY}%'
|
||||
OR a.address LIKE '%${QUERY}%'
|
||||
OR a.comment LIKE '%${QUERY}%'
|
||||
ORDER BY m.date_sent DESC
|
||||
LIMIT ${LIMIT};
|
||||
"
|
||||
60
skills/apple-mail/scripts/mail-list.sh
Normal file
60
skills/apple-mail/scripts/mail-list.sh
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
# List recent emails from a mailbox
|
||||
# Usage: mail-list.sh [mailbox] [account] [limit]
|
||||
|
||||
MAILBOX="${1:-INBOX}"
|
||||
ACCOUNT="${2:-}"
|
||||
LIMIT="${3:-10}"
|
||||
|
||||
if [ -n "$ACCOUNT" ]; then
|
||||
osascript <<EOF
|
||||
tell application "Mail"
|
||||
set output to ""
|
||||
set targetMailbox to mailbox "$MAILBOX" of account "$ACCOUNT"
|
||||
set msgs to messages 1 through $LIMIT of targetMailbox
|
||||
repeat with m in msgs
|
||||
set mid to id of m
|
||||
set msubject to subject of m
|
||||
set msender to sender of m
|
||||
set mdate to date received of m
|
||||
set mread to read status of m
|
||||
set readFlag to "●"
|
||||
if mread then set readFlag to " "
|
||||
set output to output & mid & " | " & readFlag & " | " & mdate & " | " & msender & " | " & msubject & linefeed
|
||||
end repeat
|
||||
return output
|
||||
end tell
|
||||
EOF
|
||||
else
|
||||
osascript <<EOF
|
||||
tell application "Mail"
|
||||
set output to ""
|
||||
set allAccounts to every account
|
||||
set foundMsgs to {}
|
||||
repeat with acct in allAccounts
|
||||
try
|
||||
set targetMailbox to mailbox "$MAILBOX" of acct
|
||||
set msgs to messages 1 through $LIMIT of targetMailbox
|
||||
repeat with m in msgs
|
||||
set end of foundMsgs to m
|
||||
end repeat
|
||||
end try
|
||||
end repeat
|
||||
set sortedMsgs to foundMsgs
|
||||
set countLimit to $LIMIT
|
||||
if (count of sortedMsgs) < countLimit then set countLimit to (count of sortedMsgs)
|
||||
repeat with i from 1 to countLimit
|
||||
set m to item i of sortedMsgs
|
||||
set mid to id of m
|
||||
set msubject to subject of m
|
||||
set msender to sender of m
|
||||
set mdate to date received of m
|
||||
set mread to read status of m
|
||||
set readFlag to "●"
|
||||
if mread then set readFlag to " "
|
||||
set output to output & mid & " | " & readFlag & " | " & mdate & " | " & msender & " | " & msubject & linefeed
|
||||
end repeat
|
||||
return output
|
||||
end tell
|
||||
EOF
|
||||
fi
|
||||
41
skills/apple-mail/scripts/mail-mailboxes.sh
Normal file
41
skills/apple-mail/scripts/mail-mailboxes.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# List mailboxes for an account
|
||||
# Usage: mail-mailboxes.sh [account]
|
||||
|
||||
ACCOUNT="${1:-}"
|
||||
|
||||
if [ -n "$ACCOUNT" ]; then
|
||||
osascript <<EOF
|
||||
tell application "Mail"
|
||||
set output to ""
|
||||
set acct to account "$ACCOUNT"
|
||||
repeat with mbox in every mailbox of acct
|
||||
set mboxName to name of mbox
|
||||
set msgCount to count of messages of mbox
|
||||
set output to output & mboxName & " (" & msgCount & " messages)" & linefeed
|
||||
end repeat
|
||||
return output
|
||||
end tell
|
||||
EOF
|
||||
else
|
||||
osascript <<EOF
|
||||
tell application "Mail"
|
||||
set output to ""
|
||||
repeat with acct in every account
|
||||
set acctName to name of acct
|
||||
set output to output & "=== " & acctName & " ===" & linefeed
|
||||
repeat with mbox in every mailbox of acct
|
||||
set mboxName to name of mbox
|
||||
try
|
||||
set msgCount to count of messages of mbox
|
||||
set output to output & " " & mboxName & " (" & msgCount & " messages)" & linefeed
|
||||
on error
|
||||
set output to output & " " & mboxName & linefeed
|
||||
end try
|
||||
end repeat
|
||||
set output to output & linefeed
|
||||
end repeat
|
||||
return output
|
||||
end tell
|
||||
EOF
|
||||
fi
|
||||
110
skills/apple-mail/scripts/mail-mark-read.sh
Normal file
110
skills/apple-mail/scripts/mail-mark-read.sh
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
# Mark emails as read by message ID (optimized with position hints)
|
||||
# Usage: mail-mark-read.sh <message-id> [message-id...]
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "Usage: mail-mark-read.sh <message-id> [message-id...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find the Mail database
|
||||
find_db() {
|
||||
local db
|
||||
for v in 11 10 9; do
|
||||
db="$HOME/Library/Mail/V$v/MailData/Envelope Index"
|
||||
if [[ -f "$db" ]]; then
|
||||
echo "$db"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
DB_PATH=$(find_db)
|
||||
|
||||
if [[ -z "$DB_PATH" ]]; then
|
||||
echo "Error: Mail database not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MARKED=0
|
||||
FAILED=0
|
||||
|
||||
for MSG_ID in "$@"; do
|
||||
# Get account UUID, mailbox path, and approximate position
|
||||
MSG_INFO=$(sqlite3 "$DB_PATH" "
|
||||
SELECT
|
||||
substr(mb.url, 8, instr(substr(mb.url, 8), '/') - 1) as account_uuid,
|
||||
replace(replace(substr(mb.url, 8 + instr(substr(mb.url, 8), '/')), '%5B', '['), '%5D', ']') as mailbox_path,
|
||||
(SELECT COUNT(*) FROM messages m2 WHERE m2.mailbox = m.mailbox AND m2.date_received >= m.date_received) as approx_pos
|
||||
FROM messages m
|
||||
JOIN mailboxes mb ON m.mailbox = mb.ROWID
|
||||
WHERE m.ROWID = $MSG_ID;" 2>/dev/null)
|
||||
|
||||
if [[ -z "$MSG_INFO" ]]; then
|
||||
echo "Message $MSG_ID not found in database" >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
IFS='|' read -r ACCOUNT_UUID MAILBOX_PATH APPROX_POS <<< "$MSG_INFO"
|
||||
MAILBOX_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$MAILBOX_PATH'))")
|
||||
|
||||
START_POS=$((APPROX_POS > 5 ? APPROX_POS - 5 : 1))
|
||||
END_POS=$((APPROX_POS + 20))
|
||||
|
||||
RESULT=$(osascript << EOF
|
||||
tell application "Mail"
|
||||
try
|
||||
set targetId to $MSG_ID
|
||||
set targetAccount to first account whose id is "$ACCOUNT_UUID"
|
||||
set mbx to mailbox "$MAILBOX_PATH" of targetAccount
|
||||
set msgCount to count of messages of mbx
|
||||
|
||||
if $END_POS > msgCount then
|
||||
set endPos to msgCount
|
||||
else
|
||||
set endPos to $END_POS
|
||||
end if
|
||||
|
||||
-- Search in expected range first
|
||||
repeat with i from $START_POS to endPos
|
||||
try
|
||||
set msg to message i of mbx
|
||||
if id of msg = targetId then
|
||||
set read status of msg to true
|
||||
return "OK"
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
|
||||
-- Expand search if not found
|
||||
repeat with i from 1 to msgCount
|
||||
try
|
||||
set msg to message i of mbx
|
||||
if id of msg = targetId then
|
||||
set read status of msg to true
|
||||
return "OK"
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
|
||||
return "ERROR: Message not found"
|
||||
on error errMsg
|
||||
return "ERROR: " & errMsg
|
||||
end try
|
||||
end tell
|
||||
EOF
|
||||
)
|
||||
|
||||
if [[ "$RESULT" == "OK" ]]; then
|
||||
echo "Marked message $MSG_ID as read"
|
||||
MARKED=$((MARKED + 1))
|
||||
else
|
||||
echo "Failed to mark message $MSG_ID: $RESULT" >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Summary: $MARKED marked, $FAILED failed"
|
||||
110
skills/apple-mail/scripts/mail-mark-unread.sh
Normal file
110
skills/apple-mail/scripts/mail-mark-unread.sh
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
# Mark emails as unread by message ID (optimized with position hints)
|
||||
# Usage: mail-mark-unread.sh <message-id> [message-id...]
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo "Usage: mail-mark-unread.sh <message-id> [message-id...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find the Mail database
|
||||
find_db() {
|
||||
local db
|
||||
for v in 11 10 9; do
|
||||
db="$HOME/Library/Mail/V$v/MailData/Envelope Index"
|
||||
if [[ -f "$db" ]]; then
|
||||
echo "$db"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
DB_PATH=$(find_db)
|
||||
|
||||
if [[ -z "$DB_PATH" ]]; then
|
||||
echo "Error: Mail database not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MARKED=0
|
||||
FAILED=0
|
||||
|
||||
for MSG_ID in "$@"; do
|
||||
# Get account UUID, mailbox path, and approximate position
|
||||
MSG_INFO=$(sqlite3 "$DB_PATH" "
|
||||
SELECT
|
||||
substr(mb.url, 8, instr(substr(mb.url, 8), '/') - 1) as account_uuid,
|
||||
replace(replace(substr(mb.url, 8 + instr(substr(mb.url, 8), '/')), '%5B', '['), '%5D', ']') as mailbox_path,
|
||||
(SELECT COUNT(*) FROM messages m2 WHERE m2.mailbox = m.mailbox AND m2.date_received >= m.date_received) as approx_pos
|
||||
FROM messages m
|
||||
JOIN mailboxes mb ON m.mailbox = mb.ROWID
|
||||
WHERE m.ROWID = $MSG_ID;" 2>/dev/null)
|
||||
|
||||
if [[ -z "$MSG_INFO" ]]; then
|
||||
echo "Message $MSG_ID not found in database" >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
IFS='|' read -r ACCOUNT_UUID MAILBOX_PATH APPROX_POS <<< "$MSG_INFO"
|
||||
MAILBOX_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$MAILBOX_PATH'))")
|
||||
|
||||
START_POS=$((APPROX_POS > 5 ? APPROX_POS - 5 : 1))
|
||||
END_POS=$((APPROX_POS + 20))
|
||||
|
||||
RESULT=$(osascript << EOF
|
||||
tell application "Mail"
|
||||
try
|
||||
set targetId to $MSG_ID
|
||||
set targetAccount to first account whose id is "$ACCOUNT_UUID"
|
||||
set mbx to mailbox "$MAILBOX_PATH" of targetAccount
|
||||
set msgCount to count of messages of mbx
|
||||
|
||||
if $END_POS > msgCount then
|
||||
set endPos to msgCount
|
||||
else
|
||||
set endPos to $END_POS
|
||||
end if
|
||||
|
||||
-- Search in expected range first
|
||||
repeat with i from $START_POS to endPos
|
||||
try
|
||||
set msg to message i of mbx
|
||||
if id of msg = targetId then
|
||||
set read status of msg to false
|
||||
return "OK"
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
|
||||
-- Expand search if not found
|
||||
repeat with i from 1 to msgCount
|
||||
try
|
||||
set msg to message i of mbx
|
||||
if id of msg = targetId then
|
||||
set read status of msg to false
|
||||
return "OK"
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
|
||||
return "ERROR: Message not found"
|
||||
on error errMsg
|
||||
return "ERROR: " & errMsg
|
||||
end try
|
||||
end tell
|
||||
EOF
|
||||
)
|
||||
|
||||
if [[ "$RESULT" == "OK" ]]; then
|
||||
echo "Marked message $MSG_ID as unread"
|
||||
MARKED=$((MARKED + 1))
|
||||
else
|
||||
echo "Failed to mark message $MSG_ID: $RESULT" >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Summary: $MARKED marked, $FAILED failed"
|
||||
163
skills/apple-mail/scripts/mail-read-emlx.py
Normal file
163
skills/apple-mail/scripts/mail-read-emlx.py
Normal file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Read email content from Apple Mail's database and emlx files
|
||||
Usage: mail-read-emlx.py <message-row-id>
|
||||
"""
|
||||
|
||||
import sys
|
||||
import sqlite3
|
||||
import os
|
||||
import email
|
||||
from email import policy
|
||||
from pathlib import Path
|
||||
|
||||
def find_mail_db():
|
||||
"""Find the Apple Mail database"""
|
||||
for v in [11, 10, 9]:
|
||||
db_path = Path.home() / "Library" / "Mail" / f"V{v}" / "MailData" / "Envelope Index"
|
||||
if db_path.exists():
|
||||
return str(db_path)
|
||||
return None
|
||||
|
||||
def find_emlx_file(mail_dir, account_id, mailbox_path, remote_id):
|
||||
"""Try to find the emlx file for a message"""
|
||||
# Common locations to search
|
||||
mail_v_dir = Path(mail_dir)
|
||||
account_dir = mail_v_dir / account_id
|
||||
|
||||
if not account_dir.exists():
|
||||
return None
|
||||
|
||||
# Search for emlx files with the remote_id as filename
|
||||
for emlx_file in account_dir.rglob(f"{remote_id}.emlx"):
|
||||
return str(emlx_file)
|
||||
|
||||
return None
|
||||
|
||||
def parse_emlx(emlx_path):
|
||||
"""Parse an emlx file and return the email message"""
|
||||
with open(emlx_path, 'rb') as f:
|
||||
# First line is the byte count, skip it
|
||||
first_line = f.readline()
|
||||
# Rest is the raw email
|
||||
raw_email = f.read()
|
||||
|
||||
msg = email.message_from_bytes(raw_email, policy=policy.default)
|
||||
return msg
|
||||
|
||||
def get_message_info(db_path, msg_id):
|
||||
"""Get message information from the database"""
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
mgd.message_id_header,
|
||||
s.subject,
|
||||
a.comment as sender,
|
||||
datetime(m.date_received, 'unixepoch', '31 years', 'localtime') as date_received,
|
||||
m.remote_id,
|
||||
mb.url
|
||||
FROM messages m
|
||||
LEFT JOIN message_global_data mgd ON m.global_message_id = mgd.ROWID
|
||||
LEFT JOIN subjects s ON m.subject = s.ROWID
|
||||
LEFT JOIN addresses a ON m.sender = a.ROWID
|
||||
LEFT JOIN mailboxes mb ON m.mailbox = mb.ROWID
|
||||
WHERE m.ROWID = ?
|
||||
"""
|
||||
|
||||
cursor.execute(query, (msg_id,))
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not result:
|
||||
return None
|
||||
|
||||
return {
|
||||
'message_id_header': result[0],
|
||||
'subject': result[1],
|
||||
'sender': result[2],
|
||||
'date_received': result[3],
|
||||
'remote_id': result[4],
|
||||
'mailbox_url': result[5]
|
||||
}
|
||||
|
||||
def format_email_output(msg_info, email_msg=None):
|
||||
"""Format email information for output"""
|
||||
output = []
|
||||
output.append(f"From: {msg_info['sender']}")
|
||||
|
||||
if email_msg:
|
||||
if email_msg.get('To'):
|
||||
output.append(f"To: {email_msg.get('To')}")
|
||||
if email_msg.get('Cc'):
|
||||
output.append(f"Cc: {email_msg.get('Cc')}")
|
||||
|
||||
output.append(f"Date: {msg_info['date_received']}")
|
||||
output.append(f"Subject: {msg_info['subject']}")
|
||||
output.append("")
|
||||
output.append("---")
|
||||
output.append("")
|
||||
|
||||
if email_msg:
|
||||
# Get the email body
|
||||
if email_msg.is_multipart():
|
||||
for part in email_msg.walk():
|
||||
if part.get_content_type() == "text/plain":
|
||||
body = part.get_content()
|
||||
output.append(body)
|
||||
break
|
||||
elif part.get_content_type() == "text/html":
|
||||
# Fallback to HTML if no plain text
|
||||
body = part.get_content()
|
||||
output.append(body)
|
||||
else:
|
||||
body = email_msg.get_content()
|
||||
output.append(body)
|
||||
else:
|
||||
output.append("(Message body not available - emlx file not found)")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: mail-read-emlx.py <message-row-id>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
msg_id = sys.argv[1]
|
||||
|
||||
# Find the database
|
||||
db_path = find_mail_db()
|
||||
if not db_path:
|
||||
print("Error: Mail database not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Get message info from database
|
||||
msg_info = get_message_info(db_path, msg_id)
|
||||
if not msg_info:
|
||||
print(f"Message not found with ID: {msg_id}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Try to find and parse the emlx file
|
||||
email_msg = None
|
||||
if msg_info['remote_id'] and msg_info['mailbox_url']:
|
||||
# Parse account ID from mailbox URL
|
||||
# Format: imap://ACCOUNT-ID/MAILBOX-PATH
|
||||
if msg_info['mailbox_url'].startswith('imap://'):
|
||||
parts = msg_info['mailbox_url'][7:].split('/', 1)
|
||||
if len(parts) >= 1:
|
||||
account_id = parts[0]
|
||||
mail_dir = Path(db_path).parent.parent
|
||||
emlx_file = find_emlx_file(mail_dir, account_id, None, msg_info['remote_id'])
|
||||
|
||||
if emlx_file:
|
||||
try:
|
||||
email_msg = parse_emlx(emlx_file)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not parse emlx file: {e}", file=sys.stderr)
|
||||
|
||||
# Output the formatted message
|
||||
print(format_email_output(msg_info, email_msg))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
149
skills/apple-mail/scripts/mail-read.sh
Normal file
149
skills/apple-mail/scripts/mail-read.sh
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/bin/bash
|
||||
# Read full email content by message ID (supports multiple IDs)
|
||||
# Usage: mail-read.sh <message-id> [message-id...]
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: mail-read.sh <message-id> [message-id...]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find the Mail database
|
||||
find_db() {
|
||||
local db
|
||||
for v in 11 10 9; do
|
||||
db="$HOME/Library/Mail/V$v/MailData/Envelope Index"
|
||||
if [[ -f "$db" ]]; then
|
||||
echo "$db"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
DB_PATH=$(find_db)
|
||||
|
||||
if [[ -z "$DB_PATH" ]]; then
|
||||
echo "Error: Mail database not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
READ_COUNT=0
|
||||
FAILED_COUNT=0
|
||||
FIRST=true
|
||||
|
||||
for MSG_ID in "$@"; do
|
||||
# Add separator between messages
|
||||
if [ "$FIRST" = true ]; then
|
||||
FIRST=false
|
||||
else
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Get account UUID, mailbox path, and approximate position from database
|
||||
MSG_INFO=$(sqlite3 "$DB_PATH" "
|
||||
SELECT
|
||||
substr(mb.url, 8, instr(substr(mb.url, 8), '/') - 1) as account_uuid,
|
||||
replace(replace(substr(mb.url, 8 + instr(substr(mb.url, 8), '/')), '%5B', '['), '%5D', ']') as mailbox_path,
|
||||
(SELECT COUNT(*) FROM messages m2 WHERE m2.mailbox = m.mailbox AND m2.date_received >= m.date_received) as approx_pos
|
||||
FROM messages m
|
||||
JOIN mailboxes mb ON m.mailbox = mb.ROWID
|
||||
WHERE m.ROWID = $MSG_ID;" 2>/dev/null)
|
||||
|
||||
if [[ -z "$MSG_INFO" ]]; then
|
||||
echo "Error: Message $MSG_ID not found in database" >&2
|
||||
FAILED_COUNT=$((FAILED_COUNT + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
IFS='|' read -r ACCOUNT_UUID MAILBOX_PATH APPROX_POS <<< "$MSG_INFO"
|
||||
MAILBOX_PATH=$(python3 -c "import urllib.parse; print(urllib.parse.unquote('$MAILBOX_PATH'))")
|
||||
|
||||
START_POS=$((APPROX_POS > 5 ? APPROX_POS - 5 : 1))
|
||||
END_POS=$((APPROX_POS + 20))
|
||||
|
||||
# Use AppleScript with direct account and position access
|
||||
RESULT=$(osascript <<EOF
|
||||
tell application "Mail"
|
||||
try
|
||||
set targetId to $MSG_ID
|
||||
set targetAccountId to "$ACCOUNT_UUID"
|
||||
set targetMailboxPath to "$MAILBOX_PATH"
|
||||
set startPos to $START_POS
|
||||
set endPos to $END_POS
|
||||
set foundMsg to missing value
|
||||
|
||||
set targetAccount to first account whose id is targetAccountId
|
||||
set mbx to mailbox targetMailboxPath of targetAccount
|
||||
set msgCount to count of messages of mbx
|
||||
|
||||
if endPos > msgCount then set endPos to msgCount
|
||||
if startPos < 1 then set startPos to 1
|
||||
|
||||
repeat with i from startPos to endPos
|
||||
try
|
||||
set msg to message i of mbx
|
||||
if id of msg = targetId then
|
||||
set foundMsg to msg
|
||||
exit repeat
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
|
||||
if foundMsg is missing value then
|
||||
repeat with i from 1 to msgCount
|
||||
try
|
||||
set msg to message i of mbx
|
||||
if id of msg = targetId then
|
||||
set foundMsg to msg
|
||||
exit repeat
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
end if
|
||||
|
||||
if foundMsg is missing value then
|
||||
return "ERROR:Message not found with ID: $MSG_ID"
|
||||
end if
|
||||
|
||||
set output to "From: " & sender of foundMsg & linefeed
|
||||
|
||||
set mto to ""
|
||||
try
|
||||
set recipList to to recipients of foundMsg
|
||||
repeat with r in recipList
|
||||
set mto to mto & address of r & ", "
|
||||
end repeat
|
||||
if mto ends with ", " then set mto to text 1 thru -3 of mto
|
||||
end try
|
||||
set output to output & "To: " & mto & linefeed
|
||||
|
||||
set output to output & "Date: " & date received of foundMsg & linefeed
|
||||
set output to output & "Subject: " & subject of foundMsg & linefeed
|
||||
set output to output & linefeed & "---" & linefeed & linefeed
|
||||
set output to output & content of foundMsg
|
||||
|
||||
return output
|
||||
on error errMsg
|
||||
return "ERROR:" & errMsg
|
||||
end try
|
||||
end tell
|
||||
EOF
|
||||
)
|
||||
|
||||
if [[ "$RESULT" == ERROR:* ]]; then
|
||||
echo "${RESULT#ERROR:}" >&2
|
||||
FAILED_COUNT=$((FAILED_COUNT + 1))
|
||||
else
|
||||
echo "$RESULT"
|
||||
READ_COUNT=$((READ_COUNT + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Print summary if multiple messages
|
||||
if [ $# -gt 1 ]; then
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Summary: $READ_COUNT read, $FAILED_COUNT failed"
|
||||
fi
|
||||
126
skills/apple-mail/scripts/mail-refresh.sh
Normal file
126
skills/apple-mail/scripts/mail-refresh.sh
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/bin/bash
|
||||
# Force Mail.app to check for new mail across all accounts (or a specific account)
|
||||
# Usage: mail-refresh.sh [account] [wait_seconds]
|
||||
#
|
||||
# Arguments:
|
||||
# account - Optional: specific account name (from mail-accounts.sh)
|
||||
# wait_seconds - Optional: max seconds to wait for sync (default: 10, 0 = no wait)
|
||||
#
|
||||
# Examples:
|
||||
# mail-refresh.sh # Refresh all accounts, wait up to 10s
|
||||
# mail-refresh.sh Google # Refresh only Google account
|
||||
# mail-refresh.sh "" 5 # Refresh all, wait up to 5 seconds
|
||||
# mail-refresh.sh Google 0 # Refresh Google, return immediately
|
||||
#
|
||||
# The script will return early if sync appears complete (database stops updating).
|
||||
|
||||
set -e
|
||||
|
||||
ACCOUNT="${1:-}"
|
||||
MAX_WAIT="${2:-10}"
|
||||
|
||||
# Ensure wait is a number
|
||||
if ! [[ "$MAX_WAIT" =~ ^[0-9]+$ ]]; then
|
||||
echo "ERROR: wait_seconds must be a non-negative integer" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Mail.app is running
|
||||
if ! pgrep -q "Mail"; then
|
||||
echo "ERROR: Mail.app is not running. Please open Mail.app first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find the database
|
||||
find_db() {
|
||||
for v in 11 10 9; do
|
||||
local db="$HOME/Library/Mail/V$v/MailData/Envelope Index"
|
||||
if [[ -f "$db" ]]; then
|
||||
echo "$db"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
DB_PATH=$(find_db)
|
||||
|
||||
# Get initial message count
|
||||
get_msg_count() {
|
||||
if [[ -n "$DB_PATH" ]]; then
|
||||
sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM messages;" 2>/dev/null || echo "0"
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
INITIAL_COUNT=$(get_msg_count)
|
||||
|
||||
if [ -n "$ACCOUNT" ]; then
|
||||
# Refresh specific account
|
||||
ACCOUNT_EXISTS=$(osascript -e "tell application \"Mail\" to exists account \"$ACCOUNT\"" 2>/dev/null || echo "false")
|
||||
|
||||
if [ "$ACCOUNT_EXISTS" != "true" ]; then
|
||||
echo "ERROR: Account '$ACCOUNT' not found. Run mail-accounts.sh to see available accounts." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
osascript <<EOF
|
||||
tell application "Mail"
|
||||
check for new mail in account "$ACCOUNT"
|
||||
end tell
|
||||
EOF
|
||||
|
||||
echo "Refresh triggered for account: $ACCOUNT"
|
||||
else
|
||||
# Refresh all accounts
|
||||
osascript <<EOF
|
||||
tell application "Mail"
|
||||
check for new mail
|
||||
end tell
|
||||
EOF
|
||||
|
||||
echo "Refresh triggered for all accounts"
|
||||
fi
|
||||
|
||||
# Wait for sync with smart detection
|
||||
if [ "$MAX_WAIT" -gt 0 ]; then
|
||||
echo "Waiting for sync (max ${MAX_WAIT}s)..."
|
||||
|
||||
STABLE_COUNT=0
|
||||
LAST_COUNT=$INITIAL_COUNT
|
||||
|
||||
for ((i=1; i<=MAX_WAIT; i++)); do
|
||||
sleep 1
|
||||
CURRENT_COUNT=$(get_msg_count)
|
||||
|
||||
if [ "$CURRENT_COUNT" != "$LAST_COUNT" ]; then
|
||||
# Database changed, reset stability counter
|
||||
STABLE_COUNT=0
|
||||
LAST_COUNT=$CURRENT_COUNT
|
||||
else
|
||||
# No change, increment stability counter
|
||||
STABLE_COUNT=$((STABLE_COUNT + 1))
|
||||
fi
|
||||
|
||||
# Consider stable after 2 seconds of no changes
|
||||
if [ "$STABLE_COUNT" -ge 2 ]; then
|
||||
NEW_MSGS=$((CURRENT_COUNT - INITIAL_COUNT))
|
||||
if [ "$NEW_MSGS" -gt 0 ]; then
|
||||
echo "Sync complete in ${i}s (+${NEW_MSGS} messages)"
|
||||
else
|
||||
echo "Sync complete in ${i}s (no new messages)"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
# Timeout reached
|
||||
FINAL_COUNT=$(get_msg_count)
|
||||
NEW_MSGS=$((FINAL_COUNT - INITIAL_COUNT))
|
||||
if [ "$NEW_MSGS" -gt 0 ]; then
|
||||
echo "Timeout reached (+${NEW_MSGS} messages, sync may still be in progress)"
|
||||
else
|
||||
echo "Timeout reached (no new messages detected)"
|
||||
fi
|
||||
fi
|
||||
50
skills/apple-mail/scripts/mail-reply.sh
Normal file
50
skills/apple-mail/scripts/mail-reply.sh
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# Reply to an email by message ID
|
||||
# Usage: mail-reply.sh <message-id> "Reply body" [reply-all]
|
||||
|
||||
MSG_ID="${1:-}"
|
||||
REPLY_BODY="${2:-}"
|
||||
REPLY_ALL="${3:-false}"
|
||||
|
||||
if [ -z "$MSG_ID" ] || [ -z "$REPLY_BODY" ]; then
|
||||
echo "Usage: mail-reply.sh <message-id> \"Reply body\" [reply-all]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPLY_BODY_ESCAPED=$(echo "$REPLY_BODY" | sed 's/"/\\"/g')
|
||||
|
||||
osascript <<EOF
|
||||
tell application "Mail"
|
||||
set foundMsg to missing value
|
||||
|
||||
-- Search all accounts for the message
|
||||
repeat with acct in every account
|
||||
repeat with mbox in every mailbox of acct
|
||||
try
|
||||
set msgs to (messages of mbox whose id is $MSG_ID)
|
||||
if (count of msgs) > 0 then
|
||||
set foundMsg to item 1 of msgs
|
||||
exit repeat
|
||||
end if
|
||||
end try
|
||||
end repeat
|
||||
if foundMsg is not missing value then exit repeat
|
||||
end repeat
|
||||
|
||||
if foundMsg is missing value then
|
||||
return "Message not found with ID: $MSG_ID"
|
||||
end if
|
||||
|
||||
if "$REPLY_ALL" is "true" then
|
||||
set replyMsg to reply foundMsg with opening window and reply to all
|
||||
else
|
||||
set replyMsg to reply foundMsg with opening window
|
||||
end if
|
||||
|
||||
set oldContent to content of replyMsg
|
||||
set content of replyMsg to "$REPLY_BODY_ESCAPED" & return & return & oldContent
|
||||
send replyMsg
|
||||
|
||||
return "Reply sent"
|
||||
end tell
|
||||
EOF
|
||||
65
skills/apple-mail/scripts/mail-search.sh
Normal file
65
skills/apple-mail/scripts/mail-search.sh
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
# Search emails by subject/sender/content
|
||||
# Usage: mail-search.sh "query" [mailbox] [limit]
|
||||
|
||||
QUERY="${1:-}"
|
||||
MAILBOX="${2:-}"
|
||||
LIMIT="${3:-20}"
|
||||
|
||||
if [ -z "$QUERY" ]; then
|
||||
echo "Usage: mail-search.sh \"query\" [mailbox] [limit]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
osascript <<EOF
|
||||
tell application "Mail"
|
||||
set output to ""
|
||||
set foundMsgs to {}
|
||||
set searchQuery to "$QUERY"
|
||||
set limitCount to $LIMIT
|
||||
|
||||
if "$MAILBOX" is not "" then
|
||||
-- Search specific mailbox across accounts
|
||||
repeat with acct in every account
|
||||
try
|
||||
set targetMailbox to mailbox "$MAILBOX" of acct
|
||||
set msgs to (messages of targetMailbox whose subject contains searchQuery or sender contains searchQuery)
|
||||
repeat with m in msgs
|
||||
set end of foundMsgs to m
|
||||
end repeat
|
||||
end try
|
||||
end repeat
|
||||
else
|
||||
-- Search all mailboxes
|
||||
repeat with acct in every account
|
||||
repeat with mbox in every mailbox of acct
|
||||
try
|
||||
set msgs to (messages of mbox whose subject contains searchQuery or sender contains searchQuery)
|
||||
repeat with m in msgs
|
||||
set end of foundMsgs to m
|
||||
end repeat
|
||||
end try
|
||||
end repeat
|
||||
end repeat
|
||||
end if
|
||||
|
||||
if (count of foundMsgs) < limitCount then set limitCount to (count of foundMsgs)
|
||||
|
||||
repeat with i from 1 to limitCount
|
||||
set m to item i of foundMsgs
|
||||
set mid to id of m
|
||||
set msubject to subject of m
|
||||
set msender to sender of m
|
||||
set mdate to date received of m
|
||||
set mread to read status of m
|
||||
set readFlag to "●"
|
||||
if mread then set readFlag to " "
|
||||
set output to output & mid & " | " & readFlag & " | " & mdate & " | " & msender & " | " & msubject & linefeed
|
||||
end repeat
|
||||
|
||||
if output is "" then
|
||||
return "No emails found matching: " & searchQuery
|
||||
end if
|
||||
return output
|
||||
end tell
|
||||
EOF
|
||||
73
skills/apple-mail/scripts/mail-send.sh
Normal file
73
skills/apple-mail/scripts/mail-send.sh
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
# Send an email via Mail.app
|
||||
# Usage: mail-send.sh "to@email.com" "Subject" "Body" [from-account] [attachment]
|
||||
|
||||
TO="${1:-}"
|
||||
SUBJECT="${2:-}"
|
||||
BODY="${3:-}"
|
||||
FROM_ACCOUNT="${4:-}"
|
||||
ATTACHMENT="${5:-}"
|
||||
|
||||
if [ -z "$TO" ] || [ -z "$SUBJECT" ] || [ -z "$BODY" ]; then
|
||||
echo "Usage: mail-send.sh \"to@email.com\" \"Subject\" \"Body\" [from-account] [attachment]"
|
||||
echo " All three arguments (to, subject, body) are required."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Escape quotes in body and trim whitespace
|
||||
BODY_ESCAPED=$(printf '%s' "$BODY" | sed 's/"/\\"/g')
|
||||
SUBJECT_ESCAPED=$(printf '%s' "$SUBJECT" | sed 's/"/\\"/g')
|
||||
|
||||
if [ -n "$FROM_ACCOUNT" ] && [ -n "$ATTACHMENT" ]; then
|
||||
osascript <<EOF
|
||||
tell application "Mail"
|
||||
set newMessage to make new outgoing message with properties {subject:"$SUBJECT_ESCAPED", content:"$BODY_ESCAPED", visible:false}
|
||||
tell newMessage
|
||||
make new to recipient at end of to recipients with properties {address:"$TO"}
|
||||
set sender to "$FROM_ACCOUNT"
|
||||
tell content
|
||||
make new attachment with properties {file name:POSIX file "$ATTACHMENT"} at after last paragraph
|
||||
end tell
|
||||
end tell
|
||||
send newMessage
|
||||
return "Email sent to $TO"
|
||||
end tell
|
||||
EOF
|
||||
elif [ -n "$FROM_ACCOUNT" ]; then
|
||||
osascript <<EOF
|
||||
tell application "Mail"
|
||||
set newMessage to make new outgoing message with properties {subject:"$SUBJECT_ESCAPED", content:"$BODY_ESCAPED", visible:false}
|
||||
tell newMessage
|
||||
make new to recipient at end of to recipients with properties {address:"$TO"}
|
||||
set sender to "$FROM_ACCOUNT"
|
||||
end tell
|
||||
send newMessage
|
||||
return "Email sent to $TO"
|
||||
end tell
|
||||
EOF
|
||||
elif [ -n "$ATTACHMENT" ]; then
|
||||
osascript <<EOF
|
||||
tell application "Mail"
|
||||
set newMessage to make new outgoing message with properties {subject:"$SUBJECT_ESCAPED", content:"$BODY_ESCAPED", visible:false}
|
||||
tell newMessage
|
||||
make new to recipient at end of to recipients with properties {address:"$TO"}
|
||||
tell content
|
||||
make new attachment with properties {file name:POSIX file "$ATTACHMENT"} at after last paragraph
|
||||
end tell
|
||||
end tell
|
||||
send newMessage
|
||||
return "Email sent to $TO"
|
||||
end tell
|
||||
EOF
|
||||
else
|
||||
osascript <<EOF
|
||||
tell application "Mail"
|
||||
set newMessage to make new outgoing message with properties {subject:"$SUBJECT_ESCAPED", content:"$BODY_ESCAPED", visible:false}
|
||||
tell newMessage
|
||||
make new to recipient at end of to recipients with properties {address:"$TO"}
|
||||
end tell
|
||||
send newMessage
|
||||
return "Email sent to $TO"
|
||||
end tell
|
||||
EOF
|
||||
fi
|
||||
Reference in New Issue
Block a user