email-dns-tools/assets/scripts/records/SpfRecord.js

167 lines
4.4 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()
.pos(0),
new DomainMechanism("include")
.label("Include")
.desc("Also apply the SPF records from these domains")
.multiple()
.pos(1),
new DomainMechanism("a")
.label("Domains")
.desc("Select the IP address from these domains (or current domain if none specified)")
.value(ValueRequirement.OPTIONAL)
.multiple()
.pos(1),
new DomainMechanism("mx")
.label("MX Records")
.desc("Select 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")
.hidden()
.value(ValueRequirement.OPTIONAL)
.multiple()
.pos(2),
new IPv4Mechanism("ipv4")
.label("IPv4 addresses")
.desc("Select these IP addresses")
.multiple()
.pos(2),
new IPv6Mechanism("ipv6")
.label("IPv6 addresses")
.desc("Select these IP addresses")
.multiple()
.pos(2),
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 IP addresses matched")
.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;
}
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(`Unknown term: ${name}`);
}
const [directive, value] = token.split(term.separator);
const [, qualifier, key] = directive.match(/^([-+?~]?)(\w*)/);
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(`Unknown term: ${input.key}`);
}
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 instanceof Modifier && input.qualifier) {
throw new ValidationError(`Modifier "${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;
}
}
static fieldsToString() {
const tokens = this.fields
.filter(field => !field.isHidden)
.filter(field => field.valueRequirement === ValueRequirement.REQUIRED ? field.getInputValue() : field.getInputQualifier())
.map(field =>
field.getInputQualifier()
+ field.key
+ (field.getInputValue() ? field.separator + field.getInputValue() : "")
);
return tokens.join(" ");
}
}