#!/usr/bin/env bash # Instant email reply watcher: polls all 4 Gmail accounts for new human # correspondence and wakes a worker to reply instantly. Stays silent during # quiet hours 10pm-6am ET WITHOUT marking seen, so the morning poll catches # overnight mail (same pattern as prospect-reply-watch). set -euo pipefail source "$HATCH_HOOK_RUNTIME" STATE_DIR="$HOME/hooks/state/instant-email-reply" SEEN="$STATE_DIR/seen_ids.txt" 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 mail and replies then. HOUR=$(TZ=America/New_York date +%H) if [ "$HOUR" -ge 22 ] || [ "$HOUR" -lt 6 ]; then silent "quiet hours" "{\"hour\":\"$HOUR\"}" fi ACCOUNTS="56e1ab1e01e34f2f95dafb39c3d31fa1 2ad7fb871f144ca5a6e3315f0eb94301 b8a718a76fb742818f88e80686c8cfd1 d48c9e2643f643aeb3c4dacf347245b8" # Sean's own addresses — never treat as inbound. OWN='sean@seanmattsson.com|smattsson@stratixsystems.com|seanmattsson@gmail.com|seanmichael.mattsson@gmail.com|sussanandsean@gmail.com' FOUND=0 for ACCT in $ACCOUNTS; do RAW=$(hatch_gws_cli gmail --account "$ACCT" +triage \ --query 'newer_than:1d -category:promotions -category:social' \ --max 50 --format json 2>/dev/null) || { log "gmail fetch failed" "{\"account\":\"$ACCT\"}" continue } while IFS=$'\t' read -r ID FROM SUBJ DATE; do [ -z "${ID:-}" ] && continue SENDER=$(printf '%s' "$FROM" | grep -oiE '[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:]') # Drop Sean's own addresses. if printf '%s' "$SENDER_LC" | grep -qiE "^(${OWN})$"; then continue; fi # Drop automated senders. if printf '%s' "$SENDER_LC" | grep -qiE 'noreply|donotreply|no-reply|newsletter|notification|alerts@|billing@|support\.|info@'; then continue; fi # Drop already-seen IDs. if grep -qxF "$ID" "$SEEN" 2>/dev/null; then continue; fi printf '%s\t%s\t%s\t%s\t%s\n' "$ACCT" "$ID" "$FROM" "$SUBJ" "$DATE" >> "$PENDING" if [ "$DRY" = "0" ]; then printf '%s\n' "$ID" >> "$SEEN" fi FOUND=$((FOUND + 1)) done < <(printf '%s' "$RAW" | jq -r '.messages[]? | [.id, .from, .subject, .date] | @tsv') done # Cap the seen-IDs file at ~500 lines, trimming oldest. if [ "$DRY" = "0" ] && [ "$(wc -l < "$SEEN")" -gt 500 ]; then tail -n 500 "$SEEN" > "$SEEN.tmp" && mv "$SEEN.tmp" "$SEEN" fi if [ "$FOUND" = "0" ]; then silent "no new human mail" '{}' fi PAYLOAD=$(jq -R -s ' split("\n") | map(select(length > 0) | split("\t")) | group_by(.[0]) | map({account_id: .[0][0], message_ids: map(.[1])}) | {groups: .} ' "$PENDING") log "new human mail" "$PAYLOAD" wake "new email needs instant reply" "$PAYLOAD"