Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

74 lines · 4.2 KB · executableRaw History
1#!/usr/bin/env bash
2# Checks Cloudflare Email Sending for the domain Diffinery sends mail from
3# (notifications@diffinery.com): that the domain is onboarded and that every DNS
4# record Email Sending asks for (the cf-bounce MX and SPF, DKIM and DMARC) is
5# published. It changes nothing. Onboard the domain once in the dashboard:
6# Compute > Email Service > Email Sending > Onboard Domain.
7#
8# Requires: CLOUDFLARE_API_TOKEN (zone permissions Email Sending Read or Edit and
9# DNS Read on the domain), jq, curl. Optional: DIFFINERY_DOMAIN, MAIL_DOMAIN.
10# Always exits 0, warning about anything missing, so a deploy still goes out.
11set -euo pipefail
12
13: "${CLOUDFLARE_API_TOKEN:?set CLOUDFLARE_API_TOKEN}"
14DOMAIN="${MAIL_DOMAIN:-${DIFFINERY_DOMAIN:-diffinery.com}}"
15CF="https://api.cloudflare.com/client/v4"
16
17cf() { curl -sS "$CF$1" -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"; }
18warn() { echo "::warning::$*" >&2; }
19errors() { jq -r '[.errors[]? | "\(.code): \(.message)"] | join("; ")' <<<"$1"; }
20
21echo "::group::Email Sending for $DOMAIN"
22trap 'echo "::endgroup::"' EXIT
23
24# The zone that holds the domain (the domain itself or a parent).
25ZONE_ID="" ; name="$DOMAIN"
26while [[ "$name" == *.* ]]; do
27 ZONE_ID="$(cf "/zones?name=$name" | jq -r '.result[0].id // empty')"
28 [[ -n "$ZONE_ID" ]] && break
29 name="${name#*.}"
30done
31[[ -n "$ZONE_ID" ]] || { warn "Could not find the Cloudflare zone for $DOMAIN (the token needs Zone Read on it)."; exit 0; }
32echo "zone $name ($ZONE_ID)"
33
34# Without Email Sending access on the token, look at the public DNS records onboarding creates instead.
35public_check() {
36 doh() { curl -sS "https://cloudflare-dns.com/dns-query?name=$1&type=$2" -H 'accept: application/dns-json' | jq -r '.Answer[]?.data' 2>/dev/null; }
37 local ok=1
38 if doh "cf-bounce.$DOMAIN" MX | grep -q 'mx.cloudflare.net'; then echo "ok MX cf-bounce.$DOMAIN"; else echo "missing MX cf-bounce.$DOMAIN"; ok=0; fi
39 if doh "cf-bounce.$DOMAIN" TXT | grep -q 'v=spf1'; then echo "ok TXT cf-bounce.$DOMAIN (SPF)"; else echo "missing TXT cf-bounce.$DOMAIN (SPF)"; ok=0; fi
40 if doh "_dmarc.$DOMAIN" TXT | grep -qi 'v=DMARC1'; then echo "ok TXT _dmarc.$DOMAIN"; else echo "missing TXT _dmarc.$DOMAIN"; ok=0; fi
41 [[ "$ok" == 1 ]] && echo "$DOMAIN looks onboarded to Email Sending (public DNS)" || warn "$DOMAIN does not look onboarded to Email Sending, so mail may not be delivered. Onboard it under Compute > Email Service > Email Sending in the Cloudflare dashboard."
42}
43
44LIST="$(cf "/zones/$ZONE_ID/email/sending/subdomains")"
45if ! jq -e '.success' >/dev/null <<<"$LIST"; then
46 echo "can't read Email Sending settings with this token ($(errors "$LIST")), checking public DNS instead"
47 public_check
48 exit 0
49fi
50TAG="$(jq -r --arg d "$DOMAIN" '.result[]? | select(.name == $d) | .tag' <<<"$LIST" | head -n1)"
51if [[ -z "$TAG" ]]; then
52 warn "$DOMAIN is not onboarded to Email Sending, so no email will be delivered. Onboard it under Compute > Email Service > Email Sending in the Cloudflare dashboard."
53 exit 0
54fi
55cf "/zones/$ZONE_ID/email/sending/subdomains/$TAG" | jq -c '.result | {name, enabled, dkim_selector, return_path_domain}'
56
57WANT="$(cf "/zones/$ZONE_ID/email/sending/subdomains/$TAG/dns")"
58jq -e '.success' >/dev/null <<<"$WANT" || { warn "Could not read the DNS records Email Sending needs ($(errors "$WANT"))."; exit 0; }
59norm() { tr -d '"' | tr -s ' ' | sed 's/ *$//'; }
60missing=0
61while IFS= read -r rec; do
62 type="$(jq -r .type <<<"$rec")"; rname="$(jq -r .name <<<"$rec")"
63 want="$(jq -r .content <<<"$rec" | norm)"
64 have="$(cf "/zones/$ZONE_ID/dns_records?type=$type&name=$rname&per_page=100")"
65 if ! jq -e '.success' >/dev/null <<<"$have"; then echo "can't read DNS for $rname ($(errors "$have"))"; continue; fi
66 if jq -r '.result[].content' <<<"$have" | norm | grep -qxF -- "$want"; then
67 echo "ok $type $rname"
68 elif [[ "$type" == TXT && "$rname" == _dmarc.* ]] && jq -r '.result[].content' <<<"$have" | grep -qi 'v=DMARC1'; then
69 echo "ok TXT $rname (the domain's own DMARC policy)"
70 else
71 echo "missing $type $rname"; missing=1
72 fi
73done < <(jq -c '.result[]?' <<<"$WANT")
74[[ "$missing" == 0 ]] && echo "Email Sending is set up for $DOMAIN" || warn "Some Email Sending DNS records for $DOMAIN are missing, so mail may not be delivered. Finish onboarding in the dashboard."