Integrations No. 405

Email Deliverability skill for Claude Code

From p=none to p=reject without losing real mail. Claude treats deliverability as an audit, not a guess. It inventories every service sending as your domain, reads DMARC aggregate reports with a bundled parser to find unaligned senders, fixes them with custom-domain DKIM, then moves the policy to quarantine and reject over full monthly cycles. It also checks Gmail, Yahoo and Outlook bulk rules, RFC 8058 unsubscribe, stream separation and warm-up.

  • nameemail-deliverability
  • size262 lines · ≈3,550 tokens
  • files2
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Gets transactional and marketing email into the inbox and keeps it there - inventory of every service sending as the domain, SPF/DKIM/DMARC alignment, DMARC rollout from p=none to quarantine to reject driven by aggregate reports (RFC 9989), Gmail, Yahoo and Microsoft bulk sender rules, RFC 8058 one-click unsubscribe, subdomain and IP stream separation, complaint and bounce hygiene, domain and IP warm-up, BIMI, and diagnosis from headers and SMTP errors. Use when mail lands in spam, Gmail returns 5.7.26 or 4.7.x errors, setting up DMARC enforcement, preparing for bulk sender requirements, or warming a new domain.

Try asking

  • Our password reset emails go to spam at Gmail; figure out why and fix it
  • Parse these DMARC reports and tell me what still fails before we move to p=reject
  • Get our newsletter compliant with the Gmail and Yahoo bulk sender requirements

Use it when

  • Mail lands in spam or Gmail returns 5.7.26, 5.7.32 or 4.7.28 errors
  • Moving DMARC from p=none to quarantine or reject
  • Preparing a domain for Gmail, Yahoo or Outlook bulk sender rules
  • Launching a new sending domain, subdomain or dedicated IP

Skip it when

  • You are wiring an email API, templates or bounce webhooks into the app - use transactional-email instead
  • You are planning sequences, segments and copy - use email-campaigns instead

How to check the result

The DMARC report parser shows every known source aligned and passing, seed sends to Gmail, Outlook and Yahoo show aligned SPF, DKIM and DMARC in the headers, and a one-click unsubscribe POST returns 200.

The common mistake it prevents

Switching DMARC to p=reject after a week of reports, before monthly billing or quarterly newsletters from a forgotten tool have appeared, so legitimate mail starts bouncing.

~/.claude/skills/email-deliverability/SKILL.md

--- name: email-deliverability description: Gets transactional and marketing email into the inbox and keeps it there - inventory of every service sending as the domain, SPF/DKIM/DMARC… license: MIT ---

Done well means: every legitimate source sending as your domain passes DMARC through an aligned DKIM signature, the domain sits at p=reject (or quarantine with a reason), marketing and transactional mail have separate subdomains and reputations, user-reported spam stays under 0.1% at Gmail, unsubscribes and complaints are honored automatically, and someone looks at DMARC reports and Postmaster data on a schedule. Authentication is the entry ticket; reputation decides the folder.

Ground rules

  • Find every sender before tightening anything: the app, the marketing platform, the CRM, support desk, billing, HR tools, Google Workspace or Microsoft 365. DMARC reports are how you find the ones nobody remembers.
  • Align with DKIM, not SPF. SPF breaks on forwarding and ESPs often use their own return-path domain; a DKIM signature with d= on your domain survives both.
  • Never jump to p=reject without at least one full monthly cycle of clean reports. Monthly invoices and quarterly newsletters only show up once per cycle.
  • DNS changes on the production domain (SPF, DMARC policy, deleting DKIM selectors) can stop mail company-wide. Show the exact record diff and get the user’s confirmation before publishing.
  • This skill covers infrastructure and reputation. Wiring a provider’s send API and webhooks belongs to transactional-email; campaign content and segmentation to email-campaigns.

Mailbox provider requirements (verify yearly; they tighten)

RequirementGmail (personal accounts)YahooOutlook.com consumer
Who is “bulk”About 5,000+ messages/day to Gmail, counted per primary domain; status is permanent once reachedBulk senders (no published number)More than 5,000/day, enforced since May 5, 2025
SPF and DKIMAll senders need SPF or DKIM; bulk needs bothBothBoth
DMARCBulk: published, p=none allowed, From aligned with SPF or DKIMAt least p=none, must passAt least p=none, aligned
Spam rateKeep below 0.1%, never reach 0.3% (Postmaster Tools)Below 0.3%Not published as a number
One-click unsubscribeMarketing/subscribed mail, RFC 8058, honor within 48 hoursRequired for marketing, honor within 2 daysRecommended
OtherTLS, valid forward and reverse DNS, RFC 5322 format, DKIM key >= 1024 bits (2048 recommended)Non-compliant mail junked, then rejected with 550 5.7.515

Gmail says it ramped up enforcement from November 2025, moving non-compliant traffic from temporary (4.7.x) to permanent (5.7.x) failures. Above 0.3% spam rate a bulk sender loses eligibility for mitigation until it stays below 0.3% for 7 consecutive days. Gmail’s spam rate is measured against inboxed mail, so a low number while most mail goes to spam is not good news.

Workflow

- [ ] 1. Inventory senders and current DNS (SPF, DKIM selectors, DMARC, BIMI)
- [ ] 2. Publish DMARC p=none with rua reporting; collect 2 to 4 weeks
- [ ] 3. Analyze reports; fix alignment for each legitimate source
- [ ] 4. Separate streams: subdomains, ESP streams/IP pools, Feedback-ID
- [ ] 5. Bulk sender compliance: one-click unsubscribe, complaint and bounce suppression
- [ ] 6. Monitoring: Postmaster Tools, SNDS, Yahoo CFL, DMARC report review
- [ ] 7. Enforce: quarantine, then reject; sp and np for subdomains
- [ ] 8. Warm up any new domain or IP; optional BIMI
- [ ] 9. Verify with real headers at Gmail, Outlook and Yahoo

1. Inventory

dig +short TXT example.com | grep -i spf
dig +short TXT _dmarc.example.com
dig +short TXT news.example.com; dig +short TXT _dmarc.news.example.com
dig +short TXT s1._domainkey.example.com     # repeat for each known selector
dig +short TXT default._bimi.example.com

List each service, the From domain it uses, its return-path (envelope) domain, and its DKIM d= domain. The transactional-email skill ships a DNS checker script for SPF lookup counts and record syntax.

2. DMARC with reporting

_dmarc.example.com TXT "v=DMARC1; p=none; rua=mailto:[email protected]; adkim=r; aspf=r"
  • Reports to a mailbox on another domain (for example a DMARC vendor) require that domain to publish an authorization record at example.com._report._dmarc.<vendor-domain>; vendors do this for you.
  • Volume can be large; a dedicated mailbox or a report-processing service beats a person’s inbox.
  • RFC 9989 (2026) replaces RFC 7489: pct is removed, t=y signals test mode (receivers apply one level less strict), np sets policy for non-existent subdomains, and the organizational domain is found by a DNS tree walk instead of the Public Suffix List. Existing v=DMARC1 records keep working.

3. Analyze and fix alignment

python3 scripts/dmarc_report.py reports/ --top 30
python3 scripts/dmarc_report.py reports/ --domain example.com --min-pass 98 --resolve

It reads .xml, .gz and .zip reports, prints the overall DMARC pass rate, and lists sources by failing volume with DKIM/SPF alignment and raw results. It flags “DKIM passes for a different domain” (the service signs with its own domain: enable custom-domain DKIM) and “no authentication at all” (unknown sender or spoofing). Exit 1 when below --min-pass.

Report showsMeaningFix
SPF pass for amazonses.com / sendgrid.net, DKIM pass for the ESP domain onlyService not configured for your domainAdd the ESP’s DKIM CNAMEs for your domain; optionally a custom MAIL FROM / return-path subdomain
DKIM pass, d= your domain, DMARC passCorrectNone
Your own IPs failing SPFMissing include: or IP in SPFAdd it, staying within 10 DNS lookups
Unknown IPs, no auth, small volumeSpoofing or forwardingNothing to fix; enforcement will block spoofing
Known forwarder/mailing list failingForwarding breaks SPF, list footers break DKIMExpected; ARC-sealing receivers handle much of it

4. Stream separation

StreamFrom domainWhy
Transactional (receipts, resets, alerts)example.com or mail.example.comHighest reputation; users expect it
Marketing / newslettersnews.example.comComplaints here must not sink password resets
Cold or partner outreachA separate domain entirely, or not at allHighest complaint risk
  • Each subdomain gets its own DKIM keys and return-path; DMARC at the organizational domain covers subdomains via sp= unless the subdomain publishes its own record.
  • Use separate ESP streams or pools (Postmark message streams, SES configuration sets or dedicated IP pools, SendGrid IP pools) so reputation does not mix.
  • Dedicated IPs only with steady volume high enough to keep a reputation; low or bursty volume on a dedicated IP performs worse than a good shared pool. Ask the ESP for its minimum.
  • Add a Feedback-ID: a:b:c:SenderId header (optional campaign, customer and mail-type slots, then a fixed 5 to 15 character sender ID; never a per-message value) so Gmail’s Postmaster feedback loop can flag the stream with unusual spam rates. It must be added before DKIM signing.

5. Unsubscribe, complaints, bounces

One-click unsubscribe (RFC 8058) on every marketing or subscribed message:

List-Unsubscribe: <https://news.example.com/u/3f9c2a...signed-token>, <mailto:[email protected]>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
  • The HTTPS endpoint accepts POST with body List-Unsubscribe=One-Click (form-encoded or multipart), needs no cookie or login, returns 200 without redirecting, and is idempotent.
  • The token identifies recipient and list and is signed or random and stored; never a raw email address.
  • The DKIM signature must cover both headers (h= includes them); ESPs that add the headers normally sign them, but check a real message.
  • Keep a visible unsubscribe link in the body too. A GET on that link shows a confirmation page; do not unsubscribe on GET alone, since security scanners prefetch links.
  • Transactional mail does not need one-click unsubscribe, but recipients decide what feels promotional; keep upsells out of receipts.

Complaints and bounces:

  • Suppress complainers from all non-essential mail immediately. Sources: ESP webhooks, Yahoo Complaint Feedback Loop, Microsoft JMRP. Gmail has no per-message feedback loop; watch Postmaster Tools.
  • Hard bounces: suppress permanently. Repeated soft bounces: suppress after a few consecutive attempts.
  • Sunset unengaged recipients on marketing streams (no clicks, logins or purchases for a period you define, such as 6 to 12 months); opens are unreliable since Apple Mail Privacy Protection.
  • Double opt-in on public forms; never import purchased or scraped lists (spam traps).

6. Monitoring

ToolShowsCadence
Google Postmaster ToolsCompliance status, spam rate, authentication for Gmail trafficWeekly; daily during warm-up
Microsoft SNDSIP-level data for Outlook.com (own IPs only)Weekly with dedicated IPs
Yahoo Sender Hub / CFLComplaints per messageWire into suppression
DMARC aggregate reportsAll sources sending as the domainWeekly until reject, monthly after
Blocklists (Spamhaus and others)Listings of your IPs/domainsOn delivery drops

7. Enforcement

  1. p=quarantine once all legitimate sources pass aligned DKIM and the pass rate for known sources is near 100% across a full monthly cycle. On receivers that follow RFC 9989, t=y lets you publish the stricter policy in test mode first; older receivers may still honor pct.
  2. p=reject after another clean cycle. Add sp=reject (or keep subdomain records) and np=reject to block made-up subdomains.
  3. Keep monitoring: a new SaaS tool sending as the domain will now fail loudly. Document “how to add a new sender” (DKIM first) for the team.

8. Warm-up and BIMI

Warm-up for a new domain, subdomain or dedicated IP:

  • Start with the most engaged recipients (recent clickers, active users) and transactional traffic; increase volume gradually day over day rather than sending the full list at once.
  • Watch deferrals (421 4.7.x), Postmaster spam rate and bounces daily. On deferrals, hold or reduce volume until they clear; never push through.
  • Use the ESP’s automated warm-up for dedicated IPs where available instead of inventing a schedule.

BIMI (logo in the inbox, Gmail requirements): DMARC p=quarantine or p=reject applied to 100% of mail, an SVG Tiny PS logo (baseProfile="tiny-ps", version="1.2", square, at least 96 px), and a VMC or CMC certificate:

default._bimi.example.com TXT "v=BIMI1; l=https://example.com/bimi/logo.svg; a=https://example.com/bimi/vmc.pem"

BIMI does not improve placement on its own; do it after enforcement, not instead of it.

Diagnosing from headers and errors

In Gmail use “Show original”; elsewhere read Authentication-Results:

Authentication-Results: mx.google.com;
  dkim=pass [email protected] header.s=s1;
  spf=pass smtp.mailfrom=bounces.example.com;
  dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.com

Check that header.from’s organizational domain matches header.i/d= (DKIM alignment) or smtp.mailfrom (SPF alignment).

ErrorMeaningAction
421 4.7.28 (Gmail)Unusual rate of unsolicited mail from your IP, DKIM or SPF domainSlow down, check spam rate and recent list sources
421 4.7.x other deferralsTemporary reputation or rate limitingBack off and retry; review recent changes
550 5.7.26 (Gmail)Unauthenticated, SPF hard fail, or rejected by your DMARC policyFix authentication and alignment for that source
5.7.27 / 5.7.30 (Gmail)Failed SPF / failed DKIM (both required for bulk senders)Fix the failing mechanism for that source
421 4.7.32 / 5.7.32 (Gmail)From header not aligned with SPF or DKIM domain: rate limited / blockedSign with d= on the From domain
550 5.7.515 (Outlook.com)Domain does not meet required authentication levelSPF, DKIM and DMARC for the sending domain
Inbox at Outlook, spam at Gmail (or reverse)Provider-specific reputationLook at that provider’s tool; segment by provider

Gotchas

  • Second SPF record for a new tool: both become permerror. Merge into one record.
  • SPF over 10 DNS lookups from stacked include:s; remove unused vendors before flattening.
  • DMARC report mailbox on the same domain filtered as spam by your own gateway.
  • DKIM key rotation that deletes the old selector before mail signed with it is delivered; publish the new selector, switch signing, remove the old one days later.
  • Marketing platform sending from the root domain because nobody configured the subdomain.
  • Unsubscribe endpoint behind login or bot protection: one-click POSTs fail silently.
  • Re-mailing an old list after a long gap: bounce and trap hits spike on the first send.

Verification

  1. dmarc_report.py on the latest week of reports shows every known source aligned and passing; remaining failures are identified as forwarding or spoofing.
  2. Seed sends to Gmail, Outlook.com and Yahoo inboxes show SPF, DKIM and DMARC pass with aligned domains in the headers, and land in the inbox.
  3. A one-click POST to the unsubscribe URL (curl -X POST -d 'List-Unsubscribe=One-Click' <url>) returns 200 and suppresses the address within the 48-hour window.
  4. Postmaster Tools compliance status shows passing checks and spam rate under 0.1%.

Output contract

## Deliverability audit: example.com (<date>)
- Senders found: <service -> From domain, DKIM d=, aligned yes/no>
- DNS: SPF <record, lookups n/10>; DKIM selectors <list>; DMARC <record>; BIMI <yes/no>
- DMARC pass rate (last 14 days): <x>% of <n> messages; failing sources: <list + cause>
- Streams: transactional <domain/stream>, marketing <domain/stream>
- Bulk sender compliance: one-click unsub <ok>, spam rate <x>%, complaints/bounces suppressed <ok>
- Plan: <dates for p=quarantine, p=reject, sp/np>; DNS changes awaiting approval: <diff>

Bundled files

Loaded only when a step in the skill needs them. Review them here before installing.

scripts/dmarc_report.py 225 lines · 9.2 KB
#!/usr/bin/env python3
"""Summarize DMARC aggregate (rua) reports: who sends as your domain, and what fails.

Usage:
  python3 scripts/dmarc_report.py REPORT [REPORT ...] [--top 25] [--min-pass 98]
                                   [--domain example.com] [--resolve]

REPORT may be an .xml, .xml.gz/.gz, or .zip file as mailbox providers send them, or a
directory containing such files. Handles RFC 7489 and RFC 9990 (namespaced) reports.

Output: per-report header, overall DMARC pass rate, and a table of sending sources
(source IP + header From domain) ordered by failing volume, with DKIM/SPF alignment
and the raw authentication results, so unaligned-but-passing services stand out.

Exit codes: 0 = parsed (and pass rate >= --min-pass if given), 1 = pass rate below
--min-pass, 2 = no readable reports.
"""
import argparse
import gzip
import io
import os
import socket
import sys
import xml.etree.ElementTree as ET
import zipfile
from collections import defaultdict
from datetime import datetime, timezone

MAX_BYTES = 50 * 1024 * 1024


def strip_ns(root):
    for el in root.iter():
        if isinstance(el.tag, str) and "}" in el.tag:
            el.tag = el.tag.split("}", 1)[1]
    return root


def text(el, path, default=""):
    found = el.find(path) if el is not None else None
    return (found.text or "").strip() if found is not None and found.text else default


def day(ts):
    try:
        return datetime.fromtimestamp(int(ts), timezone.utc).strftime("%Y-%m-%d")
    except (TypeError, ValueError, OverflowError, OSError):
        return ts or "?"


def read_payloads(path):
    """Yield (name, xml_bytes) for one file."""
    lower = path.lower()
    with open(path, "rb") as fh:
        raw = fh.read(MAX_BYTES + 1)
    if len(raw) > MAX_BYTES:
        raise ValueError("file larger than 50 MB")
    if lower.endswith(".zip") or raw[:4] == b"PK\x03\x04":
        with zipfile.ZipFile(io.BytesIO(raw)) as zf:
            for info in zf.infolist():
                if info.is_dir():
                    continue
                if info.file_size > MAX_BYTES:
                    raise ValueError(f"{info.filename}: uncompressed size over 50 MB")
                yield f"{path}:{info.filename}", zf.read(info)
    elif lower.endswith(".gz") or raw[:2] == b"\x1f\x8b":
        with gzip.GzipFile(fileobj=io.BytesIO(raw)) as gz:
            data = gz.read(MAX_BYTES + 1)
        if len(data) > MAX_BYTES:
            raise ValueError("decompressed size over 50 MB")
        yield path, data
    else:
        yield path, raw


def parse_report(name, data):
    head = data[:4096].upper()
    if b"<!DOCTYPE" in head or b"<!ENTITY" in data.upper():
        raise ValueError("DTD/entity declarations are not allowed in DMARC reports")
    root = strip_ns(ET.fromstring(data))
    if root.tag != "feedback":
        raise ValueError(f"root element is <{root.tag}>, expected <feedback>")
    meta = root.find("report_metadata")
    pol = root.find("policy_published")
    report = {
        "name": name,
        "org": text(meta, "org_name", "?"),
        "begin": text(meta, "date_range/begin"),
        "end": text(meta, "date_range/end"),
        "domain": text(pol, "domain"),
        "policy": "p={} sp={} adkim={} aspf={}".format(
            text(pol, "p", "?"), text(pol, "sp", "-"), text(pol, "adkim", "r"), text(pol, "aspf", "r")),
        "records": [],
    }
    for rec in root.findall("record"):
        row = rec.find("row")
        try:
            count = int(text(row, "count", "0"))
        except ValueError:
            count = 0
        dkim_eval = text(row, "policy_evaluated/dkim", "fail")
        spf_eval = text(row, "policy_evaluated/spf", "fail")
        dkim_raw = ["{}({})={}".format(text(d, "domain", "?"), text(d, "selector", "-"), text(d, "result", "?"))
                    for d in rec.findall("auth_results/dkim")]
        spf_raw = ["{}={}".format(text(s, "domain", "?"), text(s, "result", "?"))
                   for s in rec.findall("auth_results/spf")]
        report["records"].append({
            "ip": text(row, "source_ip", "?"),
            "count": count,
            "disposition": text(row, "policy_evaluated/disposition", "?"),
            "dkim_aligned": dkim_eval == "pass",
            "spf_aligned": spf_eval == "pass",
            "header_from": text(rec, "identifiers/header_from", "?").lower(),
            "dkim_raw": dkim_raw,
            "spf_raw": spf_raw,
            "dkim_any_pass": any(r.endswith("=pass") for r in dkim_raw),
            "spf_any_pass": any(r.endswith("=pass") for r in spf_raw),
        })
    return report


def collect(paths):
    files = []
    for p in paths:
        if os.path.isdir(p):
            for dirpath, _, names in os.walk(p):
                files += [os.path.join(dirpath, n) for n in sorted(names)
                          if n.lower().endswith((".xml", ".gz", ".zip"))]
        else:
            files.append(p)
    return files


def main():
    ap = argparse.ArgumentParser(description="Summarize DMARC aggregate reports")
    ap.add_argument("reports", nargs="+")
    ap.add_argument("--top", type=int, default=25, help="rows to show in the source table (default 25)")
    ap.add_argument("--min-pass", type=float, default=None, help="exit 1 if overall DMARC pass %% is below this")
    ap.add_argument("--domain", default=None, help="only count records whose header From is this domain")
    ap.add_argument("--resolve", action="store_true", help="reverse-DNS each source IP (slow)")
    args = ap.parse_args()

    reports, errors = [], 0
    for f in collect(args.reports):
        try:
            for name, data in read_payloads(f):
                reports.append(parse_report(name, data))
        except (OSError, ValueError, ET.ParseError, zipfile.BadZipFile, EOFError) as exc:
            errors += 1
            print(f"skip {f}: {exc}", file=sys.stderr)
    if not reports:
        print("error: no readable DMARC reports", file=sys.stderr)
        sys.exit(2)

    print(f"{len(reports)} report(s) parsed, {errors} skipped\n")
    for r in reports:
        print(f"- {r['org']:<28} {r['domain']:<24} {r['policy']:<36} {day(r['begin'])}..{day(r['end'])}")

    agg = defaultdict(lambda: {"count": 0, "fail": 0, "dkim_al": 0, "spf_al": 0,
                               "disp": defaultdict(int), "dkim_raw": set(), "spf_raw": set(),
                               "dkim_unaligned_pass": 0, "spf_unaligned_pass": 0})
    total = passed = 0
    want = args.domain.lower() if args.domain else None
    for r in reports:
        for rec in r["records"]:
            if want and rec["header_from"] != want:
                continue
            key = (rec["ip"], rec["header_from"])
            a = agg[key]
            n = rec["count"]
            ok = rec["dkim_aligned"] or rec["spf_aligned"]
            total += n
            passed += n if ok else 0
            a["count"] += n
            a["fail"] += 0 if ok else n
            a["dkim_al"] += n if rec["dkim_aligned"] else 0
            a["spf_al"] += n if rec["spf_aligned"] else 0
            a["disp"][rec["disposition"]] += n
            a["dkim_raw"].update(rec["dkim_raw"])
            a["spf_raw"].update(rec["spf_raw"])
            if rec["dkim_any_pass"] and not rec["dkim_aligned"]:
                a["dkim_unaligned_pass"] += n
            if rec["spf_any_pass"] and not rec["spf_aligned"]:
                a["spf_unaligned_pass"] += n

    if total == 0:
        print("\nno messages matched")
        sys.exit(2)
    rate = 100.0 * passed / total
    print(f"\nmessages: {total}   DMARC pass (aligned DKIM or SPF): {passed} ({rate:.2f}%)   fail: {total - passed}")

    if args.resolve:
        socket.setdefaulttimeout(2)
    print(f"\n{'source ip':<40} {'header from':<24} {'msgs':>7} {'fail':>7} {'dkim al':>8} {'spf al':>7}  disposition / auth results")
    rows = sorted(agg.items(), key=lambda kv: (-kv[1]["fail"], -kv[1]["count"]))
    for (ip, hfrom), a in rows[: args.top]:
        label = ip
        if args.resolve:
            try:
                label = f"{ip} ({socket.gethostbyaddr(ip)[0]})"
            except (OSError, socket.herror):
                pass
        disp = ",".join(f"{k}:{v}" for k, v in sorted(a["disp"].items()))
        print(f"{label[:40]:<40} {hfrom[:24]:<24} {a['count']:>7} {a['fail']:>7} {a['dkim_al']:>8} {a['spf_al']:>7}  {disp}")
        print(f"{'':<40}   dkim: {', '.join(sorted(a['dkim_raw'])) or 'none'}   spf: {', '.join(sorted(a['spf_raw'])) or 'none'}")
        hints = []
        if a["dkim_unaligned_pass"]:
            hints.append("DKIM passes for a different domain: set up custom-domain DKIM for this service")
        if a["spf_unaligned_pass"] and not a["dkim_al"]:
            hints.append("SPF passes for the provider's bounce domain only: needs aligned DKIM or a custom return-path")
        if a["fail"] and not a["dkim_raw"] and not a["spf_raw"]:
            hints.append("no authentication at all: unknown sender or spoofing")
        for h in hints:
            print(f"{'':<40}   hint: {h}")
    if len(rows) > args.top:
        print(f"... {len(rows) - args.top} more source(s); raise --top to see them")

    if args.min_pass is not None and rate < args.min_pass:
        print(f"\nFAIL: pass rate {rate:.2f}% is below --min-pass {args.min_pass}%")
        sys.exit(1)
    sys.exit(0)


if __name__ == "__main__":
    main()

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc