mail-db.sh 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. #!/bin/bash
  2. # mail-db.sh - SQLite mail database operations
  3. # Global mail database at ~/.claude/mail.db
  4. # Project identity: 6-char ID derived from git root commit (stable across
  5. # renames, moves, clones) with fallback to canonical path hash for non-git dirs.
  6. set -euo pipefail
  7. MAIL_DB="$HOME/.claude/mail.db"
  8. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  9. # ============================================================================
  10. # Identity - git-rooted project IDs
  11. # ============================================================================
  12. # Get canonical path (resolves symlinks + case on macOS)
  13. canonical_path() {
  14. if [ -d "${1:-$PWD}" ]; then
  15. (cd "${1:-$PWD}" && pwd -P)
  16. else
  17. printf '%s' "${1:-$PWD}"
  18. fi
  19. }
  20. # Generate 6-char project ID
  21. # Priority: git root commit hash > canonical path hash
  22. project_hash() {
  23. local dir="${1:-$PWD}"
  24. # Try git root commit (first commit in repo history)
  25. if [ -d "$dir" ]; then
  26. local root_commit
  27. root_commit=$(git -C "$dir" rev-list --max-parents=0 HEAD 2>/dev/null | head -1)
  28. if [ -n "$root_commit" ]; then
  29. echo "${root_commit:0:6}"
  30. return 0
  31. fi
  32. fi
  33. # Fallback: hash of canonical path
  34. local path
  35. path=$(canonical_path "$dir")
  36. printf '%s' "$path" | shasum -a 256 | cut -c1-6
  37. }
  38. # Get display name (basename of canonical path)
  39. project_name() {
  40. basename "$(canonical_path "${1:-$PWD}")"
  41. }
  42. # ============================================================================
  43. # Database
  44. # ============================================================================
  45. init_db() {
  46. mkdir -p "$(dirname "$MAIL_DB")"
  47. sqlite3 "$MAIL_DB" <<'SQL'
  48. CREATE TABLE IF NOT EXISTS messages (
  49. id INTEGER PRIMARY KEY AUTOINCREMENT,
  50. from_project TEXT NOT NULL,
  51. to_project TEXT NOT NULL,
  52. subject TEXT DEFAULT '',
  53. body TEXT NOT NULL,
  54. timestamp TEXT DEFAULT (datetime('now')),
  55. read INTEGER DEFAULT 0,
  56. priority TEXT DEFAULT 'normal'
  57. );
  58. CREATE INDEX IF NOT EXISTS idx_unread ON messages(to_project, read);
  59. CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp);
  60. CREATE TABLE IF NOT EXISTS projects (
  61. hash TEXT PRIMARY KEY,
  62. name TEXT NOT NULL,
  63. path TEXT NOT NULL,
  64. registered TEXT DEFAULT (datetime('now'))
  65. );
  66. SQL
  67. # Migration: add priority column if missing
  68. sqlite3 "$MAIL_DB" "SELECT priority FROM messages LIMIT 0;" 2>/dev/null || \
  69. sqlite3 "$MAIL_DB" "ALTER TABLE messages ADD COLUMN priority TEXT DEFAULT 'normal';" 2>/dev/null
  70. # Migration: create projects table if missing (for existing installs)
  71. sqlite3 "$MAIL_DB" "SELECT hash FROM projects LIMIT 0;" 2>/dev/null || \
  72. 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
  73. }
  74. sql_escape() {
  75. printf '%s' "$1" | sed "s/'/''/g"
  76. }
  77. # Register current project in the projects table (idempotent)
  78. register_project() {
  79. local hash name path
  80. hash=$(project_hash "${1:-$PWD}")
  81. name=$(sql_escape "$(project_name "${1:-$PWD}")")
  82. path=$(sql_escape "$(canonical_path "${1:-$PWD}")")
  83. sqlite3 "$MAIL_DB" \
  84. "INSERT OR REPLACE INTO projects (hash, name, path) VALUES ('${hash}', '${name}', '${path}');"
  85. }
  86. # Get project ID for current directory
  87. get_project_id() {
  88. project_hash "${1:-$PWD}"
  89. }
  90. # Resolve a user-supplied name/hash to a project hash
  91. # Accepts: hash (6 chars), project name, or path
  92. resolve_target() {
  93. local target="$1"
  94. local safe_target
  95. safe_target=$(sql_escape "$target")
  96. # 1. Exact hash match
  97. if [[ ${#target} -eq 6 ]] && [[ "$target" =~ ^[0-9a-f]+$ ]]; then
  98. local found
  99. found=$(sqlite3 "$MAIL_DB" "SELECT hash FROM projects WHERE hash='${safe_target}';")
  100. if [ -n "$found" ]; then
  101. echo "$found"
  102. return 0
  103. fi
  104. fi
  105. # 2. Name match (case-insensitive)
  106. local by_name
  107. by_name=$(sqlite3 "$MAIL_DB" "SELECT hash FROM projects WHERE LOWER(name)=LOWER('${safe_target}') ORDER BY registered DESC LIMIT 1;")
  108. if [ -n "$by_name" ]; then
  109. echo "$by_name"
  110. return 0
  111. fi
  112. # 3. Path match - target might be a directory
  113. if [ -d "$target" ]; then
  114. local hash
  115. hash=$(project_hash "$target")
  116. echo "$hash"
  117. return 0
  118. fi
  119. # 4. Generate hash from target as a string (for unknown projects)
  120. # Register it so replies work
  121. local hash
  122. hash=$(printf '%s' "$target" | shasum -a 256 | cut -c1-6)
  123. sqlite3 "$MAIL_DB" \
  124. "INSERT OR IGNORE INTO projects (hash, name, path) VALUES ('${hash}', '${safe_target}', '${safe_target}');"
  125. echo "$hash"
  126. }
  127. # Look up display name for a hash
  128. display_name() {
  129. local hash="$1"
  130. local name
  131. name=$(sqlite3 "$MAIL_DB" "SELECT name FROM projects WHERE hash='${hash}';")
  132. if [ -n "$name" ]; then
  133. echo "$name"
  134. else
  135. echo "$hash"
  136. fi
  137. }
  138. # ============================================================================
  139. # Identicon display (inline, compact)
  140. # ============================================================================
  141. show_identicon() {
  142. local target="${1:-$PWD}"
  143. if [ -f "$SCRIPT_DIR/identicon.sh" ]; then
  144. bash "$SCRIPT_DIR/identicon.sh" "$target"
  145. fi
  146. }
  147. # ============================================================================
  148. # Mail operations
  149. # ============================================================================
  150. count_unread() {
  151. init_db
  152. register_project
  153. local pid
  154. pid=$(get_project_id)
  155. sqlite3 "$MAIL_DB" "SELECT COUNT(*) FROM messages WHERE to_project='${pid}' AND read=0;"
  156. }
  157. list_unread() {
  158. init_db
  159. register_project
  160. local pid
  161. pid=$(get_project_id)
  162. local rows
  163. rows=$(sqlite3 -separator '|' "$MAIL_DB" \
  164. "SELECT id, from_project, subject, timestamp FROM messages WHERE to_project='${pid}' AND read=0 ORDER BY timestamp DESC;")
  165. [ -z "$rows" ] && return 0
  166. while IFS='|' read -r id from_hash subj ts; do
  167. local from_name
  168. from_name=$(display_name "$from_hash")
  169. echo "${id} | ${from_name} (${from_hash}) | ${subj} | ${ts}"
  170. done <<< "$rows"
  171. }
  172. read_mail() {
  173. init_db
  174. register_project
  175. local pid
  176. pid=$(get_project_id)
  177. local rows
  178. rows=$(sqlite3 -separator '|' "$MAIL_DB" \
  179. "SELECT id, from_project, subject, body, timestamp FROM messages WHERE to_project='${pid}' AND read=0 ORDER BY timestamp ASC;")
  180. if [ -z "$rows" ]; then
  181. return 0
  182. fi
  183. echo "id | from_project | subject | body | timestamp"
  184. while IFS='|' read -r id from_hash subj body ts; do
  185. local from_name
  186. from_name=$(display_name "$from_hash")
  187. echo "${id} | ${from_name} (${from_hash}) | ${subj} | ${body} | ${ts}"
  188. done <<< "$rows"
  189. sqlite3 "$MAIL_DB" \
  190. "UPDATE messages SET read=1 WHERE to_project='${pid}' AND read=0;"
  191. }
  192. read_one() {
  193. local msg_id="$1"
  194. if ! [[ "$msg_id" =~ ^[0-9]+$ ]]; then
  195. echo "Error: message ID must be numeric" >&2
  196. return 1
  197. fi
  198. init_db
  199. local row
  200. row=$(sqlite3 -separator '|' "$MAIL_DB" \
  201. "SELECT id, from_project, to_project, subject, body, timestamp FROM messages WHERE id=${msg_id};")
  202. if [ -n "$row" ]; then
  203. echo "id | from_project | to_project | subject | body | timestamp"
  204. local id from_hash to_hash subj body ts
  205. IFS='|' read -r id from_hash to_hash subj body ts <<< "$row"
  206. local from_name to_name
  207. from_name=$(display_name "$from_hash")
  208. to_name=$(display_name "$to_hash")
  209. echo "${id} | ${from_name} (${from_hash}) | ${to_name} (${to_hash}) | ${subj} | ${body} | ${ts}"
  210. fi
  211. sqlite3 "$MAIL_DB" \
  212. "UPDATE messages SET read=1 WHERE id=${msg_id};"
  213. }
  214. send() {
  215. local priority="normal"
  216. if [ "${1:-}" = "--urgent" ]; then
  217. priority="urgent"
  218. shift
  219. fi
  220. local to_input="${1:?to_project required}"
  221. local subject="${2:-no subject}"
  222. local body="${3:?body required}"
  223. if [ -z "$body" ]; then
  224. echo "Error: message body cannot be empty" >&2
  225. return 1
  226. fi
  227. init_db
  228. register_project
  229. local from_id to_id
  230. from_id=$(get_project_id)
  231. to_id=$(resolve_target "$to_input")
  232. local safe_subject safe_body
  233. safe_subject=$(sql_escape "$subject")
  234. safe_body=$(sql_escape "$body")
  235. sqlite3 "$MAIL_DB" \
  236. "INSERT INTO messages (from_project, to_project, subject, body, priority) VALUES ('${from_id}', '${to_id}', '${safe_subject}', '${safe_body}', '${priority}');"
  237. local to_name
  238. to_name=$(display_name "$to_id")
  239. echo "Sent to ${to_name} (${to_id}): ${subject}$([ "$priority" = "urgent" ] && echo " [URGENT]" || true)"
  240. }
  241. search() {
  242. local keyword="$1"
  243. if [ -z "$keyword" ]; then
  244. echo "Error: search keyword required" >&2
  245. return 1
  246. fi
  247. init_db
  248. register_project
  249. local pid
  250. pid=$(get_project_id)
  251. local safe_keyword
  252. safe_keyword=$(sql_escape "$keyword")
  253. local rows
  254. rows=$(sqlite3 -separator '|' "$MAIL_DB" \
  255. "SELECT id, from_project, subject, CASE WHEN read=0 THEN 'UNREAD' ELSE 'read' END, timestamp FROM messages WHERE to_project='${pid}' AND (subject LIKE '%${safe_keyword}%' OR body LIKE '%${safe_keyword}%') ORDER BY timestamp DESC LIMIT 20;")
  256. [ -z "$rows" ] && return 0
  257. echo "id | from | subject | status | timestamp"
  258. while IFS='|' read -r id from_hash subj status ts; do
  259. local from_name
  260. from_name=$(display_name "$from_hash")
  261. echo "${id} | ${from_name} (${from_hash}) | ${subj} | ${status} | ${ts}"
  262. done <<< "$rows"
  263. }
  264. list_all() {
  265. init_db
  266. register_project
  267. local pid
  268. pid=$(get_project_id)
  269. local limit="${1:-20}"
  270. if ! [[ "$limit" =~ ^[0-9]+$ ]]; then
  271. limit=20
  272. fi
  273. local rows
  274. rows=$(sqlite3 -separator '|' "$MAIL_DB" \
  275. "SELECT id, from_project, subject, CASE WHEN read=0 THEN 'UNREAD' ELSE 'read' END, timestamp FROM messages WHERE to_project='${pid}' ORDER BY timestamp DESC LIMIT ${limit};")
  276. [ -z "$rows" ] && return 0
  277. echo "id | from | subject | status | timestamp"
  278. while IFS='|' read -r id from_hash subj status ts; do
  279. local from_name
  280. from_name=$(display_name "$from_hash")
  281. echo "${id} | ${from_name} (${from_hash}) | ${subj} | ${status} | ${ts}"
  282. done <<< "$rows"
  283. }
  284. clear_old() {
  285. init_db
  286. local days="${1:-7}"
  287. if ! [[ "$days" =~ ^[0-9]+$ ]]; then
  288. days=7
  289. fi
  290. local deleted
  291. deleted=$(sqlite3 "$MAIL_DB" \
  292. "DELETE FROM messages WHERE read=1 AND timestamp < datetime('now', '-${days} days'); SELECT changes();")
  293. echo "Cleared ${deleted} read messages older than ${days} days"
  294. }
  295. reply() {
  296. local msg_id="$1"
  297. local body="$2"
  298. if ! [[ "$msg_id" =~ ^[0-9]+$ ]]; then
  299. echo "Error: message ID must be numeric" >&2
  300. return 1
  301. fi
  302. if [ -z "$body" ]; then
  303. echo "Error: reply body cannot be empty" >&2
  304. return 1
  305. fi
  306. init_db
  307. register_project
  308. local orig
  309. orig=$(sqlite3 -separator '|' "$MAIL_DB" "SELECT from_project, subject FROM messages WHERE id=${msg_id};")
  310. if [ -z "$orig" ]; then
  311. echo "Error: message #${msg_id} not found" >&2
  312. return 1
  313. fi
  314. local orig_from_hash orig_subject
  315. orig_from_hash=$(echo "$orig" | cut -d'|' -f1)
  316. orig_subject=$(echo "$orig" | cut -d'|' -f2)
  317. local from_id
  318. from_id=$(get_project_id)
  319. local safe_subject safe_body
  320. safe_subject=$(sql_escape "Re: ${orig_subject}")
  321. safe_body=$(sql_escape "$body")
  322. sqlite3 "$MAIL_DB" \
  323. "INSERT INTO messages (from_project, to_project, subject, body) VALUES ('${from_id}', '${orig_from_hash}', '${safe_subject}', '${safe_body}');"
  324. local orig_name
  325. orig_name=$(display_name "$orig_from_hash")
  326. echo "Replied to ${orig_name} (${orig_from_hash}): Re: ${orig_subject}"
  327. }
  328. broadcast() {
  329. local subject="$1"
  330. local body="$2"
  331. if [ -z "$body" ]; then
  332. echo "Error: message body cannot be empty" >&2
  333. return 1
  334. fi
  335. init_db
  336. register_project
  337. local from_id
  338. from_id=$(get_project_id)
  339. local targets
  340. targets=$(sqlite3 "$MAIL_DB" \
  341. "SELECT hash FROM projects WHERE hash != '${from_id}' ORDER BY name;")
  342. local count=0
  343. local safe_subject safe_body
  344. safe_subject=$(sql_escape "$subject")
  345. safe_body=$(sql_escape "$body")
  346. while IFS= read -r target_hash; do
  347. [ -z "$target_hash" ] && continue
  348. sqlite3 "$MAIL_DB" \
  349. "INSERT INTO messages (from_project, to_project, subject, body) VALUES ('${from_id}', '${target_hash}', '${safe_subject}', '${safe_body}');"
  350. count=$((count + 1))
  351. done <<< "$targets"
  352. echo "Broadcast to ${count} project(s): ${subject}"
  353. }
  354. status() {
  355. init_db
  356. register_project
  357. local pid
  358. pid=$(get_project_id)
  359. local unread total
  360. unread=$(sqlite3 "$MAIL_DB" "SELECT COUNT(*) FROM messages WHERE to_project='${pid}' AND read=0;")
  361. total=$(sqlite3 "$MAIL_DB" "SELECT COUNT(*) FROM messages WHERE to_project='${pid}';")
  362. echo "Inbox: ${unread} unread / ${total} total"
  363. if [ "${unread:-0}" -gt 0 ]; then
  364. local senders
  365. senders=$(sqlite3 -separator '|' "$MAIL_DB" \
  366. "SELECT from_project, COUNT(*) FROM messages WHERE to_project='${pid}' AND read=0 GROUP BY from_project ORDER BY COUNT(*) DESC;")
  367. while IFS='|' read -r from_hash cnt; do
  368. local from_name
  369. from_name=$(display_name "$from_hash")
  370. echo " ${from_name} (${from_hash}): ${cnt} message(s)"
  371. done <<< "$senders"
  372. fi
  373. }
  374. purge() {
  375. init_db
  376. if [ "${1:-}" = "--all" ]; then
  377. local count
  378. count=$(sqlite3 "$MAIL_DB" "DELETE FROM messages; SELECT changes();")
  379. echo "Purged all ${count} message(s) from database"
  380. else
  381. register_project
  382. local pid
  383. pid=$(get_project_id)
  384. local count
  385. count=$(sqlite3 "$MAIL_DB" \
  386. "DELETE FROM messages WHERE to_project='${pid}' OR from_project='${pid}'; SELECT changes();")
  387. local name
  388. name=$(project_name)
  389. echo "Purged ${count} message(s) for ${name} (${pid})"
  390. fi
  391. }
  392. alias_project() {
  393. local old_name="$1"
  394. local new_name="$2"
  395. if [ -z "$old_name" ] || [ -z "$new_name" ]; then
  396. echo "Error: both old and new project names required" >&2
  397. return 1
  398. fi
  399. init_db
  400. # Resolve old name to hash, then update the display name
  401. local old_hash
  402. old_hash=$(resolve_target "$old_name")
  403. local safe_new
  404. safe_new=$(sql_escape "$new_name")
  405. local safe_old
  406. safe_old=$(sql_escape "$old_name")
  407. sqlite3 "$MAIL_DB" \
  408. "UPDATE projects SET name='${safe_new}' WHERE hash='${old_hash}';"
  409. # Also update path if it matches the old name (phantom projects)
  410. sqlite3 "$MAIL_DB" \
  411. "UPDATE projects SET path='${safe_new}' WHERE hash='${old_hash}' AND path='${safe_old}';"
  412. echo "Renamed '${old_name}' -> '${new_name}' (hash: ${old_hash})"
  413. }
  414. list_projects() {
  415. init_db
  416. register_project
  417. local rows
  418. rows=$(sqlite3 -separator '|' "$MAIL_DB" \
  419. "SELECT hash, name, path FROM projects ORDER BY name;")
  420. [ -z "$rows" ] && echo "No known projects" && return 0
  421. local my_id
  422. my_id=$(get_project_id)
  423. while IFS='|' read -r hash name path; do
  424. local marker=""
  425. [ "$hash" = "$my_id" ] && marker=" (you)"
  426. echo ""
  427. # Show identicon if available
  428. if [ -f "$SCRIPT_DIR/identicon.sh" ]; then
  429. bash "$SCRIPT_DIR/identicon.sh" "$path" --compact 2>/dev/null || true
  430. fi
  431. echo "${name} ${hash}${marker}"
  432. echo "${path}"
  433. done <<< "$rows"
  434. }
  435. # Migrate old basename-style messages to hash IDs
  436. migrate() {
  437. init_db
  438. register_project
  439. echo "Migrating old messages to hash-based IDs..."
  440. # Find all unique project names in messages that aren't 6-char hex hashes
  441. local old_names
  442. old_names=$(sqlite3 "$MAIL_DB" \
  443. "SELECT DISTINCT from_project FROM messages WHERE LENGTH(from_project) != 6 OR from_project GLOB '*[^0-9a-f]*' UNION SELECT DISTINCT to_project FROM messages WHERE LENGTH(to_project) != 6 OR to_project GLOB '*[^0-9a-f]*';")
  444. if [ -z "$old_names" ]; then
  445. echo "No messages need migration."
  446. return 0
  447. fi
  448. local count=0
  449. while IFS= read -r old_name; do
  450. [ -z "$old_name" ] && continue
  451. # Try to find the project path - check common locations
  452. local found_path=""
  453. for base_dir in "$HOME/projects" "$HOME/Projects" "$HOME/code" "$HOME/Code" "$HOME/dev" "$HOME/repos"; do
  454. if [ -d "${base_dir}/${old_name}" ]; then
  455. found_path=$(cd "${base_dir}/${old_name}" && pwd -P)
  456. break
  457. fi
  458. done
  459. local new_hash
  460. if [ -n "$found_path" ]; then
  461. new_hash=$(printf '%s' "$found_path" | shasum -a 256 | cut -c1-6)
  462. local safe_name safe_path
  463. safe_name=$(sql_escape "$old_name")
  464. safe_path=$(sql_escape "$found_path")
  465. sqlite3 "$MAIL_DB" \
  466. "INSERT OR IGNORE INTO projects (hash, name, path) VALUES ('${new_hash}', '${safe_name}', '${safe_path}');"
  467. else
  468. # Can't find directory - hash the name itself
  469. new_hash=$(printf '%s' "$old_name" | shasum -a 256 | cut -c1-6)
  470. local safe_name
  471. safe_name=$(sql_escape "$old_name")
  472. sqlite3 "$MAIL_DB" \
  473. "INSERT OR IGNORE INTO projects (hash, name, path) VALUES ('${new_hash}', '${safe_name}', '${safe_name}');"
  474. fi
  475. local safe_old
  476. safe_old=$(sql_escape "$old_name")
  477. sqlite3 "$MAIL_DB" "UPDATE messages SET from_project='${new_hash}' WHERE from_project='${safe_old}';"
  478. sqlite3 "$MAIL_DB" "UPDATE messages SET to_project='${new_hash}' WHERE to_project='${safe_old}';"
  479. echo " ${old_name} -> ${new_hash}$([ -n "$found_path" ] && echo " (${found_path})" || echo " (name only)")"
  480. count=$((count + 1))
  481. done <<< "$old_names"
  482. echo "Migrated ${count} project name(s)."
  483. }
  484. # ============================================================================
  485. # Dispatch
  486. # ============================================================================
  487. case "${1:-help}" in
  488. init) init_db && echo "Mail database initialized at $MAIL_DB" ;;
  489. count) count_unread ;;
  490. unread) list_unread ;;
  491. read) if [ -n "${2:-}" ]; then read_one "$2"; else read_mail; fi ;;
  492. send) shift; send "$@" ;;
  493. reply) reply "${2:?message_id required}" "${3:?body required}" ;;
  494. list) list_all "${2:-20}" ;;
  495. clear) clear_old "${2:-7}" ;;
  496. broadcast) broadcast "${2:-no subject}" "${3:?body required}" ;;
  497. search) search "${2:?keyword required}" ;;
  498. status) status ;;
  499. purge) purge "${2:-}" ;;
  500. alias) alias_project "${2:?old name required}" "${3:?new name required}" ;;
  501. projects) list_projects ;;
  502. migrate) migrate ;;
  503. id) init_db; register_project; echo "$(project_name) $(get_project_id)" ;;
  504. help)
  505. echo "Usage: mail-db.sh <command> [args]"
  506. echo ""
  507. echo "Commands:"
  508. echo " init Initialize database"
  509. echo " id Show this project's name and hash"
  510. echo " count Count unread messages"
  511. echo " unread List unread messages (brief)"
  512. echo " read [id] Read messages and mark as read"
  513. echo " send [--urgent] <to> <subj> <body>"
  514. echo " Send a message (to = name, hash, or path)"
  515. echo " reply <id> <body> Reply to a message"
  516. echo " list [limit] List recent messages (default 20)"
  517. echo " clear [days] Clear read messages older than N days"
  518. echo " broadcast <subj> <body> Send to all known projects"
  519. echo " search <keyword> Search messages by keyword"
  520. echo " status Inbox summary"
  521. echo " purge [--all] Delete all messages for this project"
  522. echo " alias <old> <new> Rename project display name"
  523. echo " projects List known projects with identicons"
  524. echo " migrate Convert old basename messages to hash IDs"
  525. ;;
  526. *) echo "Unknown command: $1. Run with 'help' for usage." >&2; exit 1 ;;
  527. esac