CLI

usage: checkdmarc [-h] [-p] [--ns NS [NS ...]] [--mx MX [MX ...]] [-d] [-f FORMAT]
                  [-o OUTPUT [OUTPUT ...]] [-n NAMESERVER [NAMESERVER ...]] [-t TIMEOUT]
                  [--retries RETRIES] [-b BIMI_SELECTOR] [-v] [-w WAIT] [--check-mx-tls]
                  [--skip-tls] [--debug]
                  domain [domain ...]

Validates and parses email-related DNS records

positional arguments:
  domain                one or more domains, or a single path to a file containing a
                        list of domains

options:
  -h, --help            show this help message and exit
  -p, --parked          indicate that the domains are parked
  --ns NS [NS ...], --approved-ns NS [NS ...]
                        approved nameserver substrings
  --mx MX [MX ...], --approved-mx MX [MX ...]
                        approved MX hostname substrings
  -d, --descriptions    include descriptions of tags in the JSON output
  -f FORMAT, --format FORMAT
                        specify JSON or CSV screen output format (default json)
  -o OUTPUT [OUTPUT ...], --output OUTPUT [OUTPUT ...]
                        one or more file paths to output to (must end in .json or .csv)
                        (silences screen output)
  -n NAMESERVER [NAMESERVER ...], --nameserver NAMESERVER [NAMESERVER ...], --nameservers NAMESERVER [NAMESERVER ...]
                        nameservers to query: IP addresses, https:// URLs (DNS over
                        HTTPS), and/or tls://ip[:port][#hostname] (DNS over TLS)
                        (default: the system-configured resolvers). For reliability,
                        passing a mix of public resolvers is recommended, e.g. 1.1.1.1
                        8.8.8.8
  -t TIMEOUT, --timeout TIMEOUT
                        number of seconds to wait for an answer from DNS (default 2.0)
  --retries RETRIES     number of times to retry on timeout or other transient errors
                        (default 0)
  -b BIMI_SELECTOR, --bimi-selector BIMI_SELECTOR
                        the BIMI selector to use (default "default")
  -v, --version         show program's version number and exit
  -w WAIT, --wait WAIT  number of seconds to wait between checking domains (default 0.0)
  --check-mx-tls        test MX hosts for STARTTLS and TLS support
  --skip-tls            deprecated, no effect: TLS testing is off unless --check-mx-tls
                        is given
  --debug               enable debugging output

Best practice: DNS resolvers

By default, checkdmarc queries the nameservers your operating system is configured to use (/etc/resolv.conf on Linux/macOS, the OS resolver on Windows). That keeps behavior predictable for environments that rely on split-horizon or internal DNS.

For public-internet checks, passing a mix of public resolvers from different providers is recommended. It gives you cross-provider failover out of the box — if one provider’s anycast path is slow or its resolver is incompatible with a given authoritative server (e.g. Cloudflare’s QNAME minimization with certain auth servers), the query falls through to the next provider within ~1 second instead of timing out.

A nameserver that fails to answer — a timeout or other transport error — is moved behind the other configured nameservers for the next 60 seconds (set the DNS_NAMESERVER_FAILURE_COOLDOWN_SECONDS environment variable to change this), so when checking many domains an unreachable resolver costs one timeout per minute rather than one per query. It is still tried when the others fail, and gets another turn in front once the cool-down expires.

On the CLI:

checkdmarc -n 1.1.1.1 8.8.8.8 proton.me

In the API, the recommended pair is exposed as checkdmarc.RECOMMENDED_DNS_NAMESERVERS:

from checkdmarc import check_domains, RECOMMENDED_DNS_NAMESERVERS

results = check_domains(
    ["proton.me"],
    nameservers=RECOMMENDED_DNS_NAMESERVERS,
)

Pick providers that make sense for your threat model and jurisdiction; Cloudflare (1.1.1.1), Google (8.8.8.8), and Quad9 (9.9.9.9) are all reasonable starting points.

Encrypted DNS

Every entry in the nameservers list picks its own transport:

  • An IP address — plain DNS over UDP and TCP port 53, the default and the behavior of every earlier release.

  • An https:// URL — DNS over HTTPS (DoH).

  • tls://ip[:port][#hostname] — DNS over TLS (DoT). The port defaults to 853, and the optional #hostname names the TLS certificate identity of the server (SNI), matching systemd-resolved’s syntax. The host itself must be an IP address, and an IPv6 address must be wrapped in brackets, so that its colons cannot be mistaken for the port separator — tls://[2620:fe::fe]#dns.quad9.net.

Forms can be mixed, and are tried in the order given:

checkdmarc -n https://cloudflare-dns.com/dns-query tls://9.9.9.9#dns.quad9.net example.com

On a network where outbound DNS is blocked but an HTTP proxy is available, configure DoH nameservers and set the standard proxy environment variables:

export HTTPS_PROXY=http://proxy.example.net:3128

checkdmarc’s DoH queries honor HTTP_PROXY, HTTPS_PROXY, and NO_PROXY, and the proxy resolves the DoH server’s own hostname on checkdmarc’s behalf, so such a deployment needs no access to UDP port 53 at all.

If the proxy inspects TLS, point the standard SSL_CERT_FILE environment variable at your organization’s CA bundle so its certificate is trusted:

export SSL_CERT_FILE=/etc/ssl/certs/corporate-ca.pem

Note

checkdmarc’s DoT connections are made directly to TCP port 853 and do not use a proxy. Use DoH on a proxy-only network. Note also that the STARTTLS test (opt-in via --check-mx-tls) needs a direct connection to each MX host on port 25, so leave it off where that is blocked.

Example

checkdmarc proton.me
{
  "domain": "proton.me",
  "base_domain": "proton.me",
  "dnssec": true,
  "soa": {
    "record": "ns1.proton.me. support.proton.me. 2025091157 1200 144 1814400 7200",
    "values": {
      "primary_nameserver": "ns1.proton.me",
      "rname_email_address": "support@proton.me",
      "serial": 2025091157,
      "refresh": 1200,
      "retry": 144,
      "expire": 1814400,
      "minimum": 7200
    }
  },
  "ns": {
    "hostnames": [
      "ns1.proton.me",
      "ns2.proton.me",
      "ns3.proton.me"
    ],
    "warnings": []
  },
  "mx": {
    "hosts": [
      {
        "preference": 10,
        "hostname": "mail.protonmail.ch",
        "addresses": [
          "176.119.200.128",
          "185.205.70.128",
          "185.70.42.128"
        ],
        "dnssec": true,
        "tlsa": [
          "3 1 1 6111a5698d23c89e09c36ff833c1487edc1b0c841f87c49dae8f7a09e11e979e",
          "3 1 1 76bb66711da416433ca890a5b2e5a0533c6006478f7d10a4469a947acc8399e1"
        ]
      },
      {
        "preference": 20,
        "hostname": "mailsec.protonmail.ch",
        "addresses": [
          "176.119.200.129",
          "185.205.70.129",
          "185.70.42.129"
        ],
        "dnssec": true,
        "tlsa": [
          "3 1 1 6111a5698d23c89e09c36ff833c1487edc1b0c841f87c49dae8f7a09e11e979e",
          "3 1 1 76bb66711da416433ca890a5b2e5a0533c6006478f7d10a4469a947acc8399e1"
        ]
      }
    ],
    "warnings": []
  },
  "mta_sts": {
    "valid": true,
    "id": "190906205100Z",
    "policy": {
      "version": "STSv1",
      "mode": "enforce",
      "max_age": 604800,
      "mx": [
        "mail.protonmail.ch",
        "mailsec.protonmail.ch"
      ]
    },
    "warnings": []
  },
  "spf": {
    "record": "v=spf1 include:_spf.protonmail.ch ~all",
    "valid": true,
    "dns_lookups": 2,
    "void_dns_lookups": 0,
    "warnings": [],
    "parsed": {
      "mechanisms": [
        {
          "mechanism": "include",
          "value": "_spf.protonmail.ch",
          "record": "v=spf1 ip4:185.70.40.0/24 ip4:185.70.41.0/24 ip4:185.70.43.0/24 ip4:79.135.106.0/24 ip4:79.135.107.0/24 ip4:109.224.244.0/24 include:_spf2.protonmail.ch ~all",
          "dns_lookups": 2,
          "void_dns_lookups": 0,
          "parsed": {
            "mechanisms": [
              {
                "mechanism": "ip4",
                "value": "185.70.40.0/24",
                "action": "pass"
              },
              {
                "mechanism": "ip4",
                "value": "185.70.41.0/24",
                "action": "pass"
              },
              {
                "mechanism": "ip4",
                "value": "185.70.43.0/24",
                "action": "pass"
              },
              {
                "mechanism": "ip4",
                "value": "79.135.106.0/24",
                "action": "pass"
              },
              {
                "mechanism": "ip4",
                "value": "79.135.107.0/24",
                "action": "pass"
              },
              {
                "mechanism": "ip4",
                "value": "109.224.244.0/24",
                "action": "pass"
              },
              {
                "mechanism": "include",
                "value": "_spf2.protonmail.ch",
                "record": "v=spf1 ip4:85.9.206.169 ip4:85.9.210.45 ip4:188.165.51.139 ip4:57.129.93.249 ~all",
                "dns_lookups": 1,
                "void_dns_lookups": 0,
                "parsed": {
                  "mechanisms": [
                    {
                      "mechanism": "ip4",
                      "value": "85.9.206.169",
                      "action": "pass"
                    },
                    {
                      "mechanism": "ip4",
                      "value": "85.9.210.45",
                      "action": "pass"
                    },
                    {
                      "mechanism": "ip4",
                      "value": "188.165.51.139",
                      "action": "pass"
                    },
                    {
                      "mechanism": "ip4",
                      "value": "57.129.93.249",
                      "action": "pass"
                    }
                  ],
                  "redirect": null,
                  "exp": null,
                  "ra": null,
                  "rp": null,
                  "rr": null,
                  "all": "softfail"
                },
                "warnings": []
              }
            ],
            "redirect": null,
            "exp": null,
            "ra": null,
            "rp": null,
            "rr": null,
            "all": "softfail"
          },
          "warnings": []
        }
      ],
      "redirect": null,
      "exp": null,
      "ra": null,
      "rp": null,
      "rr": null,
      "all": "softfail"
    }
  },
  "dmarc": {
    "record": "v=DMARC1; p=quarantine; fo=1; aspf=s; adkim=s;",
    "valid": true,
    "location": "proton.me",
    "warnings": [
      "rua tag (destination for aggregate reports) not found."
    ],
    "tags": {
      "v": {
        "value": "DMARC1",
        "explicit": true
      },
      "p": {
        "value": "quarantine",
        "explicit": true
      },
      "fo": {
        "value": "1",
        "explicit": true
      },
      "aspf": {
        "value": "s",
        "explicit": true
      },
      "adkim": {
        "value": "s",
        "explicit": true
      },
      "sp": {
        "value": "quarantine",
        "explicit": false
      }
    }
  },
  "smtp_tls_reporting": {
    "valid": true,
    "tags": {
      "v": {
        "value": "TLSRPTv1"
      },
      "rua": {
        "value": [
          "https://reports.proton.me/reports/smtptls"
        ]
      }
    },
    "warnings": []
  },
  "bimi": {
    "record": "v=BIMI1; l=; a=;",
    "valid": true,
    "selector": "default",
    "location": "proton.me",
    "tags": {
      "v": {
        "value": "BIMI1"
      },
      "l": {
        "value": ""
      },
      "a": {
        "value": ""
      }
    },
    "warnings": []
  }
}