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#hostnamenames 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": []
}
}