Initial backup 2026-02-17

This commit is contained in:
Krilly
2026-02-17 15:50:53 +00:00
commit 8902a93add
941 changed files with 131420 additions and 0 deletions

View 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

View 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"

View 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};
"

View 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

View 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

View 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"

View 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"

View 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()

View 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

View 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

View 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

View 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

View 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