Skip to content

DKIM / SPF / DMARC Builder & Checker — C 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 C implementation — the same logic the interactive tool runs, in a shareable, citable form.

/*
 * dkim-spf-dmarc — email authentication (SPF / DKIM / DMARC) record parsing.
 *
 * Language: C (C11, POSIX) + the C stdlib only (the parsers are pure string
 *           work; the TS reference needs no crypto either).
 * Source:   CosmoDev polyglot showcase port of the dkim-spf-dmarc tool,
 *           ported from src/lib/dkim-spf-dmarc.ts (the canonical TypeScript
 *           implementation).
 * License:  display source — part of CosmoDev's polyglot tool pages.
 *
 * Like the TS reference, this file is two layers:
 *   - Pure parsers (parse_spf / parse_dkim / parse_dmarc) — deterministic,
 *     never fail, testable without network.
 *   - The TS check functions (checkSPF / checkDKIM / checkDMARC) add one
 *     DNS-over-HTTPS fetch to Cloudflare's resolver (browser `fetch`). C has
 *     no fetch; obtain the TXT records with your platform resolver (c-ares,
 *     res_query, libcurl + https://cloudflare-dns.com/dns-query) and feed
 *     the newline-joined strings to the parsers — the evaluation/status
 *     logic that sits on top of the parse is included below.
 *
 * Specs: SPF RFC 7208, DKIM RFC 6376, DMARC RFC 7489.
 *
 * Build: cc -std=c11 dkim-spf-dmarc.c
 */

#define _POSIX_C_SOURCE 200809L

#include <ctype.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* --------------------------------------------------------------- types --- */

typedef enum { STATUS_PASS, STATUS_WARN, STATUS_FAIL } check_status;

typedef enum { QUAL_PLUS, QUAL_MINUS, QUAL_TILDE, QUAL_QUESTION } spf_qualifier;

typedef enum {
    MECH_ALL, MECH_INCLUDE, MECH_A, MECH_MX, MECH_IP4, MECH_IP6, MECH_EXISTS, MECH_PTR,
} spf_mechanism_kind;

typedef struct {
    spf_qualifier qualifier;
    spf_mechanism_kind kind;
    char *value; /* domain / IP / CIDR argument after the colon; NULL when absent */
} spf_mechanism;

typedef struct {
    bool valid;
    char *version;              /* NULL when no v=spf1 line was found */
    spf_mechanism *mechanisms;
    size_t mechanism_count;
    char *redirect;             /* NULL when absent */
    char *exp;                  /* NULL when absent */
    /** Mechanisms that cost a DNS query under RFC 7208 §4.6.4 (limit 10). */
    size_t lookup_count;
    /** Number of `v=spf1` records found (more than one is a hard error). */
    size_t record_count;
    char **warnings;
    size_t warning_count;
} spf_record;

typedef struct {
    bool valid;
    char *version;              /* uppercased v= tag; NULL when absent */
    char *key_type;             /* k= tag, default "rsa" */
    char *public_key;           /* base64, whitespace-stripped; NULL until read */
    /** Approximate modulus size in bits (RSA only; derived from DER length). -1 when unknown. */
    long key_bits;
    char **hashes;   size_t hash_count;    /* h= split on ':' */
    char **services; size_t service_count; /* s= split on ':' */
    char **flags;    size_t flag_count;    /* t= split on ':' */
    char **warnings;
    size_t warning_count;
} dkim_record;

typedef enum { DMARC_NONE, DMARC_QUARANTINE, DMARC_REJECT } dmarc_policy;

typedef struct {
    bool valid;
    int policy;                 /* dmarc_policy; -1 when absent */
    int subdomain_policy;       /* sp= tag; -1 when absent/invalid */
    char **aggregate_uris;      size_t aggregate_count;  /* rua= */
    char **forensic_uris;       size_t forensic_count;   /* ruf= */
    long percent;               /* pct=; -1 when absent */
    int dkim_alignment;         /* adkim= 'r'|'s'; 0 when absent */
    int spf_alignment;          /* aspf= 'r'|'s'; 0 when absent */
    char **warnings;
    size_t warning_count;
} dmarc_record;

/* ------------------------------------------------------------- helpers --- */

static void *xrealloc(void *buf, size_t n) {
    void *p = realloc(buf, n);
    if (!p) { fprintf(stderr, "out of memory\n"); exit(1); }
    return p;
}

static char *xstrdup(const char *s) {
    size_t n = strlen(s) + 1;
    char *p = xrealloc(NULL, n);
    memcpy(p, s, n);
    return p;
}

static char *xstrndup(const char *s, size_t n) {
    char *p = xrealloc(NULL, n + 1);
    memcpy(p, s, n);
    p[n] = '\0';
    return p;
}

/** printf-style string builder (returns a fresh malloc'd string). */
static char *strf(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    int n = vsnprintf(NULL, 0, fmt, ap);
    va_end(ap);
    if (n < 0) return xstrdup("");
    char *out = xrealloc(NULL, (size_t)n + 1);
    va_start(ap, fmt);
    vsnprintf(out, (size_t)n + 1, fmt, ap);
    va_end(ap);
    return out;
}

static void push_string(char ***list, size_t *count, char *owned) {
    *list = xrealloc(*list, (*count + 1) * sizeof(char *));
    (*list)[(*count)++] = owned;
}

static void push_warning(char ***warnings, size_t *count, const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    int n = vsnprintf(NULL, 0, fmt, ap);
    va_end(ap);
    char *w = n < 0 ? xstrdup("") : xrealloc(NULL, (size_t)n + 1);
    if (n >= 0) {
        va_start(ap, fmt);
        vsnprintf(w, (size_t)n + 1, fmt, ap);
        va_end(ap);
    }
    push_string(warnings, count, w);
}

/** Trim in place by returning the first non-space offset and NUL-terminating
 *  the trailing spaces of a mutable copy. */
static char *trim_mut(char *s) {
    while (*s && isspace((unsigned char)*s)) s++;
    size_t end = strlen(s);
    while (end > 0 && isspace((unsigned char)s[end - 1])) s[--end] = '\0';
    return s;
}

static char *lowercase_dup(const char *s) {
    char *out = xstrdup(s);
    for (char *c = out; *c; c++) *c = (char)tolower((unsigned char)*c);
    return out;
}

static bool ci_equals(const char *a, const char *b) {
    while (*a && *b) {
        if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) return false;
        a++; b++;
    }
    return *a == '\0' && *b == '\0';
}

/** True when s starts with prefix, ignoring case. */
static bool ci_starts_with(const char *s, const char *prefix) {
    size_t n = strlen(prefix);
    if (strlen(s) < n) return false;
    for (size_t i = 0; i < n; i++) {
        if (tolower((unsigned char)s[i]) != tolower((unsigned char)prefix[i])) return false;
    }
    return true;
}

/** Split a mutable string on sep into trimmed, malloc'd pieces. */
static void split_all(char *s, char sep, char ***out, size_t *count) {
    *out = NULL;
    *count = 0;
    char *p = s;
    while (p) {
        char *next = strchr(p, sep);
        if (next) *next = '\0';
        push_string(out, count, xstrdup(trim_mut(p)));
        p = next ? next + 1 : NULL;
    }
}

/* ------------------------------------------------------- shared helpers --- */

/**
 * Strip a leading scheme, userinfo, path, query, port, and trailing dot from
 * user input (the TS normalizeDomain). Returns a fresh lowercase string.
 */
static char *normalize_domain(const char *raw) {
    char *s = xstrdup(raw);
    char *p = trim_mut(s);

    /* scheme:// — [a-z][a-z0-9+.-]*:\/\/ */
    if (p[0] && isalpha((unsigned char)p[0])) {
        size_t i = 1;
        while (p[i] && (isalnum((unsigned char)p[i]) || p[i] == '+' || p[i] == '.' || p[i] == '-')) i++;
        if (i >= 1 && p[i] == ':' && p[i + 1] == '/' && p[i + 2] == '/') {
            p += i + 3;
        }
    }
    /* mailto:user@domain */
    if (ci_starts_with(p, "mailto:")) p += 7;
    /* keep host of user@host */
    char *at = strrchr(p, '@');
    if (at) p = at + 1;
    /* drop path, query, port */
    char *cut = strpbrk(p, "/?:");
    if (cut) *cut = '\0';
    /* trailing dot(s) */
    size_t end = strlen(p);
    while (end > 0 && p[end - 1] == '.') p[--end] = '\0';

    char *out = lowercase_dup(p);
    free(s);
    return out;
}

/** Domain/label character tests — lowercase only, like the TS DOMAIN_RE. */
static bool is_dchar(char c) {
    unsigned char u = (unsigned char)c;
    return islower(u) || isdigit(u);
}

/** True when the string looks like a plausible multi-label domain
 *  (example.com): labels of [a-z0-9-] not starting/ending with '-', the last
 *  label alphabetic and >= 2 chars, total length <= 253. */
static bool is_domain_like(const char *domain) {
    size_t n = strlen(domain);
    if (n == 0 || n > 253 || !is_dchar(domain[0])) return false;
    size_t label_start = 0;
    bool last_alpha = false;
    for (size_t i = 0; i <= n; i++) {
        char c = domain[i];
        if (c == '.' || c == '\0') {
            size_t len = i - label_start;
            if (len == 0) return false;
            if (domain[i - 1] == '-') return false; /* label ends in '-' */
            if (c == '.') {
                if (i + 1 >= n || !is_dchar(domain[i + 1])) return false; /* '-x.' or 'a..b' */
            } else {
                /* final label must be all letters, length >= 2 */
                if (len < 2) return false;
                for (size_t j = label_start; j < i; j++) {
                    if (!islower((unsigned char)domain[j])) return false;
                }
                last_alpha = true;
            }
            label_start = i + 1;
        } else if (!is_dchar(c) && c != '-') {
            return false;
        }
    }
    return last_alpha; /* at least one '.' was seen before the final label */
}

/** True when the selector is a safe single DNS label chain (no spaces, no
 *  traversal): [a-z0-9][a-z0-9._-]* , length 1..100, no "..". */
static bool is_valid_selector(const char *selector) {
    char *orig = xstrdup(selector);
    char *trimmed = trim_mut(orig);
    size_t n = strlen(trimmed);
    bool ok = n > 0 && n <= 100 && isalnum((unsigned char)trimmed[0])
        && !strstr(trimmed, "..");
    for (size_t i = 0; ok && i < n; i++) {
        if (!isalnum((unsigned char)trimmed[i])
            && trimmed[i] != '.' && trimmed[i] != '_' && trimmed[i] != '-') ok = false;
    }
    free(orig); /* trimmed may point inside orig */
    return ok;
}

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

static bool spf_kind_from_name(const char *name, spf_mechanism_kind *out) {
    static const struct { const char *name; spf_mechanism_kind kind; } TABLE[] = {
        {"all", MECH_ALL}, {"include", MECH_INCLUDE}, {"a", MECH_A}, {"mx", MECH_MX},
        {"ip4", MECH_IP4}, {"ip6", MECH_IP6}, {"exists", MECH_EXISTS}, {"ptr", MECH_PTR},
    };
    for (size_t i = 0; i < sizeof TABLE / sizeof TABLE[0]; i++) {
        if (ci_equals(name, TABLE[i].name)) { *out = TABLE[i].kind; return true; }
    }
    return false;
}

static bool spf_kind_costs_lookup(spf_mechanism_kind kind) {
    return kind == MECH_INCLUDE || kind == MECH_A || kind == MECH_MX
        || kind == MECH_EXISTS || kind == MECH_PTR;
}

static void spf_record_free(spf_record *r) {
    free(r->version);
    for (size_t i = 0; i < r->mechanism_count; i++) {
        free(r->mechanisms[i].value);
    }
    free(r->mechanisms);
    free(r->redirect);
    free(r->exp);
    for (size_t i = 0; i < r->warning_count; i++) free(r->warnings[i]);
    free(r->warnings);
    memset(r, 0, sizeof *r);
}

/** Parse one or more (newline-joined) TXT record strings for SPF. Pure. */
static void parse_spf(const char *txt, spf_record *record) {
    memset(record, 0, sizeof *record);
    char *copy = xstrdup(txt);
    char *p = copy;

    /* Collect lines, strip one pair of surrounding quotes, keep v=spf1 ones. */
    char **spf_lines = NULL;
    size_t spf_line_count = 0;
    while (p) {
        char *next = strchr(p, '\n');
        if (next) *next = '\0';
        char *line = trim_mut(p);
        size_t len = strlen(line);
        if (len >= 2 && line[0] == '"' && line[len - 1] == '"') {
            line[len - 1] = '\0';
            line++;
        }
        line = trim_mut(line);
        if (line[0] != '\0' && ci_starts_with(line, "v=spf1")
            && (line[6] == '\0' || isspace((unsigned char)line[6]))) {
            push_string(&spf_lines, &spf_line_count, xstrdup(line));
        }
        p = next ? next + 1 : NULL;
    }
    free(copy);
    record->record_count = spf_line_count;

    if (spf_line_count == 0) {
        push_warning(&record->warnings, &record->warning_count,
            "No v=spf1 record found in the supplied text.");
        return;
    }
    if (spf_line_count > 1) {
        push_warning(&record->warnings, &record->warning_count,
            "%zu SPF records found — RFC 7208 allows exactly one. Receivers treat "
            "this as a permanent error and ignore SPF for the whole domain.",
            spf_line_count);
    }

    /* Tokenise the first record on whitespace runs. */
    char *terms[128];
    size_t term_count = 0;
    char *t = spf_lines[0];
    while (*t && term_count < 128) {
        while (*t && isspace((unsigned char)*t)) t++;
        if (!*t) break;
        char *start = t;
        while (*t && !isspace((unsigned char)*t)) t++;
        if (*t) *t++ = '\0';
        terms[term_count++] = start;
    }
    record->version = xstrdup(terms[0]);

    for (size_t i = 1; i < term_count; i++) {
        char *term = terms[i];
        /* Modifiers use '=': redirect= and exp= — ^([a-z][a-z0-9-]*)=(.*)$ */
        char *eq = strchr(term, '=');
        if (eq && eq != term) {
            bool name_ok = isalpha((unsigned char)term[0]);
            for (char *c = term + 1; name_ok && c < eq; c++) {
                if (!isalnum((unsigned char)*c) && *c != '-') name_ok = false;
            }
            if (name_ok) {
                char *name_raw = xstrndup(term, (size_t)(eq - term));
                char *name = lowercase_dup(name_raw);
                free(name_raw);
                if (strcmp(name, "redirect") == 0) {
                    free(record->redirect);
                    record->redirect = xstrdup(eq + 1);
                } else if (strcmp(name, "exp") == 0) {
                    free(record->exp);
                    record->exp = xstrdup(eq + 1);
                } else {
                    push_warning(&record->warnings, &record->warning_count,
                        "Unknown modifier \"%s\" ignored.", term);
                }
                free(name);
                continue;
            }
        }
        /* Mechanism: ^([+\-~?])?([a-z0-9]+)(?::(.*))?$ */
        char *q = term;
        spf_qualifier qual = QUAL_PLUS;
        if (*q == '+' || *q == '-' || *q == '~' || *q == '?') {
            qual = *q == '+' ? QUAL_PLUS : *q == '-' ? QUAL_MINUS
                 : *q == '~' ? QUAL_TILDE : QUAL_QUESTION;
            q++;
        }
        char *kind_start = q;
        while (*q && isalnum((unsigned char)*q)) q++;
        size_t kind_len = (size_t)(q - kind_start);
        char *value = NULL;
        if (*q == ':') value = xstrdup(q + 1);
        else if (*q != '\0') {
            push_warning(&record->warnings, &record->warning_count,
                "Unrecognized term \"%s\" ignored.", term);
            continue;
        }
        if (kind_len == 0) {
            push_warning(&record->warnings, &record->warning_count,
                "Unrecognized term \"%s\" ignored.", term);
            free(value);
            continue;
        }
        char *kind_raw = xstrndup(kind_start, kind_len);
        char *kind_lower = lowercase_dup(kind_raw);
        free(kind_raw);
        spf_mechanism_kind kind;
        if (!spf_kind_from_name(kind_lower, &kind)) {
            push_warning(&record->warnings, &record->warning_count,
                "Unknown mechanism \"%s\" ignored.", term);
            free(value);
            free(kind_lower);
            continue;
        }
        if (!value && (kind == MECH_INCLUDE || kind == MECH_EXISTS)) {
            push_warning(&record->warnings, &record->warning_count,
                "Mechanism \"%s\" is missing its required value.", term);
            free(kind_lower);
            continue;
        }
        record->mechanisms = xrealloc(record->mechanisms,
                                      (record->mechanism_count + 1) * sizeof(spf_mechanism));
        spf_mechanism *mech = &record->mechanisms[record->mechanism_count++];
        mech->qualifier = qual;
        mech->kind = kind;
        mech->value = value;
        free(kind_lower);
    }

    size_t lookups = 0;
    for (size_t i = 0; i < record->mechanism_count; i++) {
        if (spf_kind_costs_lookup(record->mechanisms[i].kind)) lookups++;
    }
    if (record->redirect) lookups++;
    record->lookup_count = lookups;

    const spf_mechanism *all = NULL;
    for (size_t i = 0; i < record->mechanism_count; i++) {
        if (record->mechanisms[i].kind == MECH_ALL) { all = &record->mechanisms[i]; break; }
    }
    if (!all && !record->redirect) {
        push_warning(&record->warnings, &record->warning_count,
            "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 && all->qualifier == QUAL_PLUS) {
        push_warning(&record->warnings, &record->warning_count,
            "\"+all\" explicitly allows every host on the internet to send mail as "
            "this domain — this defeats SPF entirely.");
    } else if (all && all->qualifier == QUAL_QUESTION) {
        push_warning(&record->warnings, &record->warning_count,
            "\"?all\" (Neutral) lets unmatched senders through with no protection. "
            "Prefer \"~all\" or \"-all\".");
    }
    for (size_t i = 0; i < record->mechanism_count; i++) {
        if (record->mechanisms[i].kind == MECH_PTR) {
            push_warning(&record->warnings, &record->warning_count,
                "The \"ptr\" mechanism is deprecated (RFC 7208 §5.5) and should not be used.");
            break;
        }
    }
    if (record->redirect && all) {
        push_warning(&record->warnings, &record->warning_count,
            "A \"redirect=\" modifier is ignored when an \"all\" mechanism is present.");
    }
    if (record->lookup_count > 10) {
        push_warning(&record->warnings, &record->warning_count,
            "%zu DNS-lookup mechanisms — RFC 7208 §4.6.4 caps SPF at 10. Receivers "
            "that hit the cap return permerror and ignore the record.",
            record->lookup_count);
    }

    record->valid = true;
    for (size_t i = 0; i < spf_line_count; i++) free(spf_lines[i]);
    free(spf_lines);
}

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

/** Decode base64 to a byte buffer without failing hard on bad input.
 *  Mirrors decodeBase64Lenient + atob: whitespace stripped, missing padding
 *  tolerated, invalid characters rejected. Returns the decoded length or -1. */
static long base64_decode_lenient(const char *b64, uint8_t *out, size_t cap) {
    size_t n = strlen(b64);
    char *clean = xrealloc(NULL, n + 1);
    size_t c = 0;
    for (size_t i = 0; i < n; i++) {
        if (!isspace((unsigned char)b64[i])) clean[c++] = b64[i];
    }
    clean[c] = '\0';
    long result = -1;
    if (c > 0 && c % 4 != 1) { /* impossible length (1 mod 4) fails, like atob */
        static const char B64[] =
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
        bool ok = true;
        size_t out_len = 0;
        for (size_t i = 0; i < c && ok; i += 4) {
            size_t group = (c - i < 4) ? c - i : 4;
            int v[4] = {0};
            size_t data = 0; /* non-padding chars in this group */
            for (size_t j = 0; j < group; j++) {
                if (clean[i + j] == '=') break; /* padding ends the input */
                const char *pos = strchr(B64, clean[i + j]);
                if (!pos) { ok = false; break; }
                v[data++] = (int)(pos - B64);
            }
            if (!ok) break;
            if (data >= 2 && out_len < cap) out[out_len++] = (uint8_t)((v[0] << 2) | (v[1] >> 4));
            if (data >= 3 && out_len < cap) out[out_len++] = (uint8_t)(((v[1] & 0x0f) << 4) | (v[2] >> 2));
            if (data >= 4 && out_len < cap) out[out_len++] = (uint8_t)(((v[2] & 0x03) << 6) | v[3]);
            if (data < group) break; /* reached '=' — nothing more to decode */
        }
        if (ok) result = (long)out_len;
    }
    free(clean);
    return result;
}

static void dkim_record_free(dkim_record *r) {
    free(r->version);
    free(r->key_type);
    free(r->public_key);
    for (size_t i = 0; i < r->hash_count; i++) free(r->hashes[i]);
    free(r->hashes);
    for (size_t i = 0; i < r->service_count; i++) free(r->services[i]);
    free(r->services);
    for (size_t i = 0; i < r->flag_count; i++) free(r->flags[i]);
    free(r->flags);
    for (size_t i = 0; i < r->warning_count; i++) free(r->warnings[i]);
    free(r->warnings);
    memset(r, 0, sizeof *r);
}

/** Look a tag up in a parsed tag list (first occurrence wins, like the TS Map). */
static const char *tag_get(char **keys, char **values, size_t count, const char *key) {
    for (size_t i = 0; i < count; i++) {
        if (strcmp(keys[i], key) == 0) return values[i];
    }
    return NULL;
}

/** Parse a `<selector>._domainkey` TXT record. Pure. */
static void parse_dkim(const char *txt, dkim_record *record) {
    memset(record, 0, sizeof *record);
    record->key_bits = -1;

    char *copy = xstrdup(txt);
    char **keys = NULL, **values = NULL;
    size_t tag_count = 0;
    char *p = copy;
    while (p) {
        char *next = strchr(p, ';');
        if (next) *next = '\0';
        char *term = trim_mut(p);
        size_t len = strlen(term);
        if (len >= 2 && term[0] == '"' && term[len - 1] == '"') {
            term[len - 1] = '\0';
            term++;
            term = trim_mut(term);
        }
        if (term[0] != '\0') {
            char *eq = strchr(term, '=');
            if (!eq || eq == term) {
                push_warning(&record->warnings, &record->warning_count,
                    "Malformed tag \"%s\" ignored.", term);
            } else {
                *eq = '\0';
                char *key = lowercase_dup(trim_mut(term));
                char *value = xstrdup(trim_mut(eq + 1));
                push_string(&keys, &tag_count, key);
                values = xrealloc(values, tag_count * sizeof(char *));
                values[tag_count - 1] = value;
            }
        }
        p = next ? next + 1 : NULL;
    }

    const char *version = tag_get(keys, values, tag_count, "v");
    if (version && !ci_equals(version, "DKIM1")) {
        push_warning(&record->warnings, &record->warning_count,
            "Unusual version tag v=%s (expected DKIM1).", version);
    }
    const char *key_type = tag_get(keys, values, tag_count, "k");
    record->key_type = xstrdup(key_type ? key_type : "rsa");
    if (version) {
        record->version = xstrdup(version);
        for (char *c = record->version; *c; c++) *c = (char)toupper((unsigned char)*c);
    }

    /* colon-separated lists: h= / s= / t= */
    char **parts = NULL;
    size_t part_count = 0;
    const char *h = tag_get(keys, values, tag_count, "h");
    char *h_copy = xstrdup(h ? h : "");
    split_all(h_copy, ':', &parts, &part_count);
    for (size_t i = 0; i < part_count; i++) {
        if (parts[i][0]) push_string(&record->hashes, &record->hash_count, parts[i]);
        else free(parts[i]);
    }
    free(parts);
    free(h_copy);

    parts = NULL; part_count = 0;
    const char *s = tag_get(keys, values, tag_count, "s");
    char *s_copy = xstrdup(s ? s : "");
    split_all(s_copy, ':', &parts, &part_count);
    for (size_t i = 0; i < part_count; i++) {
        if (parts[i][0]) push_string(&record->services, &record->service_count, parts[i]);
        else free(parts[i]);
    }
    free(parts);
    free(s_copy);

    parts = NULL; part_count = 0;
    const char *t = tag_get(keys, values, tag_count, "t");
    char *t_copy = xstrdup(t ? t : "");
    split_all(t_copy, ':', &parts, &part_count);
    for (size_t i = 0; i < part_count; i++) {
        if (parts[i][0]) push_string(&record->flags, &record->flag_count, parts[i]);
        else free(parts[i]);
    }
    free(parts);
    free(t_copy);

    record->valid = true;

    const char *pub = tag_get(keys, values, tag_count, "p");
    if (!pub) {
        record->valid = false;
        push_warning(&record->warnings, &record->warning_count,
            "No p= tag — this record is not a usable DKIM key.");
        goto done;
    }
    if (pub[0] == '\0') {
        push_warning(&record->warnings, &record->warning_count,
            "p= is empty — the key is revoked. Receivers will treat mail signed "
            "with this selector as unsigned.");
        goto done;
    }

    {
        uint8_t decoded[8192];
        long decoded_len = base64_decode_lenient(pub, decoded, sizeof decoded);
        if (decoded_len < 0) {
            push_warning(&record->warnings, &record->warning_count,
                "The p= value is not valid base64 — the key could not be read.");
            goto done;
        }
        char *pk = xstrdup(pub);
        size_t kept = 0;
        for (char *c = pk; *c; c++) {
            if (!isspace((unsigned char)*c)) pk[kept++] = *c;
        }
        pk[kept] = '\0';
        record->public_key = pk;

        if (strcmp(record->key_type, "rsa") == 0) {
            /* SubjectPublicKeyInfo DER ≈ modulus bits/8 + ~24 bytes of ASN.1
             * overhead. The estimate is close enough to classify 512/1024/
             * 2048/4096-bit keys. */
            long bits = (decoded_len - 24 > 0 ? decoded_len - 24 : 0) * 8;
            record->key_bits = bits;
            if (bits < 1024) {
                push_warning(&record->warnings, &record->warning_count,
                    "Weak RSA key (~%ld bits). Keys under 1024 bits are considered "
                    "breakable; RFC 8301 discourages short keys.", bits);
            } else if (bits < 2048) {
                push_warning(&record->warnings, &record->warning_count,
                    "RSA key of ~%ld bits works today but is below the recommended "
                    "2048 bits (RFC 8301).", bits);
            }
        }
        for (size_t i = 0; i < record->flag_count; i++) {
            if (strcmp(record->flags[i], "y") == 0) {
                push_warning(&record->warnings, &record->warning_count,
                    "t=y — the key is in test mode: receivers must treat signatures "
                    "as if unsigned.");
            } else if (strcmp(record->flags[i], "s") == 0) {
                push_warning(&record->warnings, &record->warning_count,
                    "t=s — strict domain matching: the key cannot be used for "
                    "subdomain signatures (informational).");
            }
        }
    }

done:
    for (size_t i = 0; i < tag_count; i++) { free(keys[i]); free(values[i]); }
    free(keys);
    free(values);
    free(copy);
}

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

static void dmarc_record_free(dmarc_record *r) {
    for (size_t i = 0; i < r->aggregate_count; i++) free(r->aggregate_uris[i]);
    free(r->aggregate_uris);
    for (size_t i = 0; i < r->forensic_count; i++) free(r->forensic_uris[i]);
    free(r->forensic_uris);
    for (size_t i = 0; i < r->warning_count; i++) free(r->warnings[i]);
    free(r->warnings);
    memset(r, 0, sizeof *r);
}

static bool dmarc_policy_from_name(const char *name, int *out) {
    if (strcmp(name, "none") == 0) { *out = DMARC_NONE; return true; }
    if (strcmp(name, "quarantine") == 0) { *out = DMARC_QUARANTINE; return true; }
    if (strcmp(name, "reject") == 0) { *out = DMARC_REJECT; return true; }
    return false;
}

/** Parse a `_dmarc` TXT record. Pure. */
static void parse_dmarc(const char *txt, dmarc_record *record) {
    memset(record, 0, sizeof *record);
    record->policy = -1;
    record->subdomain_policy = -1;
    record->percent = -1;
    record->dkim_alignment = 0;
    record->spf_alignment = 0;

    char *copy = xstrdup(txt);
    char **keys = NULL, **values = NULL;
    size_t tag_count = 0;
    char *p = copy;
    while (p) {
        char *next = strchr(p, ';');
        if (next) *next = '\0';
        char *term = trim_mut(p);
        size_t len = strlen(term);
        if (len >= 2 && term[0] == '"' && term[len - 1] == '"') {
            term[len - 1] = '\0';
            term++;
            term = trim_mut(term);
        }
        if (term[0] != '\0') {
            char *eq = strchr(term, '=');
            if (!eq || eq == term) {
                push_warning(&record->warnings, &record->warning_count,
                    "Malformed tag \"%s\" ignored.", term);
            } else {
                *eq = '\0';
                char *key = lowercase_dup(trim_mut(term));
                char *value = xstrdup(trim_mut(eq + 1));
                push_string(&keys, &tag_count, key);
                values = xrealloc(values, tag_count * sizeof(char *));
                values[tag_count - 1] = value;
            }
        }
        p = next ? next + 1 : NULL;
    }

    const char *version = tag_get(keys, values, tag_count, "v");
    if (!version) {
        record->valid = false;
        push_warning(&record->warnings, &record->warning_count,
            "No v= tag — this is not a DMARC record.");
        goto done;
    }
    if (!ci_equals(version, "DMARC1")) {
        record->valid = false;
        push_warning(&record->warnings, &record->warning_count,
            "Unknown version v=%s (expected DMARC1).", version);
        goto done;
    }

    {
        char *p_lower = NULL;
        const char *p_tag = tag_get(keys, values, tag_count, "p");
        if (!p_tag || p_tag[0] == '\0') {
            record->valid = false;
            push_warning(&record->warnings, &record->warning_count,
                "No p= policy tag — DMARC requires it.");
            goto done;
        }
        p_lower = lowercase_dup(p_tag);
        if (!dmarc_policy_from_name(p_lower, &record->policy)) {
            record->valid = false;
            push_warning(&record->warnings, &record->warning_count,
                "Invalid policy p=%s (expected none, quarantine, or reject).", p_lower);
            free(p_lower);
            goto done;
        }
        free(p_lower);
    }

    {
        const char *sp_tag = tag_get(keys, values, tag_count, "sp");
        if (sp_tag && sp_tag[0] != '\0') {
            char *sp_lower = lowercase_dup(sp_tag);
            int sp;
            if (dmarc_policy_from_name(sp_lower, &sp)) record->subdomain_policy = sp;
            else {
                push_warning(&record->warnings, &record->warning_count,
                    "Invalid sp=%s ignored (expected none, quarantine, or reject).", sp_lower);
            }
            free(sp_lower);
        }
    }

    {
        const char *rua = tag_get(keys, values, tag_count, "rua");
        if (rua && rua[0] != '\0') {
            char *rua_copy = xstrdup(rua);
            char **parts = NULL;
            size_t part_count = 0;
            split_all(rua_copy, ',', &parts, &part_count);
            for (size_t i = 0; i < part_count; i++) {
                if (parts[i][0]) push_string(&record->aggregate_uris, &record->aggregate_count, parts[i]);
                else free(parts[i]);
            }
            free(parts);
            free(rua_copy);
        }
        const char *ruf = tag_get(keys, values, tag_count, "ruf");
        if (ruf && ruf[0] != '\0') {
            char *ruf_copy = xstrdup(ruf);
            char **parts = NULL;
            size_t part_count = 0;
            split_all(ruf_copy, ',', &parts, &part_count);
            for (size_t i = 0; i < part_count; i++) {
                if (parts[i][0]) push_string(&record->forensic_uris, &record->forensic_count, parts[i]);
                else free(parts[i]);
            }
            free(parts);
            free(ruf_copy);
        }
    }

    {
        const char *pct = tag_get(keys, values, tag_count, "pct");
        if (pct) {
            char *end = NULL;
            long n = strtol(pct, &end, 10);
            if (end == pct || *end != '\0' || n < 0 || n > 100) {
                push_warning(&record->warnings, &record->warning_count,
                    "Invalid pct=%s ignored (must be 0-100).", pct);
            } else {
                record->percent = n;
            }
        }
    }

    {
        const char *adkim = tag_get(keys, values, tag_count, "adkim");
        if (adkim && adkim[0] != '\0') {
            if (strcmp(adkim, "r") == 0 || strcmp(adkim, "s") == 0) {
                record->dkim_alignment = adkim[0];
            } else {
                push_warning(&record->warnings, &record->warning_count,
                    "Invalid adkim=%s ignored (expected r or s).", adkim);
            }
        }
        const char *aspf = tag_get(keys, values, tag_count, "aspf");
        if (aspf && aspf[0] != '\0') {
            if (strcmp(aspf, "r") == 0 || strcmp(aspf, "s") == 0) {
                record->spf_alignment = aspf[0];
            } else {
                push_warning(&record->warnings, &record->warning_count,
                    "Invalid aspf=%s ignored (expected r or s).", aspf);
            }
        }
    }

    /* Policy guidance — mirrors the TS exactly. */
    if (record->policy == DMARC_NONE) {
        push_warning(&record->warnings, &record->warning_count,
            "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 (record->aggregate_count == 0) {
        push_warning(&record->warnings, &record->warning_count,
            "No rua= address — without aggregate reports you cannot see who is "
            "failing authentication. Add rua=mailto:reports@example.com.");
    } else if (record->forensic_count > 0) {
        push_warning(&record->warnings, &record->warning_count,
            "ruf= (forensic reports) is supported by few receivers and may leak "
            "message content to the report address (informational).");
    }
    if (record->percent >= 0 && record->percent < 100 && record->policy != DMARC_NONE) {
        push_warning(&record->warnings, &record->warning_count,
            "pct=%ld applies the policy to only %ld%% of mail — the other %ld%% is "
            "unaffected.", record->percent, record->percent, 100 - record->percent);
    }
    record->valid = true;

done:
    for (size_t i = 0; i < tag_count; i++) { free(keys[i]); free(values[i]); }
    free(keys);
    free(values);
    free(copy);
}

/* ------------------------------------------------- check-layer evaluation --- */

/** Status a parsed record maps to (the pure half of the TS check functions):
 *  warnings -> warn, otherwise pass. */
static check_status status_from_warnings(size_t warning_count) {
    return warning_count > 0 ? STATUS_WARN : STATUS_PASS;
}

static const char *status_name(check_status s) {
    return s == STATUS_PASS ? "pass" : s == STATUS_WARN ? "warn" : "fail";
}

/* ------------------------------------------------------------- demo main --- */

static void print_warnings(char **warnings, size_t count) {
    for (size_t i = 0; i < count; i++) printf("    - %s\n", warnings[i]);
}

int main(void) {
    /* SPF: one include, one ip4, ~all — healthy. */
    spf_record spf;
    parse_spf("\"v=spf1 include:_spf.google.com ip4:192.0.2.0/24 ~all\"", &spf);
    printf("spf:  valid=%d version=%s lookups=%zu mechanisms=%zu status=%s\n",
           spf.valid, spf.version ? spf.version : "-", spf.lookup_count, spf.mechanism_count,
           status_name(spf.valid ? status_from_warnings(spf.warning_count) : STATUS_FAIL));
    print_warnings(spf.warnings, spf.warning_count);

    /* DKIM: a 1024-bit RSA key — expect the "below recommended" warning. */
    dkim_record dkim;
    parse_dkim("v=DKIM1; k=rsa; s=email; h=sha256; "
               "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA"
               "wJaqNigTMVfPz8ZGfC6xc3vlzK9_LF9Ea_Y4O7dZ3uMLLpRy0iHw"
               "q3HpLQs8TvDdZCFEfjHn1MEuXh_V0LiPjA1YqNW_2pkQxIN_4f8S"
               "t9ExampleKeyTruncatedForDisplayPurposesOnly01234567"
               "89abcdefghijk3wIDAQAB", &dkim);
    printf("dkim: valid=%d key=%s bits=%ld hashes=%zu status=%s\n",
           dkim.valid, dkim.key_type, dkim.key_bits, dkim.hash_count,
           status_name(status_from_warnings(dkim.warning_count)));
    print_warnings(dkim.warnings, dkim.warning_count);

    /* DMARC: quarantine at 100% with aggregate reports — healthy. */
    dmarc_record dmarc;
    parse_dmarc("v=DMARC1; p=quarantine; rua=mailto:agg@example.com; adkim=s", &dmarc);
    printf("dmarc: valid=%d pct=%ld rua=%zu adkim=%c status=%s\n",
           dmarc.valid, dmarc.percent, dmarc.aggregate_count,
           dmarc.dkim_alignment ? (char)dmarc.dkim_alignment : '-',
           status_name(status_from_warnings(dmarc.warning_count)));
    print_warnings(dmarc.warnings, dmarc.warning_count);

    /* Domain normalization shared by all three checks. */
    const char *inputs[] = {
        "https://user@mail.example.com:8443/inbox?x=1", "Mailto:PERSON@Example.COM.",
    };
    for (size_t i = 0; i < 2; i++) {
        char *host = normalize_domain(inputs[i]);
        printf("normalize_domain(\"%s\") = \"%s\" domain-like=%d\n",
               inputs[i], host, is_domain_like(host));
        free(host);
    }

    spf_record_free(&spf);
    dkim_record_free(&dkim);
    dmarc_record_free(&dmarc);
    return 0;
}

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 →