cbowofrivia / dmarc-record-builder
A lean package that makes creating DMARC records user friendly
Package info
github.com/cbowofrivia/dmarc-record-builder
pkg:composer/cbowofrivia/dmarc-record-builder
Requires
- php: ^8.2
- illuminate/collections: ^12.0
- webmozart/assert: ^1.10
Requires (Dev)
- laravel/pint: ^1.15
- pestphp/pest: ^3
- rector/rector: ^2.2
- symfony/var-dumper: ^7.0
- dev-main
- v3.0.1
- v3.0.0
- 2.1.0
- 2.0.0
- 1.0.1
- 1.0.0
- 1.0.0-rc
- dev-dependabot/github_actions/dependabot/fetch-metadata-3.0.0
- dev-dependabot/github_actions/codecov/codecov-action-6
- dev-feat/validate-reporting-values
- dev-fix/parse-unknown-tags
- dev-fix/np-tag-serialisation
- dev-cbowofrivia/dmarcbis-support
- dev-dmarcbis
- dev-cbowofrivia-php8.4-support
- dev-cbow/feature--add-parse-functionality
This package is auto-updated.
Last update: 2026-04-02 10:23:43 UTC
README
A PHP package for building and parsing DMARC DNS records with a fluent, human-friendly API. Accepts readable values ('relaxed', 'strict', 'reject') and outputs correctly formatted DMARC strings.
Requirements
- PHP 8.2 or higher
Installation
composer require cbowofrivia/dmarc-record-builder
Quick Start
use CbowOfRivia\DmarcRecordBuilder\DmarcRecord; $record = (string) (new DmarcRecord()) ->policy('reject') ->rua('mailto:dmarc@example.com'); // v=DMARC1; p=reject; rua=mailto:dmarc@example.com
Building a Record
Fluent API
All setter methods return $this, so calls can be chained:
$record = new DmarcRecord(); $record->policy('reject') ->subdomainPolicy('quarantine') ->pct(100) ->rua('mailto:dmarc@example.com') ->ruf('mailto:dmarc-forensic@example.com') ->adkim('relaxed') ->aspf('strict') ->reporting(['dkim', 'spf']) ->interval(86400) ->nonExistentSubdomainPolicy('reject') ->publicSuffixDomainPolicy('y') ->testingMode('n'); echo $record; // v=DMARC1; p=reject; sp=quarantine; pct=100; rua=mailto:dmarc@example.com; ruf=mailto:dmarc-forensic@example.com; adkim=r; aspf=s; fo=d:s; ri=86400; np=reject; psd=y; t=n
Constructor
All parameters are optional. The constructor accepts the same values as the fluent setters:
$record = new DmarcRecord( version: 'DMARC1', policy: 'reject', subdomain_policy: 'quarantine', pct: 100, rua: 'mailto:dmarc@example.com', ruf: 'mailto:dmarc-forensic@example.com', adkim: 'relaxed', aspf: 'strict', reporting: ['dkim', 'spf'], interval: 86400, np: 'reject', psd: 'y', t: 'n', );
Static Factory
DmarcRecord::create() is a convenience wrapper around the constructor, useful when you want to build and cast in one expression:
$record = (string) DmarcRecord::create( policy: 'quarantine', pct: 20, rua: 'mailto:dmarc@example.com', ); // v=DMARC1; p=quarantine; pct=20; rua=mailto:dmarc@example.com
Parsing an Existing Record
Pass a raw DMARC TXT record string to DmarcRecord::parse() to get a populated DmarcRecord object. This is useful for reading and modifying existing records.
$record = DmarcRecord::parse('v=DMARC1; p=quarantine; adkim=r; aspf=s; fo=d:s'); $record->policy; // 'quarantine' $record->adkim; // 'relaxed' (translated from 'r') $record->aspf; // 'strict' (translated from 's') $record->reporting; // ['dkim', 'spf'] (translated from 'fo=d:s')
After parsing, you can modify the record and re-cast it to a string:
$record = DmarcRecord::parse('v=DMARC1; p=none; rua=mailto:dmarc@example.com'); $record->policy('reject'); echo $record; // v=DMARC1; p=reject; rua=mailto:dmarc@example.com
parse() requires both v and p tags to be present and will throw an InvalidArgumentException if either is missing. Unknown tags are silently ignored.
Tag Reference
| Method | DMARC Tag | Accepted Values | Default |
|---|---|---|---|
version() |
v |
'DMARC1' |
'DMARC1' |
policy() |
p |
'none', 'quarantine', 'reject' |
'none' |
subdomainPolicy() |
sp |
'none', 'quarantine', 'reject' |
null |
pct() |
pct |
Integer | null |
rua() |
rua |
'mailto:...' |
null |
ruf() |
ruf |
'mailto:...' |
null |
adkim() |
adkim |
'relaxed', 'strict' |
null |
aspf() |
aspf |
'relaxed', 'strict' |
null |
reporting() |
fo |
'all', 'any', 'dkim', 'spf' |
[] |
interval() |
ri |
Integer (seconds) | null |
nonExistentSubdomainPolicy() |
np |
'none', 'quarantine', 'reject' |
null |
publicSuffixDomainPolicy() |
psd |
'y', 'n', 'u' |
null |
testingMode() |
t |
'y', 'n' |
null |
Tags with a null value are omitted from the output string. Only v and p are always emitted.
Tag Details
policy() / subdomainPolicy() / nonExistentSubdomainPolicy()
Controls how the receiving mail server handles messages that fail DMARC checks.
'none'— take no action; useful during monitoring'quarantine'— send to spam/junk'reject'— reject the message outright
subdomainPolicy() (sp) overrides policy() for subdomains. If omitted, subdomains inherit the main policy.
nonExistentSubdomainPolicy() (np) applies to non-existent subdomains (RFC 9091 / DMARCbis). Takes precedence over both policy() and subdomainPolicy() for those domains.
pct()
The percentage of messages the policy is applied to (1–100). Useful for gradual rollout. Omit to apply the policy to all messages.
rua() / ruf()
URIs for receiving DMARC reports. Must be prefixed with mailto:.
rua— aggregate reports (daily summaries from receivers)ruf— forensic/failure reports (per-message failure details; not all receivers send these)
->rua('mailto:dmarc@example.com') ->ruf('mailto:dmarc-forensic@example.com')
adkim() / aspf()
Alignment mode for DKIM and SPF respectively.
'relaxed'— the organisational domain must match (e.g.mail.example.comaligns withexample.com)'strict'— the domains must match exactly
Omitting either defaults to relaxed alignment per the RFC.
reporting()
Specifies which failure conditions trigger a forensic report. Accepts a string or an array of options:
| Value | fo tag |
Meaning |
|---|---|---|
'all' |
fo=0 |
Report if all mechanisms fail |
'any' |
fo=1 |
Report if any mechanism fails |
'dkim' |
fo=d |
Report if DKIM fails |
'spf' |
fo=s |
Report if SPF fails |
Multiple values produce a colon-separated fo tag:
->reporting(['dkim', 'spf']) // fo=d:s ->reporting(['any']) // fo=1
Duplicate values are silently deduplicated. 'all' and 'any' are mutually exclusive — passing both throws an InvalidArgumentException.
interval()
How often (in seconds) receivers should send aggregate reports. The RFC default is 86400 (24 hours).
publicSuffixDomainPolicy()
Controls DMARC policy application at public suffix domains (DMARCbis extension).
'y'— apply DMARC policy to this public suffix domain'n'— do not apply policy to this public suffix domain'u'— undefined / unknown
testingMode()
When set to 'y', signals that DMARC is in testing mode. Receivers should not apply policy actions but may still send reports.
Validation
The package validates inputs on each setter call. Passing an invalid value throws an InvalidArgumentException from webmozart/assert.
// Throws: Expected one of: "none", "quarantine", "reject", NULL. Got: "monitor" $record->policy('monitor'); // Throws: rua mailto address should start with "mailto:" $record->rua('dmarc@example.com'); // Throws: Expected one of: "relaxed", "strict", NULL. Got: "loose" $record->adkim('loose'); // Throws: Reporting options "all" (0) and "any" (1) are mutually exclusive. $record->reporting(['all', 'any']);
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Credits
License
The MIT License (MIT). Please see License File for more information.