← zurück zur Toolbox
mail_authenticator_v2.py · Quelltext
26480 Bytes · SHA-256: 0e3a306ac1073e251417756e0af81b9e446a9507d30b168d75d8f08f63790cf3
#!/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 · <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 & 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()