mlocati/spf-lib
A PHP library for Sender Policy Framework (SPF) email authentication. Parse and validate SPF DNS TXT records, inspect mechanisms and modifiers, perform DNS lookups, and evaluate whether sending IPs are authorized. Useful for mail deliverability, diagnostics, and SPF tooling.
composer require mlocati/spf-lib
No service provider is auto-discovered — it's a framework-agnostic PHP library, so nothing registers itself in config/app.php. You'll usually consume it directly from an action, console command, or custom validation rule.
Where to look first:
SPF\Record — the central value object. Record::fromString('v=spf1 ...') parses, $record->toString() renders back to SPF syntax.SPF\Builder — fluent helpers to construct records (Builder::buildTerm(), Builder::buildModifier()).SPF\Term\* and SPF\Modifier\* — concrete mechanisms (A, Mx, Ip4, Ip6, Include, Ptr, Exists, All) and modifiers (Redirect, Explanation).SPF\Validator\* — record validation, including the 10-DNS-lookup and 2-void-lookup budget.First use case — validate a user-supplied SPF string in a form:
use SPF\Record;
use SPF\Validator\RecordValidator;
try {
$record = Record::fromString($request->string('spf'));
(new RecordValidator())->validate($record);
} catch (\Throwable $e) {
return back()->withErrors(['spf' => $e->getMessage()]);
}
A second common entry point is building a record from settings (e.g., a domains admin page that outputs v=spf1 include:_spf.google.com ~all).
Wrap it in a service. Bind a small SpfService in a service provider so you can inject it and swap it in tests:
final class SpfService
{
public function parse(string $raw): Record { return Record::fromString($raw); }
public function build(array $terms): string { /* Builder calls */ }
public function validate(string $raw): void { /* parse + validate */ }
}
Custom validation rule. This is the highest-value day-to-day integration for Laravel:
final class SpfRecord implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
try {
(new RecordValidator())->validate(Record::fromString((string) $value));
} catch (\Throwable $e) {
$fail("Invalid SPF record: {$e->getMessage()}");
}
}
}
DNS fetch is your job, not the library's. The library parses and validates syntax and semantics, but it won't resolve a domain for you. Pair it with spatie/dns or dns_get_record():
$txt = collect(dns_get_record($domain, DNS_TXT))
->pluck('txt')
->first(fn ($t) => str_starts_with($t, 'v=spf1'));
$record = Record::fromString($txt);
$validator->validate($record); // checks lookup budget, duplicates, invalid modifiers
Build vs parse. For an admin UI that lets users toggle "include Google / Microsoft", construct terms and let toString() handle qualifier serialization and ordering. For auditing a customer domain, prefer fromString() — never round-trip through your own builder if you need an exact match.
Store the raw string, re-parse on demand. Persist v=spf1 ... in a string column. Re-parsing is cheap, and you avoid schema churn when the library adds terminology.
Compose with existing audit flows. A queued job that fetches + parses + validates is the natural shape: FetchDnsSpfJob → SpfAuditJob → notification. Emit structured results (@mechanisms, @dnsLookupsUsed) so a dashboard can render them.
Include terms to actually follow DNS. The validator uses a lookup provider (implement SPF\DNSLookup-style classes) but the default wiring is limited; for anything beyond static validation you'll supply your own resolver that returns TXT records on demand.v=spf1 TXT records is a permerror. Check for that before calling the validator; the library parses one record, not a whole domain's TXT set.Record::fromString() throws on malformed input, not just returns false. Always wrap in try/catch — the exception hierarchy distinguishes parse errors from semantic ones.include, a, mx, ptr, exists, or redirect accounting.redirect terminates evaluation — terms after it are ignored. If you're generating SPF for users, either use redirect or all, not both, and never +all.%{d}, %{i}, etc.) are parsed but not expanded by this library. If you're auditing, you'll see the literal token; if you need expansion, do it before/after via your own resolver.ip4:10.0.0.0/8 is fine; ip4:10.0.0.0/33 or a hostname in an ip4: term will throw. Catch and surface to the form.+ (pass) is implicit — a term with no qualifier means pass. The library models this via a Qualifier enum/class; when reading ->getQualifier() back it returns the explicit qualified value, so don't assume null.SPF\Term\* for custom mechanisms and SPF\Modifier\Redirect/Explanation for custom modifiers, then register them with your Parser. Useful for internal RFC-compliant extensions, rare in Laravel apps.^x.y in composer.json and add a unit test that parses a canonical record — the API is stable but the exception hierarchy has shifted across majors.Record. The value objects serialize deterministically, which makes golden-file assertions in Pest/PHPUnit clean.How can I help you explore Laravel packages today?