← zurück zur Toolbox

mail_authenticator_v2.py · Quelltext

26480 Bytes · SHA-256: 0e3a306ac1073e251417756e0af81b9e446a9507d30b168d75d8f08f63790cf3

⤓ Download
#!/usr/home/jozapf/public_html/toolenv/bin/python
# -*- coding: utf-8 -*-
# TOOLBOX-TILE: {"title": "Mail Authenticator", "desc": "Audit der E-Mail-Authentifizierung und Versand-Konfiguration einer Domain — SPF, DKIM, DMARC, MX, MTA-STS, TLS-RPT, BIMI, DNSSEC. DNS-basiert; Reputation/Inhalte bewertet er nicht.", "icon": "✉️", "type": "web", "example": "?domain=example.com", "order": 40}
"""
Mail Authenticator — email authentication & deliverability audit
Version: 2.0.0 (European DNS resolvers)
Date: 2026-07-12

Checks a domain's email-related DNS records and grades them A+..F:
  SPF, DMARC, DKIM (probes common selectors + an optional ?selector=),
  MX / Null-MX, MTA-STS, TLS-RPT, BIMI, DNSSEC.

All lookups go over DNS-over-HTTPS to EUROPEAN resolvers — DNS4EU (the official
EU resolver) with Quad9 (Switzerland) as fallback — via the RFC 8484 wire format
(dnspython encodes/decodes, httpx handles HTTP/2). No US resolvers, no UDP:53, so
it still works on restricted shared hosting. Runs as a plain Python CGI.
Dark-mode UI matched to the sibling toolbox tools.
"""

import datetime
import html
import os
import re
import sys
import traceback
from concurrent.futures import ThreadPoolExecutor
from urllib.parse import parse_qs, urlparse

import requests
import dns.message
import dns.query
import dns.flags

# Bot/abuse guard (toolbox_guard.py sits next to this tool on the server).
# Fail open on import trouble: the guard is best-effort protection, never a
# reason to take a working tool offline.
try:
    from toolbox_guard import guard as _guard
except Exception:  # pragma: no cover
    def _guard(_tool):
        return True

# Hardened outbound fetch (SSRF-safe host validation + redirect host checks +
# body cap). Shared across the toolbox tools. Fail open: if the module can't be
# imported, fall back to the tool's original requests.get so an import problem
# never takes the tool offline.
try:
    from toolbox_fetch import safe_get
except Exception:  # pragma: no cover
    safe_get = None

USER_AGENT = "MailAuthCheck/2.0 (+https://jozapf.de)"
# European DoH resolvers (RFC 8484 wire format over HTTP/2): DNS4EU (official EU
# resolver, unfiltered) primary, Quad9 (Switzerland) fallback. No US resolvers,
# no UDP:53. DNS4EU-unfiltered never blocks a domain being analysed; Quad9
# validates DNSSEC (its malware blocklist is irrelevant for mail-auth lookups).
_DOH = ("https://unfiltered.joindns4.eu/dns-query", "https://dns.quad9.net/dns-query")

# DKIM selectors cannot be enumerated via DNS; probe the common provider ones.
_DKIM_SELECTORS = [
    "google", "selector1", "selector2", "k1", "k2", "k3", "s1", "s2", "default",
    "default1", "default2", "mail", "dkim", "dkim1", "dkim2", "smtp", "mandrill",
    "mxvault", "sig1", "protonmail", "zoho", "amazonses", "fm1", "fm2", "fm3",
    "sendgrid", "key1", "key2", "pm", "mte1",
]


def _dated_default_selectors(months=24):
    """KonsoleH / Hetzner-style DKIM selectors are date-based: `default<YYMM>`
    (e.g. default2507). Probe the last N months so those domains are credited."""
    out, today = [], datetime.date.today()
    y, m = today.year % 100, today.month
    for _ in range(months):
        out.append("default%02d%02d" % (y, m))
        m -= 1
        if m == 0:
            m, y = 12, (y - 1) % 100
    return out

_GRADE_BANDS = [
    (95, "A+", "#10b981"), (85, "A", "#22c55e"), (70, "B", "#84cc16"),
    (55, "C", "#eab308"), (40, "D", "#f59e0b"), (25, "E", "#f97316"), (0, "F", "#ef4444"),
]
_STATUS_LABEL = {"pass": "OK", "warn": "Weak", "fail": "Missing", "info": "Info"}


# --------------------------------------------------------------------------- #
# DNS over HTTPS
# --------------------------------------------------------------------------- #

def doh(name, rtype):
    """Query name/rtype over DoH (RFC 8484 wire format, HTTP/2) against the
    European resolvers in _DOH. Returns {status, ad, answers[]} or None."""
    try:
        query = dns.message.make_query(name, rtype, want_dnssec=True)
    except Exception:
        return None
    want = _ANS_TYPE(rtype)
    for url in _DOH:
        try:
            r = dns.query.https(query, url, timeout=8,
                                http_version=dns.query.HTTPVersion.H2)
        except Exception:
            continue
        answers = [rr.to_text() for rrset in r.answer
                   if rrset.rdtype in want for rr in rrset]
        return {"status": r.rcode(), "ad": bool(r.flags & dns.flags.AD),
                "answers": answers}
    return None


def _ANS_TYPE(rtype):
    return {"TXT": (16,), "MX": (15,), "A": (1,), "AAAA": (28,), "SOA": (6,)}.get(rtype, ())


def txt_norm(data):
    """Normalize a DoH TXT record (may arrive as quoted, space-joined chunks)."""
    s = (data or "").strip()
    if '"' in s:
        parts = re.findall(r'"((?:[^"\\]|\\.)*)"', s)
        if parts:
            s = "".join(p.replace('\\"', '"') for p in parts)
    return s


def grade_for(score):
    score = max(0, min(100, score))
    for threshold, grade, color in _GRADE_BANDS:
        if score >= threshold:
            return grade, color
    return "F", "#ef4444"


# --------------------------------------------------------------------------- #
# Analysis
# --------------------------------------------------------------------------- #

def analyze(domain, user_selector=""):
    checks = []
    score = 100

    def add(name, status, value, detail, advice=""):
        checks.append({"name": name, "status": status, "value": value,
                       "detail": detail, "advice": advice})

    # --- MX -----------------------------------------------------------------
    mx = doh(domain, "MX")
    mx_records = [r for r in (mx["answers"] if mx else [])]
    null_mx = len(mx_records) == 1 and mx_records[0].strip().rstrip(".").endswith(" 0 ") is False and \
        re.match(r"^\s*0\s+\.?\s*$", mx_records[0].strip()) is not None
    if null_mx:
        add("MX", "info", "; ".join(mx_records),
            "Null-MX (RFC 7505) — the domain explicitly receives no mail.")
    elif mx_records:
        add("MX", "pass", "; ".join(mx_records[:5]),
            "%d mail exchanger(s) configured." % len(mx_records))
    else:
        score -= 10
        add("MX", "warn", None,
            "No MX records — the domain cannot receive mail (may be send-only).",
            "Add MX records if this domain should receive email.")

    # --- SPF ----------------------------------------------------------------
    root_txt = doh(domain, "TXT")
    spf_records = [txt_norm(r) for r in (root_txt["answers"] if root_txt else [])
                   if txt_norm(r).lower().startswith("v=spf1")]
    if not spf_records:
        score -= 25
        add("SPF", "fail", None,
            "No SPF record — receivers can't tell which servers may send for you.",
            "Publish a TXT record, e.g. v=spf1 include:_spf.provider.com -all")
    elif len(spf_records) > 1:
        score -= 15
        add("SPF", "fail", " | ".join(spf_records),
            "Multiple SPF records — invalid; receivers will fail SPF (permerror).",
            "Keep exactly one v=spf1 record.")
    else:
        spf = spf_records[0]
        m = re.search(r"([-~?+])all\b", spf)
        qual = m.group(1) if m else ""
        lookups = len(re.findall(r"\b(?:include|a|mx|ptr|exists|redirect)[:=]", spf))
        notes, adv = [], []
        if qual == "+":
            score -= 25
            notes.append("`+all` allows anyone to send (defeats SPF)")
            adv.append("change +all to -all or ~all")
        elif qual == "?":
            score -= 10
            notes.append("`?all` (neutral) gives no protection")
            adv.append("use ~all or -all")
        elif qual == "~":
            score -= 3
            notes.append("`~all` (softfail) — acceptable; `-all` is stricter")
        elif qual == "-":
            notes.append("`-all` (hardfail) — strong")
        else:
            score -= 8
            notes.append("no `all` mechanism — policy is undefined")
            adv.append("end the record with -all or ~all")
        if lookups > 10:
            score -= 8
            notes.append("~%d DNS-lookup mechanisms (SPF limit is 10 → permerror risk)" % lookups)
            adv.append("flatten includes to stay under 10 lookups")
        add("SPF", "pass" if qual in ("-", "~") and lookups <= 10 else "warn",
            spf, "Present. " + "; ".join(notes) + ".", " / ".join(adv))

    # --- DMARC --------------------------------------------------------------
    dmarc_txt = doh("_dmarc." + domain, "TXT")
    dmarc_records = [txt_norm(r) for r in (dmarc_txt["answers"] if dmarc_txt else [])
                     if txt_norm(r).lower().startswith("v=dmarc1")]
    dmarc_enforced = False
    if not dmarc_records:
        score -= 30
        add("DMARC", "fail", None,
            "No DMARC record — no policy against spoofing, no reporting.",
            "Publish _dmarc TXT: v=DMARC1; p=quarantine; rua=mailto:you@domain")
    else:
        dmarc = dmarc_records[0]
        tags = dict(re.findall(r"(\w+)\s*=\s*([^;]+)", dmarc))
        p = (tags.get("p", "") or "").strip().lower()
        sp = (tags.get("sp", "") or "").strip().lower()
        rua = "rua" in tags
        pct = tags.get("pct", "100").strip()
        notes, adv = [], []
        if p == "reject":
            dmarc_enforced = True
            notes.append("`p=reject` — strongest")
        elif p == "quarantine":
            dmarc_enforced = True
            notes.append("`p=quarantine` — strong enforcement (the practical standard); `p=reject` is the strictest")
        elif p == "none":
            score -= 10
            notes.append("`p=none` — monitoring only (a valid rollout stage, but not yet protecting against spoofing)")
            adv.append("once your rua reports confirm legit mail passes, tighten to p=quarantine → p=reject")
        else:
            score -= 20
            notes.append("no/invalid policy tag")
        if not rua:
            score -= 5
            notes.append("no `rua` — you get no aggregate reports")
            adv.append("add rua=mailto:…")
        if sp == "none" and p in ("quarantine", "reject"):
            score -= 3
            notes.append("`sp=none` — subdomains are unprotected")
            adv.append("drop sp=none (or set sp=reject)")
        if pct and pct != "100":
            score -= 3
            notes.append("`pct=%s` — policy applies to only part of mail" % pct)
        # Headline status = PRIMARY policy strength only (none < quarantine < reject).
        # sp=none / pct<100 / missing rua are already score deductions + notes above and
        # must NOT drag an enforcing policy (quarantine/reject) down to "weak".
        if dmarc_enforced:
            dmarc_status = "pass"
        elif p == "none":
            dmarc_status = "info"
        else:
            dmarc_status = "warn"
        add("DMARC", dmarc_status, dmarc, "Present. " + "; ".join(notes) + ".", " / ".join(adv))

    # --- DKIM (selector probing) --------------------------------------------
    selectors = []
    if user_selector:
        selectors += [s.strip() for s in re.split(r"[,\s]+", user_selector) if s.strip()]
    selectors += [s for s in _DKIM_SELECTORS if s not in selectors]
    selectors += [s for s in _dated_default_selectors() if s not in selectors]

    def _probe(sel):
        rec = doh("%s._domainkey.%s" % (sel, domain), "TXT")
        if not rec:
            return None
        vals = [txt_norm(r) for r in rec["answers"]]
        if any("v=dkim1" in v.lower() or "k=" in v.lower() or "p=" in v.lower() for v in vals):
            return sel
        return None

    found = []
    with ThreadPoolExecutor(max_workers=12) as _ex:   # probe concurrently to stay fast
        for res in _ex.map(_probe, selectors):
            if res:
                found.append(res)
    found = found[:5]
    if found:
        add("DKIM", "pass", ", ".join(found),
            "DKIM key(s) found for selector(s): %s." % ", ".join(found))
    elif user_selector:
        score -= 10
        add("DKIM", "fail", None,
            "No DKIM key at the selector(s) you specified (%s)." % user_selector,
            "Publish the DKIM TXT at <selector>._domainkey, or check the selector name.")
    else:
        add("DKIM", "info", None,
            "DKIM can't be auto-verified — selectors aren't discoverable via DNS. Enter your "
            "selector to check it; no result here does NOT mean DKIM is missing.")

    # --- MTA-STS ------------------------------------------------------------
    mtasts_txt = doh("_mta-sts." + domain, "TXT")
    mtasts = [txt_norm(r) for r in (mtasts_txt["answers"] if mtasts_txt else [])
              if "v=stsv1" in txt_norm(r).lower()]
    if mtasts:
        mode = ""
        try:
            pol_url = "https://mta-sts.%s/.well-known/mta-sts.txt" % domain
            if safe_get is not None:
                # safe_get validates the user-derived mta-sts.<domain> host and
                # host-checks every redirect hop; returns None on refusal/error.
                pol = safe_get(pol_url, timeout=8, max_bytes=65536,
                               headers={"User-Agent": USER_AGENT})
                pol_text = pol.text if pol is not None else ""
            else:  # pragma: no cover - import fallback keeps the tool online
                pol = requests.get(pol_url, headers={"User-Agent": USER_AGENT},
                                   timeout=8)
                pol_text = pol.text
            mm = re.search(r"mode\s*:\s*(\w+)", pol_text, re.I)
            mode = mm.group(1).lower() if mm else ""
        except Exception:
            mode = "?"
        if mode == "enforce":
            add("MTA-STS", "pass", mtasts[0], "Present and in `enforce` mode — inbound TLS is required.")
        elif mode == "testing":
            add("MTA-STS", "info", mtasts[0],
                "Present in `testing` mode — a valid rollout stage: senders report TLS failures "
                "without blocking delivery. Move to `enforce` once the reports look clean.")
        else:
            add("MTA-STS", "warn", mtasts[0],
                "TXT present but the policy mode is `%s` (not enforce or testing)." % (mode or "unreadable"),
                "Publish a valid policy file (mode: testing, then enforce).")
    else:
        score -= 5
        add("MTA-STS", "warn", None,
            "No MTA-STS — inbound mail servers aren't forced to use TLS.",
            "Optional but recommended for domains that receive mail.")

    # --- TLS-RPT ------------------------------------------------------------
    tlsrpt_txt = doh("_smtp._tls." + domain, "TXT")
    tlsrpt = [txt_norm(r) for r in (tlsrpt_txt["answers"] if tlsrpt_txt else [])
              if "v=tlsrptv1" in txt_norm(r).lower()]
    if tlsrpt:
        add("TLS-RPT", "pass", tlsrpt[0], "TLS reporting configured.")
    else:
        score -= 3
        add("TLS-RPT", "warn", None,
            "No TLS-RPT — you won't get reports about failed inbound TLS.",
            "Optional: _smtp._tls TXT v=TLSRPTv1; rua=mailto:…")

    # --- BIMI ---------------------------------------------------------------
    bimi_txt = doh("default._bimi." + domain, "TXT")
    bimi = [txt_norm(r) for r in (bimi_txt["answers"] if bimi_txt else [])
            if "v=bimi1" in txt_norm(r).lower()]
    if bimi:
        add("BIMI", "info", bimi[0], "BIMI record present (brand logo; needs DMARC enforcement).")
    else:
        add("BIMI", "info", None, "No BIMI record (optional; shows a brand logo in supporting inboxes).")

    # --- DNSSEC -------------------------------------------------------------
    soa = doh(domain, "SOA")
    if soa and soa["ad"]:
        add("DNSSEC", "pass", None, "The zone is DNSSEC-signed (resolver validated, AD flag).")
    else:
        score -= 5
        add("DNSSEC", "warn", None,
            "No DNSSEC validation — DNS answers (incl. SPF/DMARC) can be spoofed in transit.",
            "Enable DNSSEC at your DNS provider/registrar.")

    score = max(0, min(100, score))
    grade, color = grade_for(score)
    return {"score": score, "grade": grade, "color": color, "checks": checks, "domain": domain}


# --------------------------------------------------------------------------- #
# HTML
# --------------------------------------------------------------------------- #

def _pill(status):
    return '<span class="pill pill-%s">%s</span>' % (html.escape(status),
                                                     html.escape(_STATUS_LABEL.get(status, status)))


def _check_card(c):
    value_html = ('<div class="check-value"><code>%s</code></div>'
                  % html.escape(str(c["value"]))) if c.get("value") else ""
    advice_html = ('<p class="check-advice"><strong>Fix:</strong> %s</p>'
                   % html.escape(c["advice"])) if c.get("advice") else ""
    return """
        <div class="check check-%s">
            <div class="check-head"><span class="check-name">%s</span>%s</div>
            %s<p class="check-detail">%s</p>%s
        </div>""" % (html.escape(c["status"]), html.escape(c["name"]), _pill(c["status"]),
                     value_html, html.escape(c.get("detail") or ""), advice_html)


def build_html_page(domain="", selector="", analysis=None, error_message=None):
    esc_domain = html.escape(("https://" + domain) if domain else "", quote=True)
    esc_selector = html.escape(selector or "", quote=True)

    error_block = ""
    if error_message:
        error_block = '<div class="card error-card"><h2>Error</h2><p>%s</p></div>' % html.escape(error_message)

    results_block = ""
    if analysis is not None:
        checks = analysis["checks"]
        n_pass = sum(1 for c in checks if c["status"] == "pass")
        n_warn = sum(1 for c in checks if c["status"] == "warn")
        n_fail = sum(1 for c in checks if c["status"] == "fail")
        checks_html = "".join(_check_card(c) for c in checks)
        results_block = """
        <div class="card summary-card">
            <div class="grade-badge" style="--grade-color: %s;">%s</div>
            <div class="summary-meta">
                <div class="score-line">Score <strong>%d</strong>/100 &middot; <code>%s</code></div>
                <div class="count-line">
                    <span class="count count-pass">%d ok</span>
                    <span class="count count-warn">%d weak</span>
                    <span class="count count-fail">%d missing</span>
                </div>
            </div>
        </div>
        <div class="card"><h2>Checks</h2><div class="checks-grid">%s</div></div>
        <p style="text-align:center;margin-top:1.75rem;font-size:0.85rem;color:#94a3b8">E-Mail-Authentifizierung härten lassen? <a href="https://jozapf.de" target="_blank" rel="noopener" style="color:#3b82f6;text-decoration:none;font-weight:500">jozapf.de</a></p>""" % (
            analysis["color"], html.escape(analysis["grade"]), analysis["score"],
            html.escape(analysis["domain"]), n_pass, n_warn, n_fail, checks_html)

    page = "Content-Type: text/html; charset=utf-8\r\n\r\n"
    page += """<!DOCTYPE html>
<!-- jozapf.de toolbox · rev jzt-7c3f9a2e -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Jo Zapf Toolbox - Mail Authenticator</title>
<link rel="preconnect" href="https://assets.jozapf.de" crossorigin>
<link rel="stylesheet" href="https://assets.jozapf.de/css/fonts.css">
<style>
:root{--bg-primary:#0f172a;--bg-secondary:#1e293b;--text-primary:#f1f5f9;--text-secondary:#94a3b8;--accent:#3b82f6;--accent-secondary:#8b5cf6;--border:#334155;--code-bg:#0f172a;--success:#10b981;--warn:#f59e0b;--error:#ef4444;}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Montserrat',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg-primary);color:var(--text-primary);line-height:1.6;min-height:100vh;padding:2rem}
.container{max-width:1100px;margin:0 auto}
h1{font-size:2rem;font-weight:700;margin-bottom:.5rem;background:linear-gradient(135deg,var(--accent),var(--accent-secondary));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.subtitle{color:var(--text-secondary);margin-bottom:2rem}
h2{font-size:1.25rem;font-weight:600;margin-bottom:1rem}
.card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:12px;padding:1.5rem;margin-bottom:1.5rem}
.error-card{border-left:4px solid var(--error)}.error-card h2{color:var(--error)}
form{display:flex;flex-wrap:wrap;gap:.75rem;align-items:center}
input[type=text]{flex:1;min-width:220px;padding:.75rem 1rem;border-radius:8px;border:1px solid var(--border);background:var(--bg-primary);color:var(--text-primary);font-size:1rem;font-family:inherit}
input[type=text]::placeholder{color:var(--text-secondary)}
input[type=text]:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px rgba(59,130,246,.2)}
.sel{max-width:180px;flex:0 1 auto}
button{padding:.75rem 1.5rem;border-radius:8px;border:none;cursor:pointer;background:var(--accent);color:#fff;font-size:1rem;font-weight:600;font-family:inherit}
button:hover{background:#2563eb}
.form-hint{width:100%;font-size:.85rem;color:var(--text-secondary);margin-top:.5rem}
.summary-card{display:flex;gap:1.5rem;align-items:center;flex-wrap:wrap}
.grade-badge{flex:0 0 auto;width:110px;height:110px;border-radius:16px;display:flex;align-items:center;justify-content:center;font-size:3.5rem;font-weight:700;color:#fff;background:var(--grade-color);box-shadow:0 8px 24px color-mix(in srgb,var(--grade-color) 40%,transparent)}
.summary-meta{flex:1;min-width:240px}
.score-line{font-size:1.1rem;color:var(--text-secondary)}.score-line strong{color:var(--text-primary);font-size:1.4rem}
.count-line{display:flex;gap:.75rem;flex-wrap:wrap;margin-top:.6rem}
.count{font-size:.85rem;font-weight:600;padding:.2em .6em;border-radius:6px}
.count-pass{background:rgba(16,185,129,.15);color:var(--success)}.count-warn{background:rgba(245,158,11,.15);color:var(--warn)}.count-fail{background:rgba(239,68,68,.15);color:var(--error)}
.checks-grid{display:grid;gap:1rem}
.check{background:var(--bg-primary);border:1px solid var(--border);border-left-width:4px;border-radius:8px;padding:1rem 1.25rem}
.check-pass{border-left-color:var(--success)}.check-warn{border-left-color:var(--warn)}.check-fail{border-left-color:var(--error)}.check-info{border-left-color:var(--accent)}
.check-head{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;margin-bottom:.4rem}
.check-name{font-weight:600}
.pill{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em;padding:.2em .6em;border-radius:999px}
.pill-pass{background:rgba(16,185,129,.15);color:var(--success)}.pill-warn{background:rgba(245,158,11,.15);color:var(--warn)}.pill-fail{background:rgba(239,68,68,.15);color:var(--error)}.pill-info{background:rgba(59,130,246,.15);color:var(--accent)}
.check-value{margin:.35rem 0}
.check-detail{color:var(--text-secondary);font-size:.9rem}
.check-advice{color:var(--text-primary);font-size:.9rem;margin-top:.4rem}.check-advice strong{color:var(--accent)}
code{font-family:'JetBrains Mono','Fira Code',Consolas,monospace;font-size:.85em;background:var(--code-bg);color:#e879f9;padding:.15em .4em;border-radius:4px;word-break:break-all}
@media(max-width:768px){body{padding:1rem}h1{font-size:1.5rem}.card{padding:1rem}.grade-badge{width:88px;height:88px;font-size:2.75rem}}
</style>
</head>
<body>
<div class="container">
<h1>toolbox.jozapf.de | Mail Authenticator v2.0.0</h1>
<p class="subtitle">Audit a domain's email authentication &amp; deliverability — SPF · DKIM · DMARC · MTA-STS · DNSSEC</p>
<div class="card">
<h2>Check a domain</h2>
<form method="get" action="">
<input type="text" name="domain" value="__TB_DOMAIN__" placeholder="https://example.com">
<input type="text" class="sel" name="selector" value="__TB_SELECTOR__" placeholder="DKIM selector (optional)">
<button type="submit">Check</button>
<p class="form-hint">Queries SPF, DMARC, DKIM, MX, MTA-STS, TLS-RPT, BIMI and DNSSEC over DNS-over-HTTPS. DKIM selectors can't be listed via DNS — enter yours, or common ones are probed. <strong>Nothing is stored.</strong></p>
</form>
</div>
__TB_ERROR__
__TB_RESULTS__
</div>
</body>
</html>
"""
    # Single-pass substitution: re.sub scans the template ONCE and replaces each
    # placeholder with its mapped value verbatim (a function replacement is used
    # literally, no group/backslash interpretation). Replacement text is never
    # re-scanned, so a user value that happens to equal another sentinel (e.g. a
    # selector of "__TB_RESULTS__") stays literal and cannot expand into markup.
    _subs = {
        "__TB_DOMAIN__": esc_domain,
        "__TB_SELECTOR__": esc_selector,
        "__TB_ERROR__": error_block,
        "__TB_RESULTS__": results_block,
    }
    page = re.sub(r"__TB_(?:DOMAIN|SELECTOR|ERROR|RESULTS)__",
                  lambda m: _subs[m.group(0)], page)
    return page


def _clean_domain(raw):
    raw = (raw or "").strip()
    if "://" in raw or raw.startswith("//"):
        raw = urlparse(raw if "://" in raw else "http:" + raw).netloc or raw
    raw = raw.split("/")[0].split("@")[-1].strip().strip(".").lower()
    return raw


def main():
    params = parse_qs(os.environ.get("QUERY_STRING", ""))
    domain = _clean_domain(params.get("domain", params.get("url", [""]))[0])
    selector = params.get("selector", [""])[0].strip()

    if not domain:
        sys.stdout.write(build_html_page())
        return

    # Guard runs only on the executing path (this does outbound DNS). On a
    # blocked request the guard has already written the full response.
    if not _guard("mail-auth"):
        return

    if not re.match(r"^[a-z0-9.-]+\.[a-z]{2,}$", domain):
        sys.stdout.write(build_html_page(domain=domain, selector=selector,
                                         error_message="Not a valid domain: %s" % domain))
        return
    try:
        analysis = analyze(domain, selector)
        sys.stdout.write(build_html_page(domain=domain, selector=selector, analysis=analysis))
    except Exception:
        sys.stdout.write("Content-Type: text/plain; charset=utf-8\r\n\r\n")
        sys.stdout.write("Error during analysis:\n")
        traceback.print_exc(file=sys.stdout)


if __name__ == "__main__":
    main()