|
@@ -81,12 +81,25 @@ SQL
|
|
|
# Migration: create projects table if missing (for existing installs)
|
|
# Migration: create projects table if missing (for existing installs)
|
|
|
sqlite3 "$MAIL_DB" "SELECT hash FROM projects LIMIT 0;" 2>/dev/null || \
|
|
sqlite3 "$MAIL_DB" "SELECT hash FROM projects LIMIT 0;" 2>/dev/null || \
|
|
|
sqlite3 "$MAIL_DB" "CREATE TABLE IF NOT EXISTS projects (hash TEXT PRIMARY KEY, name TEXT NOT NULL, path TEXT NOT NULL, registered TEXT DEFAULT (datetime('now')));" 2>/dev/null
|
|
sqlite3 "$MAIL_DB" "CREATE TABLE IF NOT EXISTS projects (hash TEXT PRIMARY KEY, name TEXT NOT NULL, path TEXT NOT NULL, registered TEXT DEFAULT (datetime('now')));" 2>/dev/null
|
|
|
|
|
+ # Migration: add thread_id column if missing
|
|
|
|
|
+ sqlite3 "$MAIL_DB" "SELECT thread_id FROM messages LIMIT 0;" 2>/dev/null || \
|
|
|
|
|
+ sqlite3 "$MAIL_DB" "ALTER TABLE messages ADD COLUMN thread_id INTEGER REFERENCES messages(id);" 2>/dev/null
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
sql_escape() {
|
|
sql_escape() {
|
|
|
printf '%s' "$1" | sed "s/'/''/g"
|
|
printf '%s' "$1" | sed "s/'/''/g"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+# Read body from argument or stdin (use - or omit for stdin)
|
|
|
|
|
+read_body() {
|
|
|
|
|
+ local arg="$1"
|
|
|
|
|
+ if [ "$arg" = "-" ] || [ -z "$arg" ]; then
|
|
|
|
|
+ cat
|
|
|
|
|
+ else
|
|
|
|
|
+ printf '%s' "$arg"
|
|
|
|
|
+ fi
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
# Register current project in the projects table (idempotent)
|
|
# Register current project in the projects table (idempotent)
|
|
|
register_project() {
|
|
register_project() {
|
|
|
local hash name path
|
|
local hash name path
|
|
@@ -200,20 +213,29 @@ read_mail() {
|
|
|
register_project
|
|
register_project
|
|
|
local pid
|
|
local pid
|
|
|
pid=$(get_project_id)
|
|
pid=$(get_project_id)
|
|
|
- local rows
|
|
|
|
|
- rows=$(sqlite3 -separator '|' "$MAIL_DB" \
|
|
|
|
|
- "SELECT id, from_project, subject, body, timestamp FROM messages WHERE to_project='${pid}' AND read=0 ORDER BY timestamp ASC;")
|
|
|
|
|
- if [ -z "$rows" ]; then
|
|
|
|
|
- return 0
|
|
|
|
|
- fi
|
|
|
|
|
|
|
+ # Use ASCII record separator (0x1E) to avoid splitting on pipes/newlines in body
|
|
|
|
|
+ local RS=$'\x1e'
|
|
|
|
|
+ local count
|
|
|
|
|
+ count=$(sqlite3 "$MAIL_DB" "SELECT COUNT(*) FROM messages WHERE to_project='${pid}' AND read=0;")
|
|
|
|
|
+ [ "${count:-0}" -eq 0 ] && return 0
|
|
|
|
|
+ # Query each message individually to preserve multi-line bodies
|
|
|
|
|
+ local ids
|
|
|
|
|
+ ids=$(sqlite3 "$MAIL_DB" "SELECT id FROM messages WHERE to_project='${pid}' AND read=0 ORDER BY timestamp ASC;")
|
|
|
echo "id | from_project | subject | body | timestamp"
|
|
echo "id | from_project | subject | body | timestamp"
|
|
|
- while IFS='|' read -r id from_hash subj body ts; do
|
|
|
|
|
- local from_name
|
|
|
|
|
|
|
+ while read -r msg_id; do
|
|
|
|
|
+ [ -z "$msg_id" ] && continue
|
|
|
|
|
+ local from_hash subj body ts from_name
|
|
|
|
|
+ from_hash=$(sqlite3 "$MAIL_DB" "SELECT from_project FROM messages WHERE id=${msg_id};")
|
|
|
|
|
+ subj=$(sqlite3 "$MAIL_DB" "SELECT subject FROM messages WHERE id=${msg_id};")
|
|
|
|
|
+ body=$(sqlite3 "$MAIL_DB" "SELECT body FROM messages WHERE id=${msg_id};")
|
|
|
|
|
+ ts=$(sqlite3 "$MAIL_DB" "SELECT timestamp FROM messages WHERE id=${msg_id};")
|
|
|
from_name=$(display_name "$from_hash")
|
|
from_name=$(display_name "$from_hash")
|
|
|
- echo "${id} | ${from_name} (${from_hash}) | ${subj} | ${body} | ${ts}"
|
|
|
|
|
- done <<< "$rows"
|
|
|
|
|
|
|
+ echo "${msg_id} | ${from_name} (${from_hash}) | ${subj} | ${body} | ${ts}"
|
|
|
|
|
+ done <<< "$ids"
|
|
|
sqlite3 "$MAIL_DB" \
|
|
sqlite3 "$MAIL_DB" \
|
|
|
"UPDATE messages SET read=1 WHERE to_project='${pid}' AND read=0;"
|
|
"UPDATE messages SET read=1 WHERE to_project='${pid}' AND read=0;"
|
|
|
|
|
+ # Clear signal file
|
|
|
|
|
+ rm -f "/tmp/agentmail_signal_${pid}"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
read_one() {
|
|
read_one() {
|
|
@@ -223,18 +245,19 @@ read_one() {
|
|
|
return 1
|
|
return 1
|
|
|
fi
|
|
fi
|
|
|
init_db
|
|
init_db
|
|
|
- local row
|
|
|
|
|
- row=$(sqlite3 -separator '|' "$MAIL_DB" \
|
|
|
|
|
- "SELECT id, from_project, to_project, subject, body, timestamp FROM messages WHERE id=${msg_id};")
|
|
|
|
|
- if [ -n "$row" ]; then
|
|
|
|
|
- echo "id | from_project | to_project | subject | body | timestamp"
|
|
|
|
|
- local id from_hash to_hash subj body ts
|
|
|
|
|
- IFS='|' read -r id from_hash to_hash subj body ts <<< "$row"
|
|
|
|
|
- local from_name to_name
|
|
|
|
|
- from_name=$(display_name "$from_hash")
|
|
|
|
|
- to_name=$(display_name "$to_hash")
|
|
|
|
|
- echo "${id} | ${from_name} (${from_hash}) | ${to_name} (${to_hash}) | ${subj} | ${body} | ${ts}"
|
|
|
|
|
- fi
|
|
|
|
|
|
|
+ local exists
|
|
|
|
|
+ exists=$(sqlite3 "$MAIL_DB" "SELECT COUNT(*) FROM messages WHERE id=${msg_id};")
|
|
|
|
|
+ [ "${exists:-0}" -eq 0 ] && return 0
|
|
|
|
|
+ local from_hash to_hash subj body ts from_name to_name
|
|
|
|
|
+ from_hash=$(sqlite3 "$MAIL_DB" "SELECT from_project FROM messages WHERE id=${msg_id};")
|
|
|
|
|
+ to_hash=$(sqlite3 "$MAIL_DB" "SELECT to_project FROM messages WHERE id=${msg_id};")
|
|
|
|
|
+ subj=$(sqlite3 "$MAIL_DB" "SELECT subject FROM messages WHERE id=${msg_id};")
|
|
|
|
|
+ body=$(sqlite3 "$MAIL_DB" "SELECT body FROM messages WHERE id=${msg_id};")
|
|
|
|
|
+ ts=$(sqlite3 "$MAIL_DB" "SELECT timestamp FROM messages WHERE id=${msg_id};")
|
|
|
|
|
+ from_name=$(display_name "$from_hash")
|
|
|
|
|
+ to_name=$(display_name "$to_hash")
|
|
|
|
|
+ echo "id | from_project | to_project | subject | body | timestamp"
|
|
|
|
|
+ echo "${msg_id} | ${from_name} (${from_hash}) | ${to_name} (${to_hash}) | ${subj} | ${body} | ${ts}"
|
|
|
sqlite3 "$MAIL_DB" \
|
|
sqlite3 "$MAIL_DB" \
|
|
|
"UPDATE messages SET read=1 WHERE id=${msg_id};"
|
|
"UPDATE messages SET read=1 WHERE id=${msg_id};"
|
|
|
}
|
|
}
|
|
@@ -247,7 +270,8 @@ send() {
|
|
|
fi
|
|
fi
|
|
|
local to_input="${1:?to_project required}"
|
|
local to_input="${1:?to_project required}"
|
|
|
local subject="${2:-no subject}"
|
|
local subject="${2:-no subject}"
|
|
|
- local body="${3:?body required}"
|
|
|
|
|
|
|
+ local body
|
|
|
|
|
+ body=$(read_body "${3:-}")
|
|
|
if [ -z "$body" ]; then
|
|
if [ -z "$body" ]; then
|
|
|
echo "Error: message body cannot be empty" >&2
|
|
echo "Error: message body cannot be empty" >&2
|
|
|
return 1
|
|
return 1
|
|
@@ -262,11 +286,31 @@ send() {
|
|
|
safe_body=$(sql_escape "$body")
|
|
safe_body=$(sql_escape "$body")
|
|
|
sqlite3 "$MAIL_DB" \
|
|
sqlite3 "$MAIL_DB" \
|
|
|
"INSERT INTO messages (from_project, to_project, subject, body, priority) VALUES ('${from_id}', '${to_id}', '${safe_subject}', '${safe_body}', '${priority}');"
|
|
"INSERT INTO messages (from_project, to_project, subject, body, priority) VALUES ('${from_id}', '${to_id}', '${safe_subject}', '${safe_body}', '${priority}');"
|
|
|
|
|
+ # Signal the recipient
|
|
|
|
|
+ touch "/tmp/agentmail_signal_${to_id}"
|
|
|
local to_name
|
|
local to_name
|
|
|
to_name=$(display_name "$to_id")
|
|
to_name=$(display_name "$to_id")
|
|
|
echo "Sent to ${to_name} (${to_id}): ${subject}$([ "$priority" = "urgent" ] && echo " [URGENT]" || true)"
|
|
echo "Sent to ${to_name} (${to_id}): ${subject}$([ "$priority" = "urgent" ] && echo " [URGENT]" || true)"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+sent() {
|
|
|
|
|
+ local limit="${1:-20}"
|
|
|
|
|
+ init_db
|
|
|
|
|
+ register_project
|
|
|
|
|
+ local pid
|
|
|
|
|
+ pid=$(get_project_id)
|
|
|
|
|
+ local rows
|
|
|
|
|
+ rows=$(sqlite3 -separator '|' "$MAIL_DB" \
|
|
|
|
|
+ "SELECT id, to_project, subject, timestamp FROM messages WHERE from_project='${pid}' ORDER BY timestamp DESC LIMIT ${limit};")
|
|
|
|
|
+ [ -z "$rows" ] && echo "No sent messages" && return 0
|
|
|
|
|
+ echo "id | to | subject | timestamp"
|
|
|
|
|
+ while IFS='|' read -r id to_hash subj ts; do
|
|
|
|
|
+ local to_name
|
|
|
|
|
+ to_name=$(display_name "$to_hash")
|
|
|
|
|
+ echo "${id} | ${to_name} (${to_hash}) | ${subj} | ${ts}"
|
|
|
|
|
+ done <<< "$rows"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
search() {
|
|
search() {
|
|
|
local keyword="$1"
|
|
local keyword="$1"
|
|
|
if [ -z "$keyword" ]; then
|
|
if [ -z "$keyword" ]; then
|
|
@@ -326,7 +370,8 @@ clear_old() {
|
|
|
|
|
|
|
|
reply() {
|
|
reply() {
|
|
|
local msg_id="$1"
|
|
local msg_id="$1"
|
|
|
- local body="$2"
|
|
|
|
|
|
|
+ local body
|
|
|
|
|
+ body=$(read_body "${2:-}")
|
|
|
if ! [[ "$msg_id" =~ ^[0-9]+$ ]]; then
|
|
if ! [[ "$msg_id" =~ ^[0-9]+$ ]]; then
|
|
|
echo "Error: message ID must be numeric" >&2
|
|
echo "Error: message ID must be numeric" >&2
|
|
|
return 1
|
|
return 1
|
|
@@ -338,26 +383,65 @@ reply() {
|
|
|
init_db
|
|
init_db
|
|
|
register_project
|
|
register_project
|
|
|
local orig
|
|
local orig
|
|
|
- orig=$(sqlite3 -separator '|' "$MAIL_DB" "SELECT from_project, subject FROM messages WHERE id=${msg_id};")
|
|
|
|
|
|
|
+ orig=$(sqlite3 -separator '|' "$MAIL_DB" "SELECT from_project, subject, thread_id FROM messages WHERE id=${msg_id};")
|
|
|
if [ -z "$orig" ]; then
|
|
if [ -z "$orig" ]; then
|
|
|
echo "Error: message #${msg_id} not found" >&2
|
|
echo "Error: message #${msg_id} not found" >&2
|
|
|
return 1
|
|
return 1
|
|
|
fi
|
|
fi
|
|
|
- local orig_from_hash orig_subject
|
|
|
|
|
|
|
+ local orig_from_hash orig_subject orig_thread
|
|
|
orig_from_hash=$(echo "$orig" | cut -d'|' -f1)
|
|
orig_from_hash=$(echo "$orig" | cut -d'|' -f1)
|
|
|
orig_subject=$(echo "$orig" | cut -d'|' -f2)
|
|
orig_subject=$(echo "$orig" | cut -d'|' -f2)
|
|
|
|
|
+ orig_thread=$(echo "$orig" | cut -d'|' -f3)
|
|
|
|
|
+ # Thread ID: inherit from parent, or use parent's ID as thread root
|
|
|
|
|
+ local thread_id="${orig_thread:-$msg_id}"
|
|
|
local from_id
|
|
local from_id
|
|
|
from_id=$(get_project_id)
|
|
from_id=$(get_project_id)
|
|
|
local safe_subject safe_body
|
|
local safe_subject safe_body
|
|
|
safe_subject=$(sql_escape "Re: ${orig_subject}")
|
|
safe_subject=$(sql_escape "Re: ${orig_subject}")
|
|
|
safe_body=$(sql_escape "$body")
|
|
safe_body=$(sql_escape "$body")
|
|
|
sqlite3 "$MAIL_DB" \
|
|
sqlite3 "$MAIL_DB" \
|
|
|
- "INSERT INTO messages (from_project, to_project, subject, body) VALUES ('${from_id}', '${orig_from_hash}', '${safe_subject}', '${safe_body}');"
|
|
|
|
|
|
|
+ "INSERT INTO messages (from_project, to_project, subject, body, thread_id) VALUES ('${from_id}', '${orig_from_hash}', '${safe_subject}', '${safe_body}', ${thread_id});"
|
|
|
|
|
+ # Signal the recipient
|
|
|
|
|
+ touch "/tmp/agentmail_signal_${orig_from_hash}"
|
|
|
local orig_name
|
|
local orig_name
|
|
|
orig_name=$(display_name "$orig_from_hash")
|
|
orig_name=$(display_name "$orig_from_hash")
|
|
|
echo "Replied to ${orig_name} (${orig_from_hash}): Re: ${orig_subject}"
|
|
echo "Replied to ${orig_name} (${orig_from_hash}): Re: ${orig_subject}"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+thread() {
|
|
|
|
|
+ local msg_id="$1"
|
|
|
|
|
+ if ! [[ "$msg_id" =~ ^[0-9]+$ ]]; then
|
|
|
|
|
+ echo "Error: message ID must be numeric" >&2
|
|
|
|
|
+ return 1
|
|
|
|
|
+ fi
|
|
|
|
|
+ init_db
|
|
|
|
|
+ # Find the thread root: either the message itself or its thread_id
|
|
|
|
|
+ local thread_root
|
|
|
|
|
+ thread_root=$(sqlite3 "$MAIL_DB" "SELECT COALESCE(thread_id, id) FROM messages WHERE id=${msg_id};" 2>/dev/null)
|
|
|
|
|
+ [ -z "$thread_root" ] && echo "Message not found" && return 1
|
|
|
|
|
+ # Get all message IDs in this thread (root + replies)
|
|
|
|
|
+ local ids
|
|
|
|
|
+ ids=$(sqlite3 "$MAIL_DB" \
|
|
|
|
|
+ "SELECT id FROM messages WHERE id=${thread_root} OR thread_id=${thread_root} ORDER BY timestamp ASC;")
|
|
|
|
|
+ [ -z "$ids" ] && echo "No thread found" && return 0
|
|
|
|
|
+ local msg_count=0
|
|
|
|
|
+ echo "=== Thread #${thread_root} ==="
|
|
|
|
|
+ while read -r tid; do
|
|
|
|
|
+ [ -z "$tid" ] && continue
|
|
|
|
|
+ local from_hash body ts from_name
|
|
|
|
|
+ from_hash=$(sqlite3 "$MAIL_DB" "SELECT from_project FROM messages WHERE id=${tid};")
|
|
|
|
|
+ body=$(sqlite3 "$MAIL_DB" "SELECT body FROM messages WHERE id=${tid};")
|
|
|
|
|
+ ts=$(sqlite3 "$MAIL_DB" "SELECT timestamp FROM messages WHERE id=${tid};")
|
|
|
|
|
+ from_name=$(display_name "$from_hash")
|
|
|
|
|
+ echo ""
|
|
|
|
|
+ echo "--- #${tid} ${from_name} @ ${ts} ---"
|
|
|
|
|
+ echo "${body}"
|
|
|
|
|
+ msg_count=$((msg_count + 1))
|
|
|
|
|
+ done <<< "$ids"
|
|
|
|
|
+ echo ""
|
|
|
|
|
+ echo "=== End of thread (${msg_count} messages) ==="
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
broadcast() {
|
|
broadcast() {
|
|
|
local subject="$1"
|
|
local subject="$1"
|
|
|
local body="$2"
|
|
local body="$2"
|
|
@@ -380,6 +464,7 @@ broadcast() {
|
|
|
[ -z "$target_hash" ] && continue
|
|
[ -z "$target_hash" ] && continue
|
|
|
sqlite3 "$MAIL_DB" \
|
|
sqlite3 "$MAIL_DB" \
|
|
|
"INSERT INTO messages (from_project, to_project, subject, body) VALUES ('${from_id}', '${target_hash}', '${safe_subject}', '${safe_body}');"
|
|
"INSERT INTO messages (from_project, to_project, subject, body) VALUES ('${from_id}', '${target_hash}', '${safe_subject}', '${safe_body}');"
|
|
|
|
|
+ touch "/tmp/agentmail_signal_${target_hash}"
|
|
|
count=$((count + 1))
|
|
count=$((count + 1))
|
|
|
done <<< "$targets"
|
|
done <<< "$targets"
|
|
|
echo "Broadcast to ${count} project(s): ${subject}"
|
|
echo "Broadcast to ${count} project(s): ${subject}"
|
|
@@ -532,7 +617,9 @@ case "${1:-help}" in
|
|
|
unread) list_unread ;;
|
|
unread) list_unread ;;
|
|
|
read) if [ -n "${2:-}" ]; then read_one "$2"; else read_mail; fi ;;
|
|
read) if [ -n "${2:-}" ]; then read_one "$2"; else read_mail; fi ;;
|
|
|
send) shift; send "$@" ;;
|
|
send) shift; send "$@" ;;
|
|
|
- reply) reply "${2:?message_id required}" "${3:?body required}" ;;
|
|
|
|
|
|
|
+ reply) reply "${2:?message_id required}" "${3:-}" ;;
|
|
|
|
|
+ sent) sent "${2:-20}" ;;
|
|
|
|
|
+ thread) thread "${2:?message_id required}" ;;
|
|
|
list) list_all "${2:-20}" ;;
|
|
list) list_all "${2:-20}" ;;
|
|
|
clear) clear_old "${2:-7}" ;;
|
|
clear) clear_old "${2:-7}" ;;
|
|
|
broadcast) broadcast "${2:-no subject}" "${3:?body required}" ;;
|
|
broadcast) broadcast "${2:-no subject}" "${3:?body required}" ;;
|
|
@@ -552,9 +639,10 @@ case "${1:-help}" in
|
|
|
echo " count Count unread messages"
|
|
echo " count Count unread messages"
|
|
|
echo " unread List unread messages (brief)"
|
|
echo " unread List unread messages (brief)"
|
|
|
echo " read [id] Read messages and mark as read"
|
|
echo " read [id] Read messages and mark as read"
|
|
|
- echo " send [--urgent] <to> <subj> <body>"
|
|
|
|
|
- echo " Send a message (to = name, hash, or path)"
|
|
|
|
|
- echo " reply <id> <body> Reply to a message"
|
|
|
|
|
|
|
+ echo " send [--urgent] <to> <subj> <body|-> Send (- or pipe for stdin body)"
|
|
|
|
|
+ echo " reply <id> <body|-> Reply (- or pipe for stdin body)"
|
|
|
|
|
+ echo " sent [limit] Show sent messages (outbox)"
|
|
|
|
|
+ echo " thread <id> View full conversation thread"
|
|
|
echo " list [limit] List recent messages (default 20)"
|
|
echo " list [limit] List recent messages (default 20)"
|
|
|
echo " clear [days] Clear read messages older than N days"
|
|
echo " clear [days] Clear read messages older than N days"
|
|
|
echo " broadcast <subj> <body> Send to all known projects"
|
|
echo " broadcast <subj> <body> Send to all known projects"
|