Initial backup 2026-02-17
This commit is contained in:
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