# How to Read DMARC Reports Before You Move to p=reject

Most domains that have DMARC at all are sitting on `p=none`. Someone added the record while setting up email, pointed `rua=` at a mailbox, and the reports have been arriving every day since: small zip and gzip attachments from Google, Outlook and other receivers, usually unopened.

Those files answer the one question you need settled before tightening the policy: who is sending mail with your domain in the From line, and would that mail survive `p=quarantine` or `p=reject`? Below is how to read them with nothing more than `dig`, `unzip` and a small XML tool, and how to move up the policy ladder without losing invoices or password resets on the way. If SPF and DKIM aren't set up yet, start with [how to set up business email on your own domain](https://blog.progineous.com/how-to-set-up-business-email/) and come back once those records exist.

## First, confirm what you've published

Reports describe how receivers judged your mail against your DNS records, so read those records the way a receiver would. Three lookups cover it:

```bash
dig +short TXT _dmarc.example.com
dig +short TXT example.com
dig +short TXT s1._domainkey.example.com
```

![Terminal screenshot of three dig lookups: the DMARC record at _dmarc.example.com showing v=DMARC1 with p=none and a rua reporting address, the SPF record for example.com ending in -all next to a site verification TXT record, and a 2048-bit DKIM public key for selector s1 split into two quoted strings](https://blog.progineous.com/wp-content/uploads/2026/10/read-dmarc-reports-dns-records.png)

The DMARC record lives at the `_dmarc` label, not on the domain itself. `p=none` means "report, but don't act", and `rua=` is where aggregate reports go. If that address is on a different domain, such as a reporting service, the other domain has to publish a small authorization record (`example.com._report._dmarc.reports.example.net` with the value `v=DMARC1`). Without it, receivers quietly skip sending.

On the domain itself, look for exactly one TXT record starting with `v=spf1`. Two of them make SPF return a permanent error, which counts as a fail for DMARC. Other TXT records, like site verification strings, can sit alongside it.

DKIM is the awkward one, because there's no way to list a domain's keys. You need the selector, which is the `s=` tag in the `DKIM-Signature` header of any message you've sent (use "show original" or "view source" in your mail client). Your mailbox provider uses one selector, and every other service that signs as your domain uses its own. A 2048-bit key is longer than the 255-character limit for a single TXT string, so `dig` prints it as two quoted chunks. That's normal. Receivers join them back together.

## Open one report

Reports arrive as attachments named after the receiver, your domain and the time window they cover:

```text
google.com!example.com!1792281600!1792367999.zip
enterprise.protection.outlook.com!example.com!1792281600!1792368000.xml.gz
```

The two long numbers are the start and end of the window in Unix time. `date -u -d @1792281600` turns the first one into midnight UTC on October 18. Some receivers zip the file and others gzip it, so `unzip` or `gunzip` gets you to the XML. After a short header naming the reporter and repeating the policy it found, there's one `<record>` per combination of sending IP and result. This one deserves a close look:

```xml
<record>
  <row>
    <source_ip>198.51.100.24</source_ip>
    <count>212</count>
    <policy_evaluated>
      <disposition>none</disposition>
      <dkim>fail</dkim>
      <spf>fail</spf>
    </policy_evaluated>
  </row>
  <identifiers>
    <header_from>example.com</header_from>
  </identifiers>
  <auth_results>
    <dkim>
      <domain>mailer.example.net</domain>
      <result>pass</result>
      <selector>m1</selector>
    </dkim>
    <spf>
      <domain>bounces.mailer.example.net</domain>
      <result>pass</result>
    </spf>
  </auth_results>
</record>
```

Read it in this order:

- **`source_ip` and `count`:** 212 messages from one IP, all showing `example.com` in the From header (`header_from`).
- **`auth_results`:** the raw checks. DKIM passed and so did SPF. On their own, they look fine.
- **`policy_evaluated`:** DMARC's verdict, and both say `fail`. DMARC only counts a pass when the domain that passed matches the From domain. DKIM was signed by `mailer.example.net` and SPF checked `bounces.mailer.example.net`. Neither is `example.com`, so neither is aligned.
- **`disposition`:** what the receiver actually did. `none`, because that's what the policy asked for. Under `p=reject`, those 212 messages would have bounced.

Passing in `auth_results` but failing in `policy_evaluated` is the pattern you'll see most, and it's nearly always a legitimate service you forgot about, not an attacker. Reports that follow the updated aggregate reporting spec (RFC 9990) may add an `xmlns` attribute and a few extra fields, but the record layout is the same.

## Turn a folder of reports into a sender list

One report is readable by eye. A month of them from a dozen receivers isn't, so pull out the useful columns with `xmlstarlet` (`apt install xmlstarlet` on Debian and Ubuntu, `brew install xmlstarlet` on a Mac):

```bash
unzip 'google.com!example.com!1792281600!1792367999.zip'
gunzip 'enterprise.protection.outlook.com!example.com!1792281600!1792368000.xml.gz'
xmlstarlet sel -t -m //record -v row/source_ip -o ' ' -v row/count \
  -o ' dkim=' -v row/policy_evaluated/dkim -o ' spf=' -v row/policy_evaluated/spf \
  -o ' signer=' -v auth_results/dkim/domain -n *.xml | sort -k2 -rn
```

Then ask DNS who each unfamiliar IP belongs to with a reverse lookup, for example `dig +short -x 198.51.100.24`.

![Terminal screenshot of extracting DMARC aggregate reports with unzip and gunzip, then an xmlstarlet one-liner listing each sending IP with its message count, aligned DKIM and SPF results and DKIM signer, followed by reverse DNS lookups that identify a newsletter platform, the company web server and an IP with no reverse DNS](https://blog.progineous.com/wp-content/uploads/2026/10/read-dmarc-reports-sender-list.png)

Every row lands in one of four buckets, and each bucket has its own fix:

1. **Your mailbox provider** (192.0.2.25 here). DKIM and SPF are both aligned. Nothing to do.
2. **A service sending on your behalf** (198.51.100.24, which reverse-resolves to a newsletter platform). It passes, but for the wrong domain. Most of these services have a "domain authentication" or "custom sending domain" setting that hands you one or two CNAME records, such as `m1._domainkey.example.com` pointing at their key. Once they sign with `d=example.com`, DKIM aligns and DMARC passes. Aligning SPF too, with a custom return-path on a subdomain, is nice to have but not required. One aligned pass is enough.
3. **Your own servers** (203.0.113.80, the web server). A contact form or shop sending straight from PHP, with no DKIM signature and an IP that isn't in SPF. Resist the urge to just add the IP to SPF. Send that mail through your mailbox provider's SMTP submission port with a real account instead, so it gets the same aligned signature as everything else.
4. **Nobody you recognize** (203.0.113.199, no reverse DNS, nine messages). That's someone using your domain without permission, and it's exactly the mail `p=reject` exists to stop. Leave it failing.

Expect a fuzzy fifth group as well: forwarded mail and mailing lists. A list that adds a footer breaks the DKIM signature, and a forwarder's IP was never in your SPF, so a small trickle of failures from university or ISP ranges is normal and can't be fixed from your side. Some large receivers use ARC to give that mail the benefit of the doubt. Don't chase it.

When the volume outgrows a shell one-liner, open-source parsers like parsedmarc can load reports into a dashboard, and hosted report services do the same job. The four buckets stay the same.

## Tighten in steps, not in one edit

![Flat diagram of a safe DMARC rollout in three stages, from p=none for monitoring to p=quarantine to p=reject, with the check to pass before each step: every legitimate sender aligned in the reports, nothing legitimate quarantined, and a lowered TTL on the _dmarc record so the change can be rolled back](https://blog.progineous.com/wp-content/uploads/2026/10/read-dmarc-reports-rollout.png)

Once every legitimate source in the last few weeks of reports is aligned, raise the policy one level at a time:

```text
v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@example.com
v=DMARC1; p=reject; sp=reject; rua=mailto:dmarc-reports@example.com
```

Older guides ramp up with `pct=10`, then `pct=25`, and so on. RFC 9989, the DMARC standard published in May 2026, retires `pct` because receivers applied the in-between values inconsistently. Its replacement is `t=y`, which asks receivers to apply one level below what you published, so `p=reject; t=y` behaves like quarantine at receivers that support it. Plain steps work everywhere: stay on quarantine for a couple of weeks, confirm that nothing legitimate shows up with a `quarantine` disposition, then move to reject.

A few habits keep each change uneventful:

- **Keep `rua=` for good.** Reports are how you'll spot the next forgotten sender, like a helpdesk or billing tool someone signs up for next year.
- **Lower the TTL on the `_dmarc` record** to 300 or 600 seconds before each change, so stepping back takes minutes rather than hours.
- **Decide on subdomains deliberately.** `sp=` sets the policy for existing subdomains, which otherwise inherit `p`. Receivers that follow RFC 9989 also read `np=reject`, which covers subdomains that don't exist at all.
- **Lock down domains that never send mail.** A parked domain can publish `v=spf1 -all` and `v=DMARC1; p=reject` on day one. There's nothing to monitor.

After each step, send a test message from every service in bucket two and check that the `Authentication-Results` header on the received copy shows `dmarc=pass`.

## What reject actually buys you

At `p=reject`, receivers that honor DMARC refuse mail that claims to be from your domain and can't prove it. Phishing that copies your exact address stops reaching customers' inboxes, and your own mail stops sharing a reputation with whoever was spoofing it. Look-alike domains are a separate problem, and DMARC doesn't cover them.

None of this needs special software. A few weeks of reading the reports, fixing the senders you recognize, and reject becomes a short DNS edit instead of a gamble. If you're moving mailboxes as part of the cleanup, Pro Gineous [business email](https://progineous.com/business-email.php?utm_source=hashnode) gives each address a mailbox on your own domain, and every check above works the same there as with any other provider.

### Related reading

- [Why Is My Email Going to Spam?](https://blog.progineous.com/how-to-keep-email-out-of-spam/)
- [How to Read dig Output When DNS Looks Wrong](https://blog.progineous.com/read-dig-output-dns/)
- [Setting Up DNS Records for Your Website and Mail](https://blog.progineous.com/how-to-set-up-dns-records/)

*Originally published on the [Pro Gineous blog](https://blog.progineous.com/read-dmarc-reports/).*

