#!/usr/bin/env bash # Prospect reply watcher: polls sean@seanmattsson.com for inbound mail that may be # replies to the prospecting engine's sent campaigns. Wakes a worker only when a # candidate appears; stays silent otherwise (and during quiet hours 10pm-6am ET). set -euo pipefail source "$HATCH_HOOK_RUNTIME" STATE_DIR="$HOME/hooks/state/prospect-reply-watch" SEEN="$STATE_DIR/seen_ids" PENDING="$STATE_DIR/pending.tsv" mkdir -p "$STATE_DIR" touch "$SEEN" : > "$PENDING" DRY=0 if [ "${HATCH_HOOK_DRY_RUN:-0}" = "1" ]; then DRY=1 fi # Quiet hours: 10pm-6am ET. Stay silent WITHOUT marking seen, so the morning # poll catches overnight replies and reports them after quiet hours. HOUR=$(TZ=America/New_York date +%H) if [ "$HOUR" -ge 22 ] || [ "$HOUR" -lt 6 ]; then silent "quiet hours" "{\"hour\":\"$HOUR\"}" fi ACCT="d48c9e2643f643aeb3c4dacf347245b8" SENTLOG="$HOME/workspace/goals/sales-weapon-ae-copilot/hidden_files/prospect-queues/sent-log.json" RAW=$(hatch_gws_cli gmail --account "$ACCT" +triage \ --query 'newer_than:1d -from:sean@seanmattsson.com -from:smattsson@stratixsystems.com' \ --max 30 --format json 2>/dev/null) || { log "gmail fetch failed" '{}' silent "fetch failed" '{}' } # Booking notifications come FROM Sean's own address (Google Calendar appointment # scheduling), so the main query excludes them. Fetch them separately. BOOKING=$(hatch_gws_cli gmail --account "$ACCT" +triage \ --query 'subject:"Appointment booked" newer_than:1d' \ --max 10 --format json 2>/dev/null) || BOOKING='{"messages":[]}' ALL_MSGS=$(printf '%s\n%s' "$RAW" "$BOOKING" | jq -s '[.[].messages[]?] | unique_by(.id)') mapfile -t WATCH_EMAILS < <(jq -r '.[].email' "$SENTLOG" 2>/dev/null | sort -u) mapfile -t WATCH_DOMAINS < <(jq -r '.[].email | split("@")[1]' "$SENTLOG" 2>/dev/null | sort -u) FOUND=0 while IFS=$'\t' read -r ID FROM SUBJ DATE; do [ -z "${ID:-}" ] && continue SENDER=$(printf '%s' "$FROM" | grep -oE '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' | tail -n 1 || true) [ -z "${SENDER:-}" ] && continue SENDER_LC=$(printf '%s' "$SENDER" | tr '[:upper:]' '[:lower:]') DOMAIN=${SENDER_LC#*@} MATCH=0 for e in ${WATCH_EMAILS[@]+"${WATCH_EMAILS[@]}"}; do EL=$(printf '%s' "$e" | tr '[:upper:]' '[:lower:]') if [ "$SENDER_LC" = "$EL" ]; then MATCH=1; break; fi done if [ "$MATCH" = "0" ]; then for d in ${WATCH_DOMAINS[@]+"${WATCH_DOMAINS[@]}"}; do if [ "$DOMAIN" = "$d" ]; then MATCH=1; break; fi done fi # Delivery bounces are always candidates: the worker identifies the failed recipient. # Google Calendar booking notifications ("Appointment booked") are always # candidates: the worker logs the meeting and sends the confirmation + form. case "$SENDER_LC" in *mailer-daemon*|*postmaster*) MATCH=1 ;; esac case "$SUBJ" in *"Appointment booked"*) MATCH=1 ;; esac [ "$MATCH" = "0" ] && continue; if grep -qxF "$ID" "$SEEN" 2>/dev/null; then continue; fi printf '%s\t%s\t%s\t%s\n' "$ID" "$FROM" "$SUBJ" "$DATE" >> "$PENDING" if [ "$DRY" = "0" ]; then printf '%s\n' "$ID" >> "$SEEN" fi FOUND=$((FOUND + 1)) done < <(printf '%s' "$ALL_MSGS" | jq -r '.[]? | [.id, .from, .subject, .date] | @tsv') if [ "$FOUND" -eq 0 ]; then silent "no campaign replies" '{}' fi PAYLOAD=$(jq -R -s ' split("\n") | map(select(length > 0) | split("\t") | {id: .[0], from: .[1], subject: .[2], date: .[3]}) | {candidates: .} ' "$PENDING") log "campaign reply candidates" "$PAYLOAD" wake "campaign reply detected" "$PAYLOAD"