email-dns-tools/assets/scripts/records/SpfRecord.js
2026-01-19 09:51:04 +01:00

173 lines
4.5 KiB
JavaScript

import { ValidationError } from "../ValidationError.js";
import { Modifier } from "../spf/Modifier.js";
import { ValueRequirement } from "../spf/ValueRequirement.js";
import { DomainMechanism } from "../spf/DomainMechanism.js";
import { IPv4Mechanism } from "../spf/IPv4Mechanism.js";
import { IPv6Mechanism } from "../spf/IPv6Mechanism.js";
import { Mechanism } from "../spf/Mechanism.js";
import { VersionTerm } from "../spf/VersionTerm.js";
export class SpfRecord {
static fields = [
new VersionTerm("v", "spf1")
.required()
.hidden()
.pos(0),
new DomainMechanism("a")
.label("Domains")
.desc("Match the IP addresses from the A records of these domains (or current domain if none specified)")
.value(ValueRequirement.OPTIONAL)
.multiple()
.pos(1),
new DomainMechanism("mx")
.label("MX Records")
.desc("Match the IP addresses from the MX records of these domains (or current domain if none specified)")
.value(ValueRequirement.OPTIONAL)
.multiple()
.pos(2),
new DomainMechanism("ptr")
.disabled()
.value(ValueRequirement.OPTIONAL)
.multiple()
.pos(2),
new IPv4Mechanism("ip4")
.label("IPv4 addresses")
.desc("Match these IP addresses")
.multiple()
.pos(2),
new IPv6Mechanism("ip6")
.label("IPv6 addresses")
.desc("Match these IP addresses")
.multiple()
.pos(2),
new DomainMechanism("include")
.label("Include")
.desc("Check the SPF record of another domain. If it passes, return with the selected result")
.multiple()
.pos(1),
new DomainMechanism("exists")
.label("Exists")
.desc("Apply only if this domain exists (can be used with macro expansions)")
.category("advanced")
.multiple()
.pos(2),
new Mechanism("all")
.label("All others")
.desc("How to treat the rest of the IP addresses")
.value(ValueRequirement.PROHIBITED)
.pos(3),
new Modifier("redirect")
.label("Redirect")
.desc("Redirect to the SPF record of this domain if no matches were found")
.category("advanced")
.pos(4),
new Modifier("exp")
.label("Explanation")
.desc("Points to a domain whose TXT record contains an error message if validation fails. Macros can be used here")
.category("advanced")
.pos(4),
];
static categories = {
"advanced": "Advanced",
};
constructor(text) {
this.text = text;
}
static createFromFieldInputItems(items) {
const tokens = items
.filter(item => !item.field.isDisabled && item.isValid())
.map(item => item.toString());
const text = tokens.join(" ");
return new this(text);
}
tokenize() {
return this.text.trim().split(/\s+/);
}
getKeyValues() {
const result = [];
for (const token of this.tokenize()) {
const name = token.match(/^[-+?~]?(\w*)/)[1];
const term = this.constructor.fields.find(f => f.key === name);
if (!term) {
throw new ValidationError(name ? `Unknown term: ${name}` : "Syntax error");
}
const [directive, value] = token.split(term.separator);
const [, qualifier, key] = directive.match(/^([-+?~]?)(.*)/);
if (key !== name) {
throw new ValidationError(`Invalid separator for term: ${name}`);
}
if (token.includes(term.separator) && !value) {
throw new ValidationError(`No value specified for term: ${name}`);
}
result.push({ qualifier, key, value });
}
return result;
}
validate() {
const values = this.getKeyValues();
for (const term of this.constructor.fields) {
if (term.isRequired && !values.some(v => v.key === term.key)) {
throw new ValidationError(`Term "${term.key}" is required`);
}
}
let lastPos = 0;
for (let i = 0; i < values.length; i++) {
const input = values[i];
const term = this.constructor.fields.find(d => d.key === input.key);
if (!term) {
throw new ValidationError(input.key ? `Unknown term: ${input.key}` : "Syntax error");
}
if (term.position < lastPos) {
const lastDirective = this.constructor.fields.find(d => d.key === values[i-1].key);
throw new ValidationError(`Term "${lastDirective.key}" must come after "${term.key}"`);
}
if (!term.qualifierAllowed && input.qualifier) {
throw new ValidationError(`Term "${term.key}" must not have a qualifier`)
}
if (!input.value && term.valueRequirement === ValueRequirement.REQUIRED) {
throw new ValidationError(`Term "${term.key}" is missing a value`);
}
if (input.value && term.valueRequirement === ValueRequirement.PROHIBITED) {
throw new ValidationError(`Term "${term.key}" must not have a value`);
}
if (input.value) term.validate(input.value);
lastPos = term.position;
}
}
}