The bounce usually comes back from an Outlook.com or Microsoft 365 recipient and reads like this:
Remote server returned '554 5.7.5 Permanent error evaluating DMARC policy'
It is not a spam verdict, and the content of the message has nothing to do with it. The receiving server went to read the DMARC record of the domain in your From address, got something it could not evaluate, and refused the message instead of guessing. "Permanent" is literal: resending changes nothing until the record changes.
What breaks a record
Three of the causes below come from a Microsoft moderator answering the same question on Microsoft Q&A, who names stray quotation marks, extra characters and a missing semicolon. The other two come from how a receiver finds and reads the record.
Two records on _dmarc. A second TXT record that starts with v=DMARC1, left behind by an older setup or added by a second provider's guide. The parser gets two policies for one name and no rule for choosing between them. The current standard tells a receiver to discard both and keep looking at the parent domain. Whatever happens next depends on what that parent publishes, and a receiver that stops at the conflict has nothing left to evaluate.
Quotes inside the value. Depending on the panel, quotation marks you paste can end up inside the value. A record copied from a web page with curly quotes, “v=DMARC1; p=none”, arrives with the quotes as part of the text, and a value that starts with “ does not start with v=DMARC1.
Anything before v=DMARC1. The version tag has to come first. A leading space, an invisible character pasted from a document, or another tag moved in front of it is enough.
Tags without semicolons between them. v=DMARC1 p=quarantine reads as a single tag with an odd value, and the policy never appears.
Values the standard does not know. p=block or p=quarantined instead of none, quarantine or reject.
Finding the broken part
Read the raw record:
dig +short TXT _dmarc.example.com
Count the lines first: more than one starting with v=DMARC1 is the whole answer. Then read the one line character by character, quotes and leading spaces included.
If the From domain is a subdomain such as news.example.com and it has no record of its own, receivers fall back to the record of example.com. The broken record may be the parent's, published by somebody else years ago.
The DMARC checker does the same reading and names what it finds: several records, a policy value it does not know, a tag given twice, a report address that is not a valid mailto:.
The fix
One record, plain text, the version first and every tag ended with a semicolon:
v=DMARC1; p=none; rua=mailto:dmarc@example.com;
Delete the duplicate rather than editing both. Wait for the old record's TTL to run out, check again with dig, and send a new test message to the recipient that bounced. Mail already refused stays refused; it has to be sent again.
Not the same as "rejected per DMARC policy"
A bounce that says 550 5.7.1 Email rejected per DMARC policy means the opposite case. The record was read without trouble, the message failed the check, and the policy you published told the receiver to refuse it. With the record fine, what needs work is getting SPF or DKIM to pass for the From domain, which the DMARC fail page goes through.
How the test reports it
The full test reports DMARC the way its own receiving server evaluated it, with the record text quoted beside the result, so a send after the fix shows whether a receiver now reads the record. It does not check the record's grammar. That job belongs to the checker above.