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

168 lines
4.0 KiB
JavaScript

import { Mechanism } from "../spf/Mechanism.js";
import { ValidationError } from "../ValidationError.js";
import { Modifier } from "../spf/Modifier.js";
import { ValueRequirement } from "../spf/ValueRequirement.js";
export class SpfRecord {
static fields = [
new Modifier("v")
.required()
.validate((key, val) => {
if (val !== "spf1") throw new ValidationError(`Version must be "spf1"`);
return true;
})
.pos(0),
new Mechanism("include")
.validate(this.validateDomain)
.multiple()
.pos(1),
new Mechanism("a")
.value(ValueRequirement.OPTIONAL)
.validate(this.validateDomain)
.multiple()
.pos(1),
new Mechanism("mx")
.value(ValueRequirement.OPTIONAL)
.validate(this.validateDomain)
.multiple()
.pos(2),
new Mechanism("ptr")
.value(ValueRequirement.OPTIONAL)
.validate(() => { throw new ValidationError(`"ptr" mechanism should not be used`) })
.multiple()
.pos(2),
new Mechanism("ipv4")
.validate(this.validateIPv4)
.multiple()
.pos(2),
new Mechanism("ipv6")
.validate(this.validateIPv6)
.multiple()
.pos(2),
new Mechanism("exists")
.validate(this.validateDomain)
.multiple()
.pos(2),
new Mechanism("all")
.value(ValueRequirement.PROHIBITED)
.pos(3),
new Modifier("redirect")
.validate(this.validateDomain)
.pos(4),
new Modifier("exp")
.validate(this.validateDomain)
.pos(4),
];
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.validationFunction(term.key, input.value);
lastPos = term.position;
}
}
static validateDomain(key, value) {
// https://www.rfc-editor.org/rfc/rfc7208#section-7.1
const valid = value
.split(".")
.every(segment => segment.match(/^([^%]|%_|%%|%-|%\{[slodiphcrtv]\d*r?[-.+,\/_=]*})+$/));
if (!valid) throw new ValidationError(`Value for "${key}" is not a valid domain name`);
return true;
}
static validateIPv4(key, value) {
const segments = value.split(".");
const valid = segments.every(segment => {
const number = parseInt(segment);
return !isNaN(number) && number >= 0 && number <= 255;
});
if (segments.length !== 4 || !valid) {
throw new ValidationError(`Value for ${key} is not a valid IPv4 address`);
}
return true;
}
static validateIPv6(key, value) {
return true; // TODO validate properly
}
}