← back to Cncp Domains

scripts/expiring-soon.sh

185 lines

#!/usr/bin/env bash
# expiring-soon.sh — alert Steve about CNCP domains expiring within 14 days.
#   - reads ~/cncp-starter/cncp-config.json
#   - filters domains with `expires` in the next 14 days from today
#   - posts an HTML email via George Gmail bridge (localhost:9850)
#   - on George failure, writes a markdown report to logs/ and exits 1
#   - silent exit 0 when nothing is expiring

set -uo pipefail

CONFIG="${HOME}/cncp-starter/cncp-config.json"
PROJECT_DIR="${HOME}/Projects/cncp-domains"
LOG_DIR="${PROJECT_DIR}/logs"
GEORGE_URL="http://localhost:9850/api/send"
GEORGE_AUTH="${GEORGE_AUTH:?GEORGE_AUTH must be exported as admin:password (no longer hardcoded)}"
RECIPIENT="steveabramsdesigns@gmail.com"
WINDOW_DAYS=14
URGENT_DAYS=7
DRY_RUN="${DRY_RUN:-0}"

mkdir -p "${LOG_DIR}"

if [[ ! -f "${CONFIG}" ]]; then
  echo "[expiring-soon] config not found: ${CONFIG}" >&2
  exit 1
fi

if ! command -v jq >/dev/null 2>&1; then
  echo "[expiring-soon] jq is required" >&2
  exit 1
fi

TODAY="$(date -I)"
TODAY_EPOCH="$(date -j -f '%Y-%m-%d' "${TODAY}" +%s 2>/dev/null || date -d "${TODAY}" +%s)"

# Build a JSON array of expiring domains, each with a `daysLeft` field.
EXPIRING_JSON="$(
  jq --arg today "${TODAY}" --argjson window "${WINDOW_DAYS}" '
    def dayDiff(d):
      ((d | strptime("%Y-%m-%d") | mktime) - ($today | strptime("%Y-%m-%d") | mktime)) / 86400 | floor;
    [
      .domains[]
      | select(.expires != null and .expires != "")
      | . + {daysLeft: dayDiff(.expires)}
      | select(.daysLeft >= 0 and .daysLeft <= $window)
    ]
    | sort_by(.daysLeft)
  ' "${CONFIG}"
)"

COUNT="$(echo "${EXPIRING_JSON}" | jq 'length')"

if [[ "${COUNT}" -eq 0 ]]; then
  # Nothing to alert about — silent exit per spec.
  exit 0
fi

# ---------- build HTML body ----------
HTML_TABLE="$(
  echo "${EXPIRING_JSON}" | jq -r --argjson urgent "${URGENT_DAYS}" '
    def autorenew(n):
      if n == null then "unknown"
      elif (n | test("(?i)Auto-renew:\\s*ON")) then "ON"
      elif (n | test("(?i)Auto-renew:\\s*OFF")) then "OFF"
      else "unknown"
      end;
    def keepish(s): (s // "" | ascii_downcase) | startswith("keep");
    def warnRow(d):
      d.daysLeft < $urgent
      and autorenew(d.note) == "OFF"
      and keepish(d.suggestion);
    map(
      "<tr style=\"background:" +
      (if warnRow(.) then "#fee2e2" else (if (.daysLeft|tonumber) < 7 then "#fef3c7" else "#ffffff" end) end) +
      ";\">" +
      "<td style=\"padding:6px 10px;border:1px solid #e5e7eb;text-align:right;\">" +
        (if warnRow(.) then "&#9888; " else "" end) + (.daysLeft|tostring) +
      "</td>" +
      "<td style=\"padding:6px 10px;border:1px solid #e5e7eb;\"><b>" + (.name // "?") + "</b></td>" +
      "<td style=\"padding:6px 10px;border:1px solid #e5e7eb;\">" + (.reg // "?") + "</td>" +
      "<td style=\"padding:6px 10px;border:1px solid #e5e7eb;\">" + autorenew(.note) + "</td>" +
      "<td style=\"padding:6px 10px;border:1px solid #e5e7eb;\">" + (.suggestion // "-") + "</td>" +
      "<td style=\"padding:6px 10px;border:1px solid #e5e7eb;color:#555;\">" + (.expires // "") + "</td>" +
      "</tr>"
    ) | join("\n")
  '
)"

KEEP_COUNT="$(echo "${EXPIRING_JSON}" | jq '[.[] | select((.suggestion // "" | ascii_downcase) | startswith("keep"))] | length')"
TOSS_COUNT="$(echo "${EXPIRING_JSON}" | jq '[.[] | select((.suggestion // "" | ascii_downcase) | startswith("toss"))] | length')"
URGENT_COUNT="$(echo "${EXPIRING_JSON}" | jq --argjson u "${URGENT_DAYS}" '[.[] | select(.daysLeft < $u)] | length')"

REGISTRAR_BREAKDOWN="$(
  echo "${EXPIRING_JSON}" | jq -r '
    group_by(.reg // "Unknown")
    | map("<li><b>" + (.[0].reg // "Unknown") + ":</b> " + (length|tostring) + "</li>")
    | join("")
  '
)"

HTML_BODY="$(cat <<HTMLEOF
<html><body style="font-family:-apple-system,Segoe UI,Roboto,sans-serif;color:#111;">
<h2 style="margin:0 0 8px;">CNCP domains expiring in the next ${WINDOW_DAYS} days</h2>
<p style="margin:0 0 12px;color:#555;">As of ${TODAY} &middot; <b>${COUNT}</b> expiring &middot; <b>${KEEP_COUNT}</b> Keep &middot; <b>${TOSS_COUNT}</b> Toss &middot; <b>${URGENT_COUNT}</b> under ${URGENT_DAYS} days</p>
<h3 style="margin:14px 0 6px;">By registrar</h3>
<ul style="margin:0 0 12px 18px;">${REGISTRAR_BREAKDOWN}</ul>
<table style="border-collapse:collapse;border:1px solid #e5e7eb;font-size:13px;min-width:640px;">
<thead><tr style="background:#f3f4f6;">
<th style="padding:6px 10px;border:1px solid #e5e7eb;text-align:right;">Days Left</th>
<th style="padding:6px 10px;border:1px solid #e5e7eb;text-align:left;">Domain</th>
<th style="padding:6px 10px;border:1px solid #e5e7eb;text-align:left;">Registrar</th>
<th style="padding:6px 10px;border:1px solid #e5e7eb;text-align:left;">Auto-Renew</th>
<th style="padding:6px 10px;border:1px solid #e5e7eb;text-align:left;">Suggestion</th>
<th style="padding:6px 10px;border:1px solid #e5e7eb;text-align:left;">Expires</th>
</tr></thead>
<tbody>
${HTML_TABLE}
</tbody></table>
<p style="margin:14px 0 0;color:#888;font-size:12px;">Red rows = under ${URGENT_DAYS} days, auto-renew OFF, suggestion = Keep. Yellow rows = under 7 days but lower urgency.</p>
<p style="margin:6px 0 0;color:#888;font-size:12px;">Source: ~/cncp-starter/cncp-config.json &middot; alert script: ~/Projects/cncp-domains/scripts/expiring-soon.sh</p>
</body></html>
HTMLEOF
)"

SUBJECT="CNCP domains expiring this week (${COUNT} in ${WINDOW_DAYS}d, ${URGENT_COUNT} urgent)"

# ---------- markdown fallback (always written for audit) ----------
LOG_FILE="${LOG_DIR}/expiring-${TODAY}.md"
{
  echo "# CNCP domains expiring within ${WINDOW_DAYS} days"
  echo ""
  echo "_As of ${TODAY} — ${COUNT} expiring (${KEEP_COUNT} Keep, ${TOSS_COUNT} Toss, ${URGENT_COUNT} under ${URGENT_DAYS}d)_"
  echo ""
  echo "| Days Left | Domain | Registrar | Auto-Renew | Suggestion | Expires |"
  echo "|---:|---|---|---|---|---|"
  echo "${EXPIRING_JSON}" | jq -r --argjson u "${URGENT_DAYS}" '
    def autorenew(n):
      if n == null then "unknown"
      elif (n | test("(?i)Auto-renew:\\s*ON")) then "ON"
      elif (n | test("(?i)Auto-renew:\\s*OFF")) then "OFF"
      else "unknown"
      end;
    def keepish(s): (s // "" | ascii_downcase) | startswith("keep");
    .[] | (
      (if (.daysLeft < $u and autorenew(.note) == "OFF" and keepish(.suggestion)) then "⚠ " else "" end)
      + (.daysLeft|tostring)
    ) as $d
    | "| " + $d + " | " + (.name // "?") + " | " + (.reg // "?") + " | " + autorenew(.note) + " | " + (.suggestion // "-") + " | " + (.expires // "") + " |"
  '
} > "${LOG_FILE}"

# ---------- DRY RUN: never actually email ----------
if [[ "${DRY_RUN}" == "1" ]]; then
  echo "[expiring-soon] DRY RUN — would email ${RECIPIENT}"
  echo "[expiring-soon] subject: ${SUBJECT}"
  echo "[expiring-soon] markdown report: ${LOG_FILE}"
  echo "[expiring-soon] expiring count: ${COUNT}"
  echo "----- markdown table -----"
  cat "${LOG_FILE}"
  exit 0
fi

# ---------- POST to George ----------
PAYLOAD="$(jq -nc \
  --arg to "${RECIPIENT}" \
  --arg subject "${SUBJECT}" \
  --arg body "${HTML_BODY}" \
  '{to:$to, subject:$subject, body:$body}')"

HTTP_OUT="$(curl -sS -o "${LOG_DIR}/last-george.json" -w '%{http_code}' \
  --connect-timeout 5 --max-time 30 \
  -X POST "${GEORGE_URL}" \
  -u "${GEORGE_AUTH}" \
  -H 'content-type: application/json' \
  --data-binary "${PAYLOAD}" 2>>"${LOG_DIR}/last-george.err")"
# curl's -w '%{http_code}' already emits "000" on connection failure; no fallback needed.

if [[ "${HTTP_OUT}" =~ ^2 ]]; then
  echo "[expiring-soon] sent ${COUNT} expiring domains to ${RECIPIENT} (HTTP ${HTTP_OUT})"
  exit 0
fi

echo "[expiring-soon] George send FAILED (HTTP ${HTTP_OUT}); markdown report kept at ${LOG_FILE}" >&2
exit 1