first commit
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# spf-checker
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Implements the SPF (RFC 7208) evaluation algorithm: parses the
|
||||
record, walks each mechanism in order, follows `include:` and
|
||||
`redirect=` with bounded depth and lookup limits, and matches the
|
||||
candidate IPv4 / IPv6 address against `ip4:` and `ip6:` CIDR ranges.
|
||||
The DNS resolver is fully injectable so the package can be embedded
|
||||
in browsers, edge workers, or sandboxed runtimes that need to route
|
||||
lookups through DoH or a service mesh.
|
||||
|
||||
## Features
|
||||
|
||||
- Parses every mechanism token (`ip4`, `ip6`, `a`, `mx`, `include`,
|
||||
`all`, `exists`, `redirect`) and their qualifier prefixes
|
||||
(`+ - ~ ?`).
|
||||
- Bounded recursion: `MAX_INCLUDE_DEPTH = 10` and `MAX_DNS_LOOKUPS =
|
||||
10` to surface RFC 7208 §4.6.4 permerror conditions instead of
|
||||
hanging.
|
||||
- IPv4 matching with `Number.parseInt` + bitmasks; IPv6 matching by
|
||||
expanding `::` shorthand and comparing bit prefixes.
|
||||
- Records `email_processing.spf.result_total` attributed by `result`
|
||||
for every outcome.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { checkSpf } from "@suaveplan/email-processing/spf";
|
||||
|
||||
const out = await checkSpf(senderIp, fromDomain, async (d) => {
|
||||
const recs = await dns.resolveTxt(d);
|
||||
return recs.map((parts) => parts.join(""));
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { checkSpf, parseSpfRecord } from "@suaveplan/email-processing/spf";
|
||||
|
||||
const recs = await dnsResolver(domain);
|
||||
const v = recs.find((r) => r.startsWith("v=spf1"));
|
||||
if (v) {
|
||||
const mechs = parseSpfRecord(v);
|
||||
if (mechs.some((m) => m.type === "all" && m.qualifier === "+")) {
|
||||
log.warn({ domain }, "open SPF policy (+all)");
|
||||
}
|
||||
}
|
||||
const out = await checkSpf(ip, domain, dnsResolver);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `parseSpfRecord(record): SpfMechanism[]`.
|
||||
- `ipv4Matches(ip, cidr): boolean`.
|
||||
- `ipv6Matches(ip, cidr): boolean`.
|
||||
- `checkSpf(ip, domain, resolver, depth?, lookupCount?): Promise<SpfCheckResult>`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`a` / `mx` / `exists` mechanisms are recognised but treated as
|
||||
non-matches because resolving them requires additional DNS lookups
|
||||
the caller may not want to fund — most real-world SPF records lean on
|
||||
`ip4:` / `ip6:` / `include:` anyway. The `lookupCount` parameter is
|
||||
exposed so callers can compose `checkSpf` across multiple top-level
|
||||
domains in a single run without re-instantiating the budget. The
|
||||
function records the SPF outcome to the OTel counter before returning,
|
||||
including the `permerror` paths, so dashboards can distinguish "bad
|
||||
record" from "no record".
|
||||
Reference in New Issue
Block a user