Refactors the contacts system from being embedded in cal_tool.py into a standalone contacts skill that serves as the single source of truth for recipient validation across all outbound email paths. - New skills/contacts/ skill: list, add, delete, resolve commands - New skills/himalaya/scripts/himalaya.sh wrapper: validates To/Cc/Bcc recipients against contacts for message send, template send, and message write commands; passes everything else through unchanged - cal_tool.py now delegates to contacts.py resolve instead of inline logic - TOOLS.md updated: agent should use himalaya wrapper, not raw himalaya
196 lines
5.7 KiB
Bash
Executable File
196 lines
5.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# himalaya wrapper — validates outbound email recipients against the contacts list.
|
|
#
|
|
# Drop-in replacement for himalaya. All commands pass through unchanged except
|
|
# those that send email, which first validate To/Cc/Bcc recipients.
|
|
#
|
|
# Gated commands:
|
|
# message send — parses MIME headers from stdin
|
|
# template send — parses MML headers from stdin
|
|
# message write — parses -H header flags from args
|
|
#
|
|
# All other commands (envelope list, message read, message delete, folder,
|
|
# flag, attachment, account, etc.) pass through directly.
|
|
#
|
|
# Usage: use this script wherever you would use `himalaya`.
|
|
|
|
set -euo pipefail
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Config
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Find the real himalaya binary (skip this script if it's in PATH)
|
|
SCRIPT_PATH="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
|
|
HIMALAYA=""
|
|
while IFS= read -r candidate; do
|
|
resolved="$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")"
|
|
if [[ "$resolved" != "$SCRIPT_PATH" ]]; then
|
|
HIMALAYA="$candidate"
|
|
break
|
|
fi
|
|
done < <(which -a himalaya 2>/dev/null || true)
|
|
|
|
if [[ -z "$HIMALAYA" ]]; then
|
|
# Fallback: check common locations
|
|
for path in "$HOME/.local/bin/himalaya" /usr/local/bin/himalaya /usr/bin/himalaya; do
|
|
if [[ -x "$path" ]]; then
|
|
HIMALAYA="$path"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [[ -z "$HIMALAYA" ]]; then
|
|
echo "Error: himalaya binary not found" >&2
|
|
exit 1
|
|
fi
|
|
|
|
CONTACTS="$(cd "$(dirname "$0")/../../contacts/scripts" && pwd)/contacts.py"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
validate_address() {
|
|
local addr="$1"
|
|
# Skip empty addresses
|
|
[[ -z "$addr" ]] && return 0
|
|
# Validate against contacts
|
|
python3 "$CONTACTS" resolve "$addr" > /dev/null 2>&1
|
|
return $?
|
|
}
|
|
|
|
# Extract email address from "Display Name <email>" or bare "email" format
|
|
extract_email() {
|
|
local raw="$1"
|
|
raw="$(echo "$raw" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
if [[ "$raw" == *"<"*">"* ]]; then
|
|
echo "$raw" | sed 's/.*<//;s/>.*//'
|
|
else
|
|
echo "$raw"
|
|
fi
|
|
}
|
|
|
|
# Validate a comma-separated list of addresses. Prints errors to stderr.
|
|
# Returns 0 if all valid, 1 if any invalid.
|
|
validate_address_list() {
|
|
local header_value="$1"
|
|
local all_valid=0
|
|
|
|
# Split on commas
|
|
while IFS= read -r addr; do
|
|
addr="$(extract_email "$addr")"
|
|
[[ -z "$addr" ]] && continue
|
|
if ! validate_address "$addr"; then
|
|
all_valid=1
|
|
fi
|
|
done < <(echo "$header_value" | tr ',' '\n')
|
|
|
|
return $all_valid
|
|
}
|
|
|
|
# Parse To/Cc/Bcc from MIME/MML headers in a file.
|
|
# Headers end at the first blank line.
|
|
validate_stdin_headers() {
|
|
local tmpfile="$1"
|
|
local failed=0
|
|
|
|
# Extract header block (everything before first blank line)
|
|
while IFS= read -r line; do
|
|
# Stop at blank line (end of headers)
|
|
[[ -z "$line" ]] && break
|
|
|
|
# Match To:, Cc:, Bcc: headers (case-insensitive)
|
|
if echo "$line" | grep -iqE '^(to|cc|bcc):'; then
|
|
local value
|
|
value="$(echo "$line" | sed 's/^[^:]*:[[:space:]]*//')"
|
|
if ! validate_address_list "$value"; then
|
|
failed=1
|
|
fi
|
|
fi
|
|
done < "$tmpfile"
|
|
|
|
return $failed
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Detect sending commands
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Collect all args into a string for pattern matching
|
|
ALL_ARGS="$*"
|
|
|
|
# Check if this is a sending command
|
|
is_stdin_send=false
|
|
is_write_send=false
|
|
|
|
# "message send" or "template send" — reads from stdin
|
|
if echo "$ALL_ARGS" | grep -qE '(message|template)[[:space:]]+send'; then
|
|
is_stdin_send=true
|
|
fi
|
|
|
|
# "message write" — may have -H flags with recipients
|
|
if echo "$ALL_ARGS" | grep -qE 'message[[:space:]]+write'; then
|
|
is_write_send=true
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Handle stdin-based sends (message send, template send)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if $is_stdin_send; then
|
|
# Read stdin into temp file
|
|
tmpfile="$(mktemp)"
|
|
trap 'rm -f "$tmpfile"' EXIT
|
|
cat > "$tmpfile"
|
|
|
|
# Validate recipients from headers
|
|
if ! validate_stdin_headers "$tmpfile"; then
|
|
exit 1
|
|
fi
|
|
|
|
# Pass through to real himalaya
|
|
cat "$tmpfile" | exec "$HIMALAYA" "$@"
|
|
exit $?
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Handle message write with -H flags
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if $is_write_send; then
|
|
# Parse -H flags for To/Cc/Bcc without consuming args
|
|
failed=0
|
|
original_args=("$@")
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-H)
|
|
shift
|
|
if [[ $# -gt 0 ]]; then
|
|
header="$1"
|
|
if echo "$header" | grep -iqE '^(to|cc|bcc):'; then
|
|
value="$(echo "$header" | sed 's/^[^:]*:[[:space:]]*//')"
|
|
if ! validate_address_list "$value"; then
|
|
failed=1
|
|
fi
|
|
fi
|
|
fi
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [[ $failed -ne 0 ]]; then
|
|
exit 1
|
|
fi
|
|
|
|
exec "$HIMALAYA" "${original_args[@]}"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pass through everything else
|
|
# ---------------------------------------------------------------------------
|
|
|
|
exec "$HIMALAYA" "$@"
|