Skip to content

DKIM / SPF / DMARC Builder & Checker — Java source

Build the three email-authentication DNS records — SPF, DKIM, and DMARC — as ready-to-paste TXT values, or look up a domain's live records over DNS-over-HTTPS. Import existing records, catch publishing mistakes, share via URL. Entirely client-side.

This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Email authentication (SPF / DKIM / DMARC) record parsing and lookup.
//
// Language: Java (17+, standard library only)
// Ported from src/lib/dkim-spf-dmarc.ts
// display source — part of CosmoDev's polyglot tool pages.
//
// Two layers:
//   - Pure parsers (parseSPF / parseDKIM / parseDMARC) — deterministic, never
//     throw, unit-testable without network.
//   - Check functions (checkSPF / checkDKIM / checkDMARC) — one DNS-over-HTTPS
//     fetch to Cloudflare's public resolver, then a pure parse. No backend.
//
// The JDK has no JSON API, so the DoH response is read by a ~60-line
// recursive-descent reader (MiniJson below) that understands exactly the JSON
// grammar the resolver emits: objects, arrays, strings, numbers, literals.
//
// Specs: SPF RFC 7208, DKIM RFC 6376, DMARC RFC 7489.

import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;

public final class DkimSpfDmarc {

    public enum CheckStatus { PASS, WARN, FAIL }

    /** One SPF term: qualifier + mechanism kind + optional value. */
    public record SPFMechanism(char qualifier, String kind, String value) {
    }

    public record SPFRecord(
            boolean valid,
            String version,
            List<SPFMechanism> mechanisms,
            String redirect,
            String exp,
            /** Mechanisms that cost a DNS query under RFC 7208 §4.6.4 (limit 10). */
            int lookupCount,
            /** Number of `v=spf1` records found (more than one is a hard error for receivers). */
            int recordCount,
            List<String> warnings) {

        static SPFRecord invalid(List<String> warnings) {
            return new SPFRecord(false, null, List.of(), null, null, 0, 0, warnings);
        }
    }

    public record DKIMRecord(
            boolean valid,
            String version,
            String keyType,
            /** Public key, base64, whitespace-stripped. */
            String publicKey,
            /** Approximate modulus size in bits (RSA only; derived from DER byte length). */
            Integer keyBits,
            List<String> hashes,
            List<String> services,
            List<String> flags,
            List<String> warnings) {
    }

    public record DMARCRecord(
            boolean valid,
            String policy,
            String subdomainPolicy,
            List<String> aggregateUris,
            List<String> forensicUris,
            Integer percent,
            String dkimAlignment,
            String spfAlignment,
            List<String> warnings) {
    }

    public record SPFResult(CheckStatus status, boolean found, SPFRecord record, String message) {
    }

    public record DKIMResult(CheckStatus status, boolean found, DKIMRecord record, String message) {
    }

    public record DMARCResult(CheckStatus status, boolean found, DMARCRecord record, String message) {
    }

    private DkimSpfDmarc() {
    }

    // ---------------------------------------------------------------------------
    // Shared helpers
    // ---------------------------------------------------------------------------

    /** Strip a leading scheme, userinfo, path, query, port, and trailing dot from user input. */
    public static String normalizeDomain(String raw) {
        String s = raw.trim();
        s = s.replaceFirst("(?i)^[a-z][a-z0-9+.-]*://", ""); // scheme://
        s = s.replaceFirst("(?i)^mailto:", ""); // mailto:user@domain
        if (s.contains("@")) s = s.substring(s.lastIndexOf('@') + 1); // keep host of user@host
        s = s.split("/")[0]; // drop path
        s = s.split("\\?")[0]; // drop query
        s = s.split(":")[0]; // drop port
        s = s.replaceFirst("\\.+$", ""); // trailing dot(s)
        return s.toLowerCase(Locale.ROOT);
    }

    private static final Pattern DOMAIN_RE =
            Pattern.compile("(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z]{2,}");

    /** True when the string looks like a plausible multi-label domain (example.com). */
    public static boolean isDomainLike(String domain) {
        return DOMAIN_RE.matcher(domain).matches() && domain.length() <= 253;
    }

    private static final Pattern SELECTOR_RE = Pattern.compile("[a-z0-9][a-z0-9._-]*", Pattern.CASE_INSENSITIVE);

    /** True when the selector is a safe single DNS label chain (no spaces, no traversal). */
    public static boolean isValidSelector(String selector) {
        String s = selector.trim();
        return !s.isEmpty() && s.length() <= 100 && SELECTOR_RE.matcher(s).matches() && !s.contains("..");
    }

    /** Split a tag value on `regex`, trim each part, drop empties. */
    private static List<String> splitList(String joined, String regex) {
        List<String> out = new ArrayList<>();
        for (String part : joined.split(regex, -1)) {
            String p = part.trim();
            if (!p.isEmpty()) out.add(p);
        }
        return out;
    }

    // ---------------------------------------------------------------------------
    // SPF — RFC 7208
    // ---------------------------------------------------------------------------

    private static final List<String> SPF_LOOKUP_KINDS = List.of("include", "a", "mx", "exists", "ptr");
    private static final List<String> SPF_KINDS = List.of("all", "include", "a", "mx", "ip4", "ip6", "exists", "ptr");
    private static final Pattern SPF_LINE_RE = Pattern.compile("(?i)v=spf1(\\s|$)");
    private static final Pattern SPF_MODIFIER_RE = Pattern.compile("(?i)([a-z][a-z0-9-]*)=(.*)");
    private static final Pattern MECHANISM_RE = Pattern.compile("(?i)([+\\-~?]?)([a-z0-9]+)(?::(.*))?");

    /** Parse one or more (newline-joined) TXT record strings for SPF. Pure. */
    public static SPFRecord parseSPF(String txt) {
        List<String> spfLines = new ArrayList<>();
        for (String line : txt.split("\n", -1)) {
            String l = line.trim().replaceFirst("^\"(.*)\"$", "$1");
            if (!l.isEmpty() && SPF_LINE_RE.matcher(l).find()) spfLines.add(l);
        }
        List<String> warnings = new ArrayList<>();

        if (spfLines.isEmpty()) {
            return SPFRecord.invalid(List.of("No v=spf1 record found in the supplied text."));
        }
        if (spfLines.size() > 1) {
            warnings.add(spfLines.size() + " SPF records found — RFC 7208 allows exactly one. Receivers treat this as a permanent error and ignore SPF for the whole domain.");
        }

        String[] terms = spfLines.get(0).split("\\s+");
        String version = terms[0];
        List<SPFMechanism> mechanisms = new ArrayList<>();
        String redirect = null;
        String exp = null;

        for (int i = 1; i < terms.length; i++) {
            String term = terms[i];
            // Modifiers use '=': redirect= and exp=
            var modifier = SPF_MODIFIER_RE.matcher(term);
            if (modifier.matches()) {
                String name = modifier.group(1).toLowerCase(Locale.ROOT);
                if (name.equals("redirect")) redirect = modifier.group(2);
                else if (name.equals("exp")) exp = modifier.group(2);
                else warnings.add("Unknown modifier \"" + term + "\" ignored.");
                continue;
            }
            var m = MECHANISM_RE.matcher(term);
            if (!m.matches()) {
                warnings.add("Unrecognized term \"" + term + "\" ignored.");
                continue;
            }
            char qualifier = (m.group(1) == null || m.group(1).isEmpty()) ? '+' : m.group(1).charAt(0);
            String kind = m.group(2).toLowerCase(Locale.ROOT);
            String value = m.group(3);
            if (!SPF_KINDS.contains(kind)) {
                warnings.add("Unknown mechanism \"" + term + "\" ignored.");
                continue;
            }
            if ((value == null || value.isEmpty()) && (kind.equals("include") || kind.equals("exists"))) {
                warnings.add("Mechanism \"" + term + "\" is missing its required value.");
                continue;
            }
            mechanisms.add(new SPFMechanism(qualifier, kind, value));
        }

        int lookupCount = (int) mechanisms.stream().filter(mech -> SPF_LOOKUP_KINDS.contains(mech.kind())).count()
                + (redirect != null ? 1 : 0);

        SPFMechanism all = mechanisms.stream().filter(mech -> mech.kind().equals("all")).findFirst().orElse(null);
        if (all == null && redirect == null) {
            warnings.add("No \"all\" mechanism and no \"redirect=\" — unmatched senders get a Neutral result, so anyone can still send mail that looks like this domain.");
        }
        if (all != null && all.qualifier() == '+') {
            warnings.add("\"+all\" explicitly allows every host on the internet to send mail as this domain — this defeats SPF entirely.");
        } else if (all != null && all.qualifier() == '?') {
            warnings.add("\"?all\" (Neutral) lets unmatched senders through with no protection. Prefer \"~all\" or \"-all\".");
        }
        if (mechanisms.stream().anyMatch(mech -> mech.kind().equals("ptr"))) {
            warnings.add("The \"ptr\" mechanism is deprecated (RFC 7208 §5.5) and should not be used.");
        }
        if (redirect != null && all != null) {
            warnings.add("A \"redirect=\" modifier is ignored when an \"all\" mechanism is present.");
        }
        if (lookupCount > 10) {
            warnings.add(lookupCount + " DNS-lookup mechanisms — RFC 7208 §4.6.4 caps SPF at 10. Receivers that hit the cap return permerror and ignore the record.");
        }

        return new SPFRecord(true, version, mechanisms, redirect, exp, lookupCount, spfLines.size(), warnings);
    }

    // ---------------------------------------------------------------------------
    // DKIM — RFC 6376
    // ---------------------------------------------------------------------------

    /** Decode base64 to bytes without throwing on bad input; null when invalid. */
    private static byte[] decodeBase64Lenient(String b64) {
        // Callers pass trimmed, non-empty values (empty p= is handled before this).
        String clean = b64.replaceAll("\\s+", "");
        String padded = clean + "=".repeat((4 - clean.length() % 4) % 4);
        try {
            return Base64.getDecoder().decode(padded);
        } catch (IllegalArgumentException e) {
            return null;
        }
    }

    /** Parse a `<selector>._domainkey` TXT record. Pure. */
    public static DKIMRecord parseDKIM(String txt) {
        List<String> warnings = new ArrayList<>();
        Map<String, String> tags = new LinkedHashMap<>();
        for (String part : txt.split(";", -1)) {
            String term = part.trim().replaceFirst("^\"(.*)\"$", "$1").trim();
            if (term.isEmpty()) continue;
            int eq = term.indexOf('=');
            if (eq <= 0) {
                warnings.add("Malformed tag \"" + term + "\" ignored.");
                continue;
            }
            tags.put(term.substring(0, eq).trim().toLowerCase(Locale.ROOT), term.substring(eq + 1).trim());
        }

        String version = tags.get("v");
        if (version != null && !version.toUpperCase(Locale.ROOT).equals("DKIM1")) {
            warnings.add("Unusual version tag v=" + version + " (expected DKIM1).");
        }
        String keyType = tags.getOrDefault("k", "rsa");
        String p = tags.get("p");
        List<String> hashes = splitList(tags.getOrDefault("h", ""), ":");
        List<String> services = splitList(tags.getOrDefault("s", ""), ":");
        List<String> flags = splitList(tags.getOrDefault("t", ""), ":");

        boolean valid = true;
        String publicKey = null;
        Integer keyBits = null;

        if (p == null) {
            warnings.add("No p= tag — this record is not a usable DKIM key.");
            valid = false;
        } else if (p.isEmpty()) {
            warnings.add("p= is empty — the key is revoked. Receivers will treat mail signed with this selector as unsigned.");
        } else {
            byte[] decoded = decodeBase64Lenient(p);
            if (decoded == null) {
                warnings.add("The p= value is not valid base64 — the key could not be read.");
            } else {
                publicKey = p.replaceAll("\\s+", "");
                if (keyType.equals("rsa")) {
                    // SubjectPublicKeyInfo DER ≈ modulus bits/8 + ~24 bytes of ASN.1 overhead.
                    // The estimate is close enough to classify 512/1024/2048/4096-bit keys.
                    int bits = Math.max(0, decoded.length - 24) * 8;
                    keyBits = bits;
                    if (bits < 1024) {
                        warnings.add("Weak RSA key (~" + bits + " bits). Keys under 1024 bits are considered breakable; RFC 8301 discourages short keys.");
                    } else if (bits < 2048) {
                        warnings.add("RSA key of ~" + bits + " bits works today but is below the recommended 2048 bits (RFC 8301).");
                    }
                }
            }
        }
        if (flags.contains("y")) {
            warnings.add("t=y — the key is in test mode: receivers must treat signatures as if unsigned.");
        }
        if (flags.contains("s")) {
            warnings.add("t=s — strict domain matching: the key cannot be used for subdomain signatures (informational).");
        }

        return new DKIMRecord(valid,
                version == null ? null : version.toUpperCase(Locale.ROOT),
                keyType, publicKey, keyBits, hashes, services, flags, warnings);
    }

    // ---------------------------------------------------------------------------
    // DMARC — RFC 7489
    // ---------------------------------------------------------------------------

    private static final List<String> DMARC_POLICIES = List.of("none", "quarantine", "reject");

    /** Parse a `_dmarc` TXT record. Pure. */
    public static DMARCRecord parseDMARC(String txt) {
        List<String> warnings = new ArrayList<>();
        Map<String, String> tags = new LinkedHashMap<>();
        for (String part : txt.split(";", -1)) {
            String term = part.trim().replaceFirst("^\"(.*)\"$", "$1").trim();
            if (term.isEmpty()) continue;
            int eq = term.indexOf('=');
            if (eq <= 0) {
                warnings.add("Malformed tag \"" + term + "\" ignored.");
                continue;
            }
            tags.put(term.substring(0, eq).trim().toLowerCase(Locale.ROOT), term.substring(eq + 1).trim());
        }

        boolean valid = true;
        String policy = null;
        String subdomainPolicy = null;
        List<String> aggregateUris = List.of();
        List<String> forensicUris = List.of();
        Integer percent = null;
        String dkimAlignment = null;
        String spfAlignment = null;

        String version = tags.get("v");
        if (version == null) {
            warnings.add("No v= tag — this is not a DMARC record.");
            valid = false;
        } else if (!version.toUpperCase(Locale.ROOT).equals("DMARC1")) {
            warnings.add("Unknown version v=" + version + " (expected DMARC1).");
            valid = false;
        }
        if (valid) {
            String p = tags.get("p") == null ? null : tags.get("p").toLowerCase(Locale.ROOT);
            if (p == null) {
                warnings.add("No p= policy tag — DMARC requires it.");
                valid = false;
            } else if (!DMARC_POLICIES.contains(p)) {
                warnings.add("Invalid policy p=" + p + " (expected none, quarantine, or reject).");
                valid = false;
            } else {
                policy = p;
            }
        }
        if (!valid) {
            return new DMARCRecord(false, null, null, aggregateUris, forensicUris, null, null, null, warnings);
        }

        String sp = tags.get("sp") == null ? null : tags.get("sp").toLowerCase(Locale.ROOT);
        if (sp != null) {
            if (DMARC_POLICIES.contains(sp)) subdomainPolicy = sp;
            else warnings.add("Invalid sp=" + sp + " ignored (expected none, quarantine, or reject).");
        }

        String rua = tags.get("rua");
        if (rua != null) aggregateUris = splitList(rua, ",");
        String ruf = tags.get("ruf");
        if (ruf != null) forensicUris = splitList(ruf, ",");

        String pct = tags.get("pct");
        if (pct != null) {
            try {
                long n = Long.parseLong(pct.trim());
                if (n < 0 || n > 100) throw new NumberFormatException();
                percent = (int) n;
            } catch (NumberFormatException e) {
                warnings.add("Invalid pct=" + pct + " ignored (must be 0-100).");
            }
        }

        String adkim = tags.get("adkim");
        if (adkim != null) {
            if (adkim.equals("r") || adkim.equals("s")) dkimAlignment = adkim;
            else warnings.add("Invalid adkim=" + adkim + " ignored (expected r or s).");
        }
        String aspf = tags.get("aspf");
        if (aspf != null) {
            if (aspf.equals("r") || aspf.equals("s")) spfAlignment = aspf;
            else warnings.add("Invalid aspf=" + aspf + " ignored (expected r or s).");
        }

        // Policy guidance
        if (policy.equals("none")) {
            warnings.add("p=none is monitor-only — no mail is quarantined or rejected, but you still need SPF/DKIM to pass for reports to be useful.");
        }
        if (aggregateUris.isEmpty()) {
            warnings.add("No rua= address — without aggregate reports you cannot see who is failing authentication. Add rua=mailto:reports@example.com.");
        } else if (!forensicUris.isEmpty()) {
            warnings.add("ruf= (forensic reports) is supported by few receivers and may leak message content to the report address (informational).");
        }
        if (percent != null && percent < 100 && !policy.equals("none")) {
            warnings.add("pct=" + percent + " applies the policy to only " + percent + "% of mail — the other " + (100 - percent) + "% is unaffected.");
        }
        return new DMARCRecord(true, policy, subdomainPolicy, aggregateUris, forensicUris, percent, dkimAlignment, spfAlignment, warnings);
    }

    // ---------------------------------------------------------------------------
    // DNS-over-HTTPS lookup
    // ---------------------------------------------------------------------------

    private static final String DOH_ENDPOINT = "https://cloudflare-dns.com/dns-query";
    private static final HttpClient HTTP = HttpClient.newHttpClient();

    /**
     * Minimal recursive-descent JSON reader — just enough grammar for the DoH
     * response: objects, arrays, strings (with escapes), numbers, literals.
     * Objects decode to Map<String, Object>, arrays to List<Object>, numbers
     * to Double. Not a general-purpose JSON library.
     */
    private static final class MiniJson {
        private final String s;
        private int i;

        MiniJson(String s) {
            this.s = s;
        }

        static Object parse(String s) {
            MiniJson p = new MiniJson(s);
            p.ws();
            Object v = p.value();
            p.ws();
            if (p.i != s.length()) throw new IllegalArgumentException("trailing JSON content");
            return v;
        }

        private Object value() {
            char c = s.charAt(i);
            return switch (c) {
                case '{' -> object();
                case '[' -> array();
                case '"' -> string();
                case 't' -> { expect("true"); yield Boolean.TRUE; }
                case 'f' -> { expect("false"); yield Boolean.FALSE; }
                case 'n' -> { expect("null"); yield null; }
                default -> number();
            };
        }

        private Map<String, Object> object() {
            Map<String, Object> m = new LinkedHashMap<>();
            i++; // {
            ws();
            if (s.charAt(i) == '}') { i++; return m; }
            while (true) {
                ws();
                String key = string();
                ws();
                if (s.charAt(i) != ':') throw new IllegalArgumentException("expected ':'");
                i++;
                ws();
                m.put(key, value());
                ws();
                char c = s.charAt(i++);
                if (c == '}') return m;
                if (c != ',') throw new IllegalArgumentException("expected ',' or '}'");
            }
        }

        private List<Object> array() {
            List<Object> l = new ArrayList<>();
            i++; // [
            ws();
            if (s.charAt(i) == ']') { i++; return l; }
            while (true) {
                ws();
                l.add(value());
                ws();
                char c = s.charAt(i++);
                if (c == ']') return l;
                if (c != ',') throw new IllegalArgumentException("expected ',' or ']'");
            }
        }

        private String string() {
            if (s.charAt(i) != '"') throw new IllegalArgumentException("expected string");
            i++;
            StringBuilder sb = new StringBuilder();
            while (s.charAt(i) != '"') {
                char c = s.charAt(i++);
                if (c == '\\') {
                    char e = s.charAt(i++);
                    sb.append(switch (e) {
                        case '"' -> '"';
                        case '\\' -> '\\';
                        case '/' -> '/';
                        case 'b' -> '\b';
                        case 'f' -> '\f';
                        case 'n' -> '\n';
                        case 'r' -> '\r';
                        case 't' -> '\t';
                        case 'u' -> (char) Integer.parseInt(s.substring(i, i + 4), 16); // also advances below
                        default -> throw new IllegalArgumentException("bad escape");
                    });
                    if (e == 'u') i += 4;
                } else {
                    sb.append(c);
                }
            }
            i++;
            return sb.toString();
        }

        private Double number() {
            int start = i;
            while (i < s.length() && "+-0123456789.eE".indexOf(s.charAt(i)) >= 0) i++;
            return Double.parseDouble(s.substring(start, i));
        }

        private void expect(String literal) {
            if (!s.startsWith(literal, i)) throw new IllegalArgumentException("bad literal");
            i += literal.length();
        }

        private void ws() {
            while (i < s.length() && Character.isWhitespace(s.charAt(i))) i++;
        }
    }

    /** Query TXT records for a name via Cloudflare's DoH JSON API. Returns unquoted strings. */
    static List<String> queryTxt(String name) throws IOException, InterruptedException {
        String url = DOH_ENDPOINT + "?name=" + URLEncoder.encode(name, StandardCharsets.UTF_8) + "&type=TXT";
        HttpRequest req = HttpRequest.newBuilder(URI.create(url))
                .header("Accept", "application/dns-json")
                .GET()
                .build();
        HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() != 200) {
            throw new IOException("DNS resolver responded with HTTP " + res.statusCode() + ".");
        }
        @SuppressWarnings("unchecked")
        Map<String, Object> json = (Map<String, Object>) MiniJson.parse(res.body());
        Double status = (Double) json.get("Status");
        if (status != null && status == 3) return List.of(); // NXDOMAIN — no such domain
        if (status != null && status != 0) {
            throw new IOException("DNS query failed with status " + status.intValue() + ".");
        }
        List<String> out = new ArrayList<>();
        for (Object a : (List<?>) json.getOrDefault("Answer", List.of())) {
            @SuppressWarnings("unchecked")
            Map<String, Object> answer = (Map<String, Object>) a;
            if (Double.valueOf(16).equals(answer.get("type"))) {
                // multi-chunk TXT: "part1" "part2"
                out.add(((String) answer.get("data"))
                        .replaceFirst("^\"(.*)\"$", "$1")
                        .replace("\" \"", ""));
            }
        }
        return out;
    }

    // ---------------------------------------------------------------------------
    // Check functions
    // ---------------------------------------------------------------------------

    /** Look up and evaluate a domain's SPF record. */
    public static SPFResult checkSPF(String domain) {
        String host = normalizeDomain(domain);
        if (!isDomainLike(host)) {
            return new SPFResult(CheckStatus.FAIL, false, null, "Enter a valid domain, e.g. example.com.");
        }
        try {
            SPFRecord record = parseSPF(String.join("\n", queryTxt(host)));
            if (!record.valid()) {
                return new SPFResult(CheckStatus.FAIL, false, null,
                        "No SPF record found for " + host + ". Receivers cannot verify which servers may send mail for it.");
            }
            CheckStatus status = record.warnings().isEmpty() ? CheckStatus.PASS : CheckStatus.WARN;
            return new SPFResult(status, true, record,
                    status == CheckStatus.PASS ? "SPF record found and looks healthy." : null);
        } catch (Exception err) {
            return new SPFResult(CheckStatus.FAIL, false, null,
                    err.getMessage() != null ? err.getMessage() : "DNS lookup failed.");
        }
    }

    /** Look up and evaluate a domain's DKIM public key for one selector. */
    public static DKIMResult checkDKIM(String domain, String selector) {
        String host = normalizeDomain(domain);
        if (!isDomainLike(host)) {
            return new DKIMResult(CheckStatus.FAIL, false, null, "Enter a valid domain, e.g. example.com.");
        }
        String sel = selector.trim().toLowerCase(Locale.ROOT);
        if (!isValidSelector(sel)) {
            return new DKIMResult(CheckStatus.FAIL, false, null,
                    "Enter a valid selector (letters, digits, dots, hyphens, underscores).");
        }
        try {
            DKIMRecord record = parseDKIM(String.join("\n", queryTxt(sel + "._domainkey." + host)));
            if (!record.valid()) {
                return new DKIMResult(CheckStatus.FAIL, false, null,
                        "No DKIM record found at " + sel + "._domainkey." + host + ". Try another selector — only one is checked per lookup.");
            }
            boolean revoked = record.warnings().stream().anyMatch(w -> w.contains("revoked"));
            CheckStatus status = revoked ? CheckStatus.FAIL
                    : record.warnings().isEmpty() ? CheckStatus.PASS : CheckStatus.WARN;
            return new DKIMResult(status, true, record, null);
        } catch (Exception err) {
            return new DKIMResult(CheckStatus.FAIL, false, null,
                    err.getMessage() != null ? err.getMessage() : "DNS lookup failed.");
        }
    }

    /** Look up and evaluate a domain's DMARC policy. */
    public static DMARCResult checkDMARC(String domain) {
        String host = normalizeDomain(domain);
        if (!isDomainLike(host)) {
            return new DMARCResult(CheckStatus.FAIL, false, null, "Enter a valid domain, e.g. example.com.");
        }
        try {
            DMARCRecord record = parseDMARC(String.join("\n", queryTxt("_dmarc." + host)));
            if (!record.valid()) {
                return new DMARCResult(CheckStatus.FAIL, false, null,
                        "No DMARC record found at _dmarc." + host + ". Receivers have no policy to apply when SPF or DKIM fails.");
            }
            CheckStatus status = record.warnings().isEmpty() ? CheckStatus.PASS : CheckStatus.WARN;
            return new DMARCResult(status, true, record, null);
        } catch (Exception err) {
            return new DMARCResult(CheckStatus.FAIL, false, null,
                    err.getMessage() != null ? err.getMessage() : "DNS lookup failed.");
        }
    }
}

Also available in 9 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →