mail-db.sh 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. #!/bin/bash
  2. # mail-db.sh - SQLite mail database operations
  3. # Global mail database at ~/.claude/mail.db
  4. # Project identity derived from basename of working directory
  5. set -euo pipefail
  6. MAIL_DB="$HOME/.claude/mail.db"
  7. # Ensure database and schema exist
  8. init_db() {
  9. mkdir -p "$(dirname "$MAIL_DB")"
  10. sqlite3 "$MAIL_DB" <<'SQL'
  11. CREATE TABLE IF NOT EXISTS messages (
  12. id INTEGER PRIMARY KEY AUTOINCREMENT,
  13. from_project TEXT NOT NULL,
  14. to_project TEXT NOT NULL,
  15. subject TEXT DEFAULT '',
  16. body TEXT NOT NULL,
  17. timestamp TEXT DEFAULT (datetime('now')),
  18. read INTEGER DEFAULT 0
  19. );
  20. CREATE INDEX IF NOT EXISTS idx_unread ON messages(to_project, read);
  21. CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp);
  22. SQL
  23. }
  24. # Sanitize string for safe SQL interpolation (escape single quotes)
  25. sql_escape() {
  26. printf '%s' "$1" | sed "s/'/''/g"
  27. }
  28. # Get project name from cwd
  29. get_project() {
  30. basename "$PWD"
  31. }
  32. # Count unread messages for current project
  33. count_unread() {
  34. init_db
  35. local project
  36. project=$(sql_escape "$(get_project)")
  37. sqlite3 "$MAIL_DB" "SELECT COUNT(*) FROM messages WHERE to_project='${project}' AND read=0;"
  38. }
  39. # List unread messages (brief) for current project
  40. list_unread() {
  41. init_db
  42. local project
  43. project=$(sql_escape "$(get_project)")
  44. sqlite3 -separator ' | ' "$MAIL_DB" \
  45. "SELECT id, from_project, subject, timestamp FROM messages WHERE to_project='${project}' AND read=0 ORDER BY timestamp DESC;"
  46. }
  47. # Read all unread messages (full) and mark as read
  48. read_mail() {
  49. init_db
  50. local project
  51. project=$(sql_escape "$(get_project)")
  52. sqlite3 -header -separator ' | ' "$MAIL_DB" \
  53. "SELECT id, from_project, subject, body, timestamp FROM messages WHERE to_project='${project}' AND read=0 ORDER BY timestamp ASC;"
  54. sqlite3 "$MAIL_DB" \
  55. "UPDATE messages SET read=1 WHERE to_project='${project}' AND read=0;"
  56. }
  57. # Read a single message by ID and mark as read
  58. read_one() {
  59. local msg_id="$1"
  60. # Validate ID is numeric
  61. if ! [[ "$msg_id" =~ ^[0-9]+$ ]]; then
  62. echo "Error: message ID must be numeric" >&2
  63. return 1
  64. fi
  65. init_db
  66. sqlite3 -header -separator ' | ' "$MAIL_DB" \
  67. "SELECT id, from_project, to_project, subject, body, timestamp FROM messages WHERE id=${msg_id};"
  68. sqlite3 "$MAIL_DB" \
  69. "UPDATE messages SET read=1 WHERE id=${msg_id};"
  70. }
  71. # Send a message
  72. send() {
  73. local to_project="$1"
  74. local subject="$2"
  75. local body="$3"
  76. if [ -z "$body" ]; then
  77. echo "Error: message body cannot be empty" >&2
  78. return 1
  79. fi
  80. init_db
  81. local from_project
  82. from_project=$(sql_escape "$(get_project)")
  83. local safe_to safe_subject safe_body
  84. safe_to=$(sql_escape "$to_project")
  85. safe_subject=$(sql_escape "$subject")
  86. safe_body=$(sql_escape "$body")
  87. sqlite3 "$MAIL_DB" \
  88. "INSERT INTO messages (from_project, to_project, subject, body) VALUES ('${from_project}', '${safe_to}', '${safe_subject}', '${safe_body}');"
  89. echo "Sent to ${to_project}: ${subject}"
  90. }
  91. # List all messages (read and unread) for current project
  92. list_all() {
  93. init_db
  94. local project
  95. project=$(sql_escape "$(get_project)")
  96. local limit="${1:-20}"
  97. # Validate limit is numeric
  98. if ! [[ "$limit" =~ ^[0-9]+$ ]]; then
  99. limit=20
  100. fi
  101. sqlite3 -header -separator ' | ' "$MAIL_DB" \
  102. "SELECT id, from_project, subject, CASE WHEN read=0 THEN 'UNREAD' ELSE 'read' END as status, timestamp FROM messages WHERE to_project='${project}' ORDER BY timestamp DESC LIMIT ${limit};"
  103. }
  104. # Clear old read messages (default: older than 7 days)
  105. clear_old() {
  106. init_db
  107. local days="${1:-7}"
  108. # Validate days is numeric
  109. if ! [[ "$days" =~ ^[0-9]+$ ]]; then
  110. days=7
  111. fi
  112. local deleted
  113. deleted=$(sqlite3 "$MAIL_DB" \
  114. "DELETE FROM messages WHERE read=1 AND timestamp < datetime('now', '-${days} days'); SELECT changes();")
  115. echo "Cleared ${deleted} read messages older than ${days} days"
  116. }
  117. # List all known projects (that have sent or received mail)
  118. list_projects() {
  119. init_db
  120. sqlite3 "$MAIL_DB" \
  121. "SELECT DISTINCT from_project FROM messages UNION SELECT DISTINCT to_project FROM messages ORDER BY 1;"
  122. }
  123. # Dispatch
  124. case "${1:-help}" in
  125. init) init_db && echo "Mail database initialized at $MAIL_DB" ;;
  126. count) count_unread ;;
  127. unread) list_unread ;;
  128. read) if [ -n "${2:-}" ]; then read_one "$2"; else read_mail; fi ;;
  129. send) send "${2:?to_project required}" "${3:-no subject}" "${4:?body required}" ;;
  130. list) list_all "${2:-20}" ;;
  131. clear) clear_old "${2:-7}" ;;
  132. projects) list_projects ;;
  133. help)
  134. echo "Usage: mail-db.sh <command> [args]"
  135. echo ""
  136. echo "Commands:"
  137. echo " init Initialize database"
  138. echo " count Count unread messages"
  139. echo " unread List unread messages (brief)"
  140. echo " read [id] Read messages and mark as read"
  141. echo " send <to> <subj> <body> Send a message"
  142. echo " list [limit] List recent messages (default 20)"
  143. echo " clear [days] Clear read messages older than N days"
  144. echo " projects List known projects"
  145. ;;
  146. *) echo "Unknown command: $1. Run with 'help' for usage." >&2; exit 1 ;;
  147. esac