Refactor names, separate SPF validator from others
This commit is contained in:
parent
82c4579c58
commit
80ad069793
@ -1,22 +1,22 @@
|
|||||||
import { DmarcTool } from "./tools/DmarcTool.js";
|
import { DmarcRecord } from "./records/DmarcRecord.js";
|
||||||
|
|
||||||
const tools = {
|
const records = {
|
||||||
"/dmarc-creator": DmarcTool,
|
"/dmarc-creator": DmarcRecord,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Tool = tools[location.pathname];
|
const Record = records[location.pathname];
|
||||||
|
|
||||||
addFields(document.getElementById("form"), Tool.fields.filter(field => !field.categoryName));
|
addFields(document.getElementById("form"), Record.fields.filter(field => !field.categoryName));
|
||||||
|
|
||||||
const categories = Tool.fields.map(field => field.categoryName).filter(isUnique).filter(val => val);
|
const categories = Record.fields.map(field => field.categoryName).filter(isUnique).filter(val => val);
|
||||||
|
|
||||||
for (const category of categories) {
|
for (const category of categories) {
|
||||||
const details = document.createElement("details");
|
const details = document.createElement("details");
|
||||||
details.innerHTML = `<summary>${Tool.categories[category]}</summary>`;
|
details.innerHTML = `<summary>${Record.categories[category]}</summary>`;
|
||||||
|
|
||||||
document.getElementById("form").appendChild(details);
|
document.getElementById("form").appendChild(details);
|
||||||
|
|
||||||
addFields(details, Tool.fields.filter(field => field.categoryName === category));
|
addFields(details, Record.fields.filter(field => field.categoryName === category));
|
||||||
}
|
}
|
||||||
|
|
||||||
function addFields(elem, fields) {
|
function addFields(elem, fields) {
|
||||||
@ -36,9 +36,9 @@ document.getElementById("form").onchange = () => generate();
|
|||||||
generate();
|
generate();
|
||||||
|
|
||||||
function generate() {
|
function generate() {
|
||||||
const tool = new Tool();
|
const record = new Record();
|
||||||
|
|
||||||
document.getElementById("record").value = tool.fieldsToString();
|
document.getElementById("record").value = record.fieldsToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("record").onclick = (e) => {
|
document.getElementById("record").onclick = (e) => {
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
import { Field } from "./Field.js";
|
|
||||||
import { ValidationError } from "../ValidationError.js";
|
|
||||||
|
|
||||||
export class DomainField extends Field {
|
|
||||||
constructor(key, separator) {
|
|
||||||
super(key);
|
|
||||||
this.separator = separator;
|
|
||||||
}
|
|
||||||
|
|
||||||
validate(value) {
|
|
||||||
if (!value.match(/\w+(\.\w+)+/)) {
|
|
||||||
throw new ValidationError(`Field ${this.key} is not a valid domain`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,61 +1,58 @@
|
|||||||
import { ConstantField } from "../fields/ConstantField.js";
|
import { ConstantTag } from "../tags/ConstantTag.js";
|
||||||
import { EnumField } from "../fields/EnumField.js";
|
import { EnumTag } from "../tags/EnumTag.js";
|
||||||
import { IntField } from "../fields/IntField.js";
|
import { IntTag } from "../tags/IntTag.js";
|
||||||
import { Field } from "../fields/Field.js";
|
import { Tag } from "../tags/Tag.js";
|
||||||
import { DmarcUriListField } from "../fields/DmarcUriListField.js";
|
import { DmarcUriListTag } from "../tags/DmarcUriListTag.js";
|
||||||
import { DnsTool } from "./DnsTool.js";
|
import { TagListRecord } from "./TagListRecord.js";
|
||||||
import { ValidationError } from "../ValidationError.js";
|
|
||||||
|
|
||||||
export class DmarcTool extends DnsTool {
|
|
||||||
static allowWhitespaceAroundSeparator = true;
|
|
||||||
|
|
||||||
|
export class DmarcRecord extends TagListRecord {
|
||||||
static fields = [
|
static fields = [
|
||||||
new ConstantField("v", "DMARC1")
|
new ConstantTag("v", "DMARC1")
|
||||||
.required()
|
.required()
|
||||||
.pos(0),
|
.pos(0),
|
||||||
|
|
||||||
new EnumField("p", ["none", "quarantine", "reject"])
|
new EnumTag("p", ["none", "quarantine", "reject"])
|
||||||
.label("Mail Receiver policy")
|
.label("Mail Receiver policy")
|
||||||
.desc("How to handle failed validations. The email may be quarantined (usually means sent to spam) or rejected")
|
.desc("How to handle failed validations. The email may be quarantined (usually means sent to spam) or rejected")
|
||||||
.options(["None", "Quarantine", "Reject"])
|
.options(["None", "Quarantine", "Reject"])
|
||||||
.required()
|
.required()
|
||||||
.pos(1),
|
.pos(1),
|
||||||
|
|
||||||
new EnumField("adkim", ["r", "s"])
|
new EnumTag("adkim", ["r", "s"])
|
||||||
.label("DKIM")
|
.label("DKIM")
|
||||||
.desc("How strictly to handle DKIM validation")
|
.desc("How strictly to handle DKIM validation")
|
||||||
.options(["Relaxed", "Strict"])
|
.options(["Relaxed", "Strict"])
|
||||||
.default("r")
|
.default("r")
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new EnumField("aspf", ["r", "s"])
|
new EnumTag("aspf", ["r", "s"])
|
||||||
.label("SPF")
|
.label("SPF")
|
||||||
.desc("How strictly to handle SPF validation")
|
.desc("How strictly to handle SPF validation")
|
||||||
.options(["Relaxed", "Strict"])
|
.options(["Relaxed", "Strict"])
|
||||||
.default("r")
|
.default("r")
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new EnumField("sp", ["none", "quarantine", "reject"])
|
new EnumTag("sp", ["none", "quarantine", "reject"])
|
||||||
.label("Mail Receiver policy (for subdomains)")
|
.label("Mail Receiver policy (for subdomains)")
|
||||||
.desc("Same as Mail Receiver policy, but applies only to subdomains. If not set, Mail Receiver policy applies to both top-level domain and subdomains")
|
.desc("Same as Mail Receiver policy, but applies only to subdomains. If not set, Mail Receiver policy applies to both top-level domain and subdomains")
|
||||||
.category("advanced")
|
.category("advanced")
|
||||||
.options(["None", "Quarantine", "Reject"])
|
.options(["None", "Quarantine", "Reject"])
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new IntField("pct", 0, 100)
|
new IntTag("pct", 0, 100)
|
||||||
.label("Percentage")
|
.label("Percentage")
|
||||||
.desc("Percentage of emails to apply DMARC validation on. Useful for split-testing and continuous rollout")
|
.desc("Percentage of emails to apply DMARC validation on. Useful for split-testing and continuous rollout")
|
||||||
.category("advanced")
|
.category("advanced")
|
||||||
.default(100)
|
.default(100)
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new DmarcUriListField("ruf")
|
new DmarcUriListTag("ruf")
|
||||||
.label("Send failure reports to")
|
.label("Send failure reports to")
|
||||||
.desc("When DMARC validation fails, reports are sent to this email")
|
.desc("When DMARC validation fails, reports are sent to this email")
|
||||||
.category("failure-reporting")
|
.category("failure-reporting")
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new EnumField("fo", ["0", "1", "d", "s"])
|
new EnumTag("fo", ["0", "1", "d", "s"])
|
||||||
.label("Failure reporting options")
|
.label("Failure reporting options")
|
||||||
.desc("Define how reports will be generated")
|
.desc("Define how reports will be generated")
|
||||||
.category("failure-reporting")
|
.category("failure-reporting")
|
||||||
@ -68,19 +65,19 @@ export class DmarcTool extends DnsTool {
|
|||||||
.default("0")
|
.default("0")
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new DmarcUriListField("rua")
|
new DmarcUriListTag("rua")
|
||||||
.label("Send aggregate feedback to")
|
.label("Send aggregate feedback to")
|
||||||
.desc("Aggregate reports will be sent to this email, if defined")
|
.desc("Aggregate reports will be sent to this email, if defined")
|
||||||
.category("failure-reporting"),
|
.category("failure-reporting"),
|
||||||
|
|
||||||
new IntField("ri", 0, 2 ** 32)
|
new IntTag("ri", 0, 2 ** 32)
|
||||||
.label("Aggregate report interval")
|
.label("Aggregate report interval")
|
||||||
.desc("Interval (in seconds) between aggregate reports")
|
.desc("Interval (in seconds) between aggregate reports")
|
||||||
.category("failure-reporting")
|
.category("failure-reporting")
|
||||||
.default(86400)
|
.default(86400)
|
||||||
.pos(2),
|
.pos(2),
|
||||||
|
|
||||||
new Field("rf").default("afrf"), // Other values not supported
|
new Tag("rf").default("afrf"), // Other values not supported
|
||||||
];
|
];
|
||||||
|
|
||||||
static categories = {
|
static categories = {
|
||||||
167
assets/scripts/records/SpfRecord.js
Normal file
167
assets/scripts/records/SpfRecord.js
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { ValidationError } from "../ValidationError.js";
|
import { ValidationError } from "../ValidationError.js";
|
||||||
|
|
||||||
export class DnsTool {
|
/** Common class for DMARC/DKIM which both use semicolon-separated tag-value lists */
|
||||||
static allowWhitespaceAroundSeparator;
|
export class TagListRecord {
|
||||||
static fields = [];
|
static fields = [];
|
||||||
|
|
||||||
constructor(text) {
|
constructor(text) {
|
||||||
@ -16,20 +16,10 @@ export class DnsTool {
|
|||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
for (const token of this.tokenize()) {
|
for (const token of this.tokenize()) {
|
||||||
const key = token.match(/^\w*/)[0];
|
const [key, value] = token.split(/\s*=\s*/);
|
||||||
|
|
||||||
const field = this.constructor.fields.find(f => f.key === key);
|
|
||||||
if (!field) {
|
|
||||||
throw new ValidationError(`Unknown field: ${key}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const wsp = this.constructor.allowWhitespaceAroundSeparator ? "\\s*" : "";
|
|
||||||
const separator = new RegExp(wsp + field.separator + wsp);
|
|
||||||
|
|
||||||
const value = token.split(separator)[1];
|
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
throw new ValidationError(`Field "${key}" is missing a value`);
|
throw new ValidationError(`Tag "${key}" is missing a value`);
|
||||||
}
|
}
|
||||||
|
|
||||||
result.push({ key, value });
|
result.push({ key, value });
|
||||||
@ -43,7 +33,7 @@ export class DnsTool {
|
|||||||
|
|
||||||
for (const field of this.constructor.fields) {
|
for (const field of this.constructor.fields) {
|
||||||
if (field.isRequired && !values.some(v => v.key === field.key)) {
|
if (field.isRequired && !values.some(v => v.key === field.key)) {
|
||||||
throw new ValidationError(`Field "${field.key}" is required`);
|
throw new ValidationError(`Tag "${field.key}" is required`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,13 +43,13 @@ export class DnsTool {
|
|||||||
const field = this.constructor.fields.find(f => f.key === input.key);
|
const field = this.constructor.fields.find(f => f.key === input.key);
|
||||||
|
|
||||||
if (!field) {
|
if (!field) {
|
||||||
throw new ValidationError(`Unknown field: ${input.key}`);
|
throw new ValidationError(`Unknown tag: ${input.key}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.position < lastPos) {
|
if (field.position < lastPos) {
|
||||||
const lastField = this.constructor.fields.find(f => f.key === values[i-1].key);
|
const lastField = this.constructor.fields.find(f => f.key === values[i-1].key);
|
||||||
|
|
||||||
throw new ValidationError(`Field "${lastField.key}" must come after "${field.key}"`);
|
throw new ValidationError(`Tag "${lastField.key}" must come after "${field.key}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
field.validate(input.value);
|
field.validate(input.value);
|
||||||
9
assets/scripts/spf/Mechanism.js
Normal file
9
assets/scripts/spf/Mechanism.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Term } from "./Term.js";
|
||||||
|
|
||||||
|
export class Mechanism extends Term {
|
||||||
|
separator = ":";
|
||||||
|
|
||||||
|
constructor(key) {
|
||||||
|
super(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
assets/scripts/spf/Modifier.js
Normal file
9
assets/scripts/spf/Modifier.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Term } from "./Term.js";
|
||||||
|
|
||||||
|
export class Modifier extends Term {
|
||||||
|
separator = "=";
|
||||||
|
|
||||||
|
constructor(key) {
|
||||||
|
super(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
assets/scripts/spf/Term.js
Normal file
41
assets/scripts/spf/Term.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { ValueRequirement } from "./ValueRequirement.js";
|
||||||
|
|
||||||
|
export class Term {
|
||||||
|
separator = null;
|
||||||
|
isRequired = false;
|
||||||
|
position = null;
|
||||||
|
allowMultiple = false;
|
||||||
|
valueRequirement = ValueRequirement.REQUIRED;
|
||||||
|
validationFunction = null;
|
||||||
|
|
||||||
|
constructor(key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builder methods
|
||||||
|
|
||||||
|
required() {
|
||||||
|
this.isRequired = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos(i) {
|
||||||
|
this.position = i;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
multiple() {
|
||||||
|
this.allowMultiple = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
value(requirement) {
|
||||||
|
this.valueRequirement = requirement;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(func) {
|
||||||
|
this.validationFunction = func;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
assets/scripts/spf/ValueRequirement.js
Normal file
5
assets/scripts/spf/ValueRequirement.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const ValueRequirement = {
|
||||||
|
REQUIRED: "required",
|
||||||
|
OPTIONAL: "optional",
|
||||||
|
PROHIBITED: "prohibited",
|
||||||
|
};
|
||||||
@ -1,9 +1,7 @@
|
|||||||
import { Field } from "./Field.js";
|
import { Tag } from "./Tag.js";
|
||||||
import { ValidationError } from "../ValidationError.js";
|
import { ValidationError } from "../ValidationError.js";
|
||||||
|
|
||||||
export class ConstantField extends Field {
|
export class ConstantTag extends Tag {
|
||||||
separator = "=";
|
|
||||||
|
|
||||||
constructor(key, value) {
|
constructor(key, value) {
|
||||||
super(key);
|
super(key);
|
||||||
this.value = value;
|
this.value = value;
|
||||||
@ -1,9 +1,7 @@
|
|||||||
import { Field } from "./Field.js";
|
import { Tag } from "./Tag.js";
|
||||||
import { ValidationError } from "../ValidationError.js";
|
import { ValidationError } from "../ValidationError.js";
|
||||||
|
|
||||||
export class DmarcUriListField extends Field {
|
export class DmarcUriListTag extends Tag {
|
||||||
separator = "=";
|
|
||||||
|
|
||||||
constructor(key) {
|
constructor(key) {
|
||||||
super(key);
|
super(key);
|
||||||
}
|
}
|
||||||
@ -17,7 +15,7 @@ export class DmarcUriListField extends Field {
|
|||||||
try {
|
try {
|
||||||
new URL(uri);
|
new URL(uri);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
throw new ValidationError(`Invalid URI for field "${this.key}": ${uri}`);
|
throw new ValidationError(`Invalid URI for tag "${this.key}": ${uri}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,9 +1,7 @@
|
|||||||
import { Field } from "./Field.js";
|
import { Tag } from "./Tag.js";
|
||||||
import { ValidationError } from "../ValidationError.js";
|
import { ValidationError } from "../ValidationError.js";
|
||||||
|
|
||||||
export class EnumField extends Field {
|
export class EnumTag extends Tag {
|
||||||
separator = "=";
|
|
||||||
|
|
||||||
constructor(key, values) {
|
constructor(key, values) {
|
||||||
super(key);
|
super(key);
|
||||||
this.values = values;
|
this.values = values;
|
||||||
@ -14,7 +12,7 @@ export class EnumField extends Field {
|
|||||||
if (this.values.includes(value))
|
if (this.values.includes(value))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
throw new ValidationError(`Invalid value for field "${this.key}" - must be one of: ${this.values.join(", ")}`);
|
throw new ValidationError(`Invalid value for tag "${this.key}" - must be one of: ${this.values.join(", ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getInputHtml() {
|
getInputHtml() {
|
||||||
@ -1,9 +1,7 @@
|
|||||||
import { Field } from "./Field.js";
|
import { Tag } from "./Tag.js";
|
||||||
import { ValidationError } from "../ValidationError.js";
|
import { ValidationError } from "../ValidationError.js";
|
||||||
|
|
||||||
export class IntField extends Field {
|
export class IntTag extends Tag {
|
||||||
separator = "=";
|
|
||||||
|
|
||||||
constructor(key, min, max) {
|
constructor(key, min, max) {
|
||||||
super(key);
|
super(key);
|
||||||
this.min = min;
|
this.min = min;
|
||||||
@ -1,7 +1,5 @@
|
|||||||
export class Field {
|
export class Tag {
|
||||||
separator = null;
|
|
||||||
isRequired = false;
|
isRequired = false;
|
||||||
allowMultiple = false;
|
|
||||||
defaultValue = null;
|
defaultValue = null;
|
||||||
displayName = null;
|
displayName = null;
|
||||||
description = null;
|
description = null;
|
||||||
@ -10,7 +8,7 @@ export class Field {
|
|||||||
|
|
||||||
constructor(key) {
|
constructor(key) {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
this.id = "field-" + key;
|
this.id = "tag-" + key;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Virtual methods
|
// Virtual methods
|
||||||
@ -34,11 +32,6 @@ export class Field {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
multiple() {
|
|
||||||
this.allowMultiple = true;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
default(value) {
|
default(value) {
|
||||||
this.defaultValue = value;
|
this.defaultValue = value;
|
||||||
return this;
|
return this;
|
||||||
@ -1,57 +0,0 @@
|
|||||||
import { ConstantField } from "../fields/ConstantField.js";
|
|
||||||
import { DomainField } from "../fields/DomainField.js";
|
|
||||||
import { DnsTool } from "./DnsTool.js";
|
|
||||||
|
|
||||||
export class SpfTool extends DnsTool {
|
|
||||||
static allowWhitespaceAroundSeparator = false;
|
|
||||||
|
|
||||||
static fields = [
|
|
||||||
new ConstantField("v", "spf1")
|
|
||||||
.required()
|
|
||||||
.pos(0),
|
|
||||||
|
|
||||||
new DomainField("include", ":")
|
|
||||||
.multiple()
|
|
||||||
.pos(1),
|
|
||||||
|
|
||||||
new DomainField("a", ":")
|
|
||||||
.multiple()
|
|
||||||
.pos(1),
|
|
||||||
|
|
||||||
new DomainField("mx", ":")
|
|
||||||
.multiple()
|
|
||||||
.pos(2),
|
|
||||||
|
|
||||||
new DomainField("ptr", ":")
|
|
||||||
.multiple()
|
|
||||||
.pos(2),
|
|
||||||
|
|
||||||
new DomainField("ipv4", ":")
|
|
||||||
.multiple()
|
|
||||||
.pos(2),
|
|
||||||
|
|
||||||
new DomainField("ipv6", ":")
|
|
||||||
.multiple()
|
|
||||||
.pos(2),
|
|
||||||
|
|
||||||
new DomainField("exists", ":")
|
|
||||||
.multiple()
|
|
||||||
.pos(2),
|
|
||||||
|
|
||||||
new DomainField("redirect", "=")
|
|
||||||
.pos(3),
|
|
||||||
|
|
||||||
new DomainField("exp", "=")
|
|
||||||
.pos(3),
|
|
||||||
|
|
||||||
// TODO all
|
|
||||||
];
|
|
||||||
|
|
||||||
constructor(text) {
|
|
||||||
super(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenize() {
|
|
||||||
return this.text.split(/\s+/);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import { DmarcTool } from "./tools/DmarcTool.js";
|
import { DmarcRecord } from "./records/DmarcRecord.js";
|
||||||
import { SpfTool } from "./tools/SpfTool.js";
|
import { SpfRecord } from "./records/SpfRecord.js";
|
||||||
|
|
||||||
const tools = {
|
const records = {
|
||||||
"/dmarc-validator": DmarcTool,
|
"/dmarc-validator": DmarcRecord,
|
||||||
"/spf-validator": SpfTool,
|
"/spf-validator": SpfRecord,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Tool = tools[location.pathname];
|
const Record = records[location.pathname];
|
||||||
|
|
||||||
document.getElementById("record").oninput = event => validate(event.target.value);
|
document.getElementById("record").oninput = event => validate(event.target.value);
|
||||||
|
|
||||||
@ -26,10 +26,10 @@ function validate(value) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tool = new Tool(value);
|
const record = new Record(value);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
tool.validate();
|
record.validate();
|
||||||
|
|
||||||
document.getElementById("record").classList.add("valid");
|
document.getElementById("record").classList.add("valid");
|
||||||
document.getElementById("success").style.display = "flex";
|
document.getElementById("success").style.display = "flex";
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
<p id="result-placeholder" class="validation-result"></p>
|
<p id="result-placeholder" class="validation-result"></p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
DMARC is a standard for web servers to tell how to handle validation errors in SPF and DKIM.
|
DMARC is a standard for email servers to tell how to handle validation errors in SPF and DKIM.
|
||||||
It is a DNS TXT record with different values, defining rules on when to reject the email, how to report
|
It is a DNS TXT record with different values, defining rules on when to reject the email, how to report
|
||||||
failures etc.
|
failures etc.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -27,7 +27,40 @@
|
|||||||
|
|
||||||
<p id="result-placeholder" class="validation-result"></p>
|
<p id="result-placeholder" class="validation-result"></p>
|
||||||
|
|
||||||
<p>Insert SPF description</p>
|
<p>
|
||||||
|
SPF is an email standard for authorizing hosts to send emails from a specific domain. Most email
|
||||||
|
servers, such as Gmail or Outlook, require SPF for security reasons, since without it, it is easy to
|
||||||
|
spoof email addresses.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
SPF is defined with a TXT record on the email domain. It must start with "v=spf1" (to differentiate it
|
||||||
|
from other TXT records), and after that contains a set of mechanisms and modifiers (together called
|
||||||
|
terms), separated by space.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Mechanisms are colon-separated key-value pairs, whose primary purpose is to define which IPs are allowed
|
||||||
|
to send emails from the domain. The most used mechanisms are:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><b>include</b>: Includes the SPF definition from the specified domain</li>
|
||||||
|
<li><b>a</b>: Allows emails from the specified domain</li>
|
||||||
|
<li><b>mx</b>: Allows emails from the IP addresses specified in the MX records</li>
|
||||||
|
<li><b>ipv6</b> / <b>ipv6</b>: Allows emails from the specified IP address</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
By default, mechanisms allow the specified IPs to send emails. You can add a qualifier, such as <b>~</b>
|
||||||
|
or <b>-</b> to prohibit them instead.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Often, you would include some IP addresses (using e.g. the <b>include</b>, <b>a</b> and <b>mx</b>
|
||||||
|
mechanisms), and end with <b>-all</b> or <b>~all</b> to reject emails from everywhere else. If the
|
||||||
|
<b>all</b> mechanism is used, it must come after the other mechanisms.
|
||||||
|
</p>
|
||||||
|
|
||||||
<center>
|
<center>
|
||||||
<h3>More tools:</h3>
|
<h3>More tools:</h3>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user