#!/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")/../skills/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 " or bare "email" format extract_email() { local raw="$1" raw="$(echo "$raw" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" if [[ "$raw" == *"<"*">"* ]]; then echo "$raw" | sed '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" "$@"