167 lines
4.3 KiB
JavaScript
167 lines
4.3 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")
|
|
.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(" ");
|
|
}
|
|
}
|