Compare commits

..

No commits in common. "f8e10f32d0c29dc7fcdc0f63af0663fa00e68603" and "82c4579c585a0647b4e5a668536dd54791ea45f5" have entirely different histories.

29 changed files with 237 additions and 779 deletions

51
assets/scripts/creator.js Normal file
View File

@ -0,0 +1,51 @@
import { DmarcTool } from "./tools/DmarcTool.js";
const tools = {
"/dmarc-creator": DmarcTool,
};
const Tool = tools[location.pathname];
addFields(document.getElementById("form"), Tool.fields.filter(field => !field.categoryName));
const categories = Tool.fields.map(field => field.categoryName).filter(isUnique).filter(val => val);
for (const category of categories) {
const details = document.createElement("details");
details.innerHTML = `<summary>${Tool.categories[category]}</summary>`;
document.getElementById("form").appendChild(details);
addFields(details, Tool.fields.filter(field => field.categoryName === category));
}
function addFields(elem, fields) {
for (const field of fields) {
if (!field.getInputHtml()) continue;
elem.innerHTML += `
<label for="${field.key}">${field.displayName}</label>
<p class="description">${field.description ?? ""}</p>
${field.getInputHtml()}
`;
}
}
document.getElementById("form").onchange = () => generate();
generate();
function generate() {
const tool = new Tool();
document.getElementById("record").value = tool.fieldsToString();
}
document.getElementById("record").onclick = (e) => {
e.target.select();
document.execCommand("copy");
}
function isUnique(value, index, array) {
return array.indexOf(value) === index;
}

View File

@ -1,7 +1,9 @@
import { Tag } from "./Tag.js"; import { Field } from "./Field.js";
import { ValidationError } from "../ValidationError.js"; import { ValidationError } from "../ValidationError.js";
export class ConstantTag extends Tag { export class ConstantField extends Field {
separator = "=";
constructor(key, value) { constructor(key, value) {
super(key); super(key);
this.value = value; this.value = value;

View File

@ -1,7 +1,9 @@
import { Tag } from "./Tag.js"; import { Field } from "./Field.js";
import { ValidationError } from "../ValidationError.js"; import { ValidationError } from "../ValidationError.js";
export class DmarcUriListTag extends Tag { export class DmarcUriListField extends Field {
separator = "=";
constructor(key) { constructor(key) {
super(key); super(key);
} }
@ -15,7 +17,7 @@ export class DmarcUriListTag extends Tag {
try { try {
new URL(uri); new URL(uri);
} catch(e) { } catch(e) {
throw new ValidationError(`Invalid URI for tag "${this.key}": ${uri}`); throw new ValidationError(`Invalid URI for field "${this.key}": ${uri}`);
} }
} }

View File

@ -0,0 +1,17 @@
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;
}
}

View File

@ -1,7 +1,9 @@
import { Tag } from "./Tag.js"; import { Field } from "./Field.js";
import { ValidationError } from "../ValidationError.js"; import { ValidationError } from "../ValidationError.js";
export class EnumTag extends Tag { export class EnumField extends Field {
separator = "=";
constructor(key, values) { constructor(key, values) {
super(key); super(key);
this.values = values; this.values = values;
@ -12,7 +14,7 @@ export class EnumTag extends Tag {
if (this.values.includes(value)) if (this.values.includes(value))
return true; return true;
throw new ValidationError(`Invalid value for tag "${this.key}" - must be one of: ${this.values.join(", ")}`); throw new ValidationError(`Invalid value for field "${this.key}" - must be one of: ${this.values.join(", ")}`);
} }
getInputHtml() { getInputHtml() {

View File

@ -1,12 +1,12 @@
/**
* Defines any key-value pair in any type of DNS-record
* Used for input fields on DNS creator pages
*/
export class Field { export class Field {
separator = null;
isRequired = false;
allowMultiple = false;
defaultValue = null;
displayName = null; displayName = null;
description = null; description = null;
categoryName = null; categoryName = null;
isHidden = false; position = null;
constructor(key) { constructor(key) {
this.key = key; this.key = key;
@ -15,6 +15,10 @@ export class Field {
// Virtual methods // Virtual methods
validate() {
return true;
}
getInputHtml() { getInputHtml() {
return null; return null;
} }
@ -25,6 +29,21 @@ export class Field {
// Builder methods // Builder methods
required() {
this.isRequired = true;
return this;
}
multiple() {
this.allowMultiple = true;
return this;
}
default(value) {
this.defaultValue = value;
return this;
}
label(label) { label(label) {
this.displayName = label; this.displayName = label;
return this; return this;
@ -40,8 +59,8 @@ export class Field {
return this; return this;
} }
hidden() { pos(i) {
this.isHidden = true; this.position = i;
return this; return this;
} }
} }

View File

@ -1,7 +1,9 @@
import { Tag } from "./Tag.js"; import { Field } from "./Field.js";
import { ValidationError } from "../ValidationError.js"; import { ValidationError } from "../ValidationError.js";
export class IntTag extends Tag { export class IntField extends Field {
separator = "=";
constructor(key, min, max) { constructor(key, min, max) {
super(key); super(key);
this.min = min; this.min = min;

View File

@ -1,166 +0,0 @@
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(" ");
}
}

View File

@ -1,17 +0,0 @@
import { ValidationError } from "../ValidationError.js";
import { Mechanism } from "./Mechanism.js";
import { validateSpfDomain } from "./utils.js";
export class DomainMechanism extends Mechanism {
placeholder = "example.com";
constructor(key) {
super(key);
}
validate(value) {
if (!validateSpfDomain(value)) throw new ValidationError(`Value for "${this.key}" is not a valid domain name`);
return true;
}
}

View File

@ -1,26 +0,0 @@
import { ValidationError } from "../ValidationError.js";
import { Mechanism } from "./Mechanism.js";
export class IPv4Mechanism extends Mechanism {
placeholder = "0.0.0.0";
constructor(key) {
super(key);
}
validate(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;
}
}

View File

@ -1,14 +0,0 @@
import { Mechanism } from "./Mechanism.js";
export class IPv6Mechanism extends Mechanism {
placeholder = "2001:db8::1";
constructor(key) {
super(key);
}
validate(value) {
// TODO validate
return true;
}
}

View File

@ -1,36 +0,0 @@
import { Term } from "./Term.js";
import { ValueRequirement } from "./ValueRequirement.js";
export class Mechanism extends Term {
separator = ":";
placeholder = null;
constructor(key) {
super(key);
}
getInputQualifier() {
return document.getElementById(this.id + "-qualifier").value;
}
getInputValue() {
return document.getElementById(this.id + "-value")?.value;
}
getInputHtml() {
const noValue = this.valueRequirement === ValueRequirement.PROHIBITED;
const placeholder = this.placeholder
+ (this.valueRequirement === ValueRequirement.OPTIONAL ? " (Optional)" : "");
return `
<select id="${this.id}-qualifier">
${this.isRequired ? "" : `<option value="">&lt;not set&gt;</option>`}
<option value="+">Pass</option>
<option value="-">Fail</option>
<option value="~">Soft fail</option>
<option value="?">Neutral</option>
</select>
${noValue ? "" : `<input id="${this.id}-value" type="text" placeholder="${placeholder}">`}
`;
}
}

View File

@ -1,25 +0,0 @@
import { Term } from "./Term.js";
import { ValidationError } from "../ValidationError.js";
import { validateSpfDomain } from "./utils.js";
export class Modifier extends Term {
separator = "=";
constructor(key) {
super(key);
}
validate(value) {
if (!validateSpfDomain(value)) throw new ValidationError(`Value for "${this.key}" is not a valid domain name`);
return true;
}
getInputValue() {
return document.getElementById(this.id).value;
}
getInputHtml() {
return `<input id="${this.id}" type="text" placeholder="example.com">`;
}
}

View File

@ -1,42 +0,0 @@
import { ValueRequirement } from "./ValueRequirement.js";
import { Field } from "../Field.js";
export class Term extends Field {
separator = null;
isRequired = false;
position = null;
allowMultiple = false;
valueRequirement = ValueRequirement.REQUIRED;
constructor(key) {
super(key)
}
// Virtual methods
getInputQualifier() {
return "";
}
// 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;
}
}

View File

@ -1,5 +0,0 @@
export const ValueRequirement = {
REQUIRED: "required",
OPTIONAL: "optional",
PROHIBITED: "prohibited",
};

View File

@ -1,21 +0,0 @@
import { Term } from "./Term.js";
import { ValidationError } from "../ValidationError.js";
export class VersionTerm extends Term {
separator = "=";
constructor(key, version) {
super(key);
this.version = version;
}
validate(value) {
if (value !== this.version) throw new ValidationError(`Version must be "${this.version}"`);
return true;
}
getInputValue() {
return this.version;
}
}

View File

@ -1,6 +0,0 @@
// https://www.rfc-editor.org/rfc/rfc7208#section-7.1
export function validateSpfDomain(domain) {
return domain
.split(".")
.every(segment => segment.match(/^([^%]|%_|%%|%-|%\{[slodiphcrtv]\d*r?[-.+,\/_=]*})+$/));
}

View File

@ -1,35 +0,0 @@
import { Field } from "../Field.js";
/** A tag within a DMARC/DKIM record */
export class Tag extends Field {
isRequired = false;
defaultValue = null;
position = null;
constructor(key) {
super(key);
}
// Virtual methods
validate() {
return true;
}
// Builder methods
required() {
this.isRequired = true;
return this;
}
default(value) {
this.defaultValue = value;
return this;
}
pos(i) {
this.position = i;
return this;
}
}

View File

@ -1,58 +1,61 @@
import { ConstantTag } from "../tags/ConstantTag.js"; import { ConstantField } from "../fields/ConstantField.js";
import { EnumTag } from "../tags/EnumTag.js"; import { EnumField } from "../fields/EnumField.js";
import { IntTag } from "../tags/IntTag.js"; import { IntField } from "../fields/IntField.js";
import { Tag } from "../tags/Tag.js"; import { Field } from "../fields/Field.js";
import { DmarcUriListTag } from "../tags/DmarcUriListTag.js"; import { DmarcUriListField } from "../fields/DmarcUriListField.js";
import { TagListRecord } from "./TagListRecord.js"; import { DnsTool } from "./DnsTool.js";
import { ValidationError } from "../ValidationError.js";
export class DmarcTool extends DnsTool {
static allowWhitespaceAroundSeparator = true;
export class DmarcRecord extends TagListRecord {
static fields = [ static fields = [
new ConstantTag("v", "DMARC1") new ConstantField("v", "DMARC1")
.required() .required()
.pos(0), .pos(0),
new EnumTag("p", ["none", "quarantine", "reject"]) new EnumField("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 EnumTag("adkim", ["r", "s"]) new EnumField("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 EnumTag("aspf", ["r", "s"]) new EnumField("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 EnumTag("sp", ["none", "quarantine", "reject"]) new EnumField("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 IntTag("pct", 0, 100) new IntField("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 DmarcUriListTag("ruf") new DmarcUriListField("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 EnumTag("fo", ["0", "1", "d", "s"]) new EnumField("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")
@ -65,19 +68,19 @@ export class DmarcRecord extends TagListRecord {
.default("0") .default("0")
.pos(2), .pos(2),
new DmarcUriListTag("rua") new DmarcUriListField("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 IntTag("ri", 0, 2 ** 32) new IntField("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 Tag("rf").default("afrf"), // Other values not supported new Field("rf").default("afrf"), // Other values not supported
]; ];
static categories = { static categories = {
@ -88,4 +91,18 @@ export class DmarcRecord extends TagListRecord {
constructor(text) { constructor(text) {
super(text); super(text);
} }
tokenize() {
return this.text
.replace(/;\s*$/, "")
.split(/;\s*/);
}
fieldsToString() {
let tokens = this.constructor.fields
.filter(field => !field.defaultValue || field.getInputValue() !== field.defaultValue)
.filter(field => field.getInputValue())
.map(field => field.key + "=" + field.getInputValue());
return tokens.join("; ");
}
} }

View File

@ -1,7 +1,7 @@
import { ValidationError } from "../ValidationError.js"; import { ValidationError } from "../ValidationError.js";
/** Common class for DMARC/DKIM which both use semicolon-separated tag-value lists */ export class DnsTool {
export class TagListRecord { static allowWhitespaceAroundSeparator;
static fields = []; static fields = [];
constructor(text) { constructor(text) {
@ -9,19 +9,27 @@ export class TagListRecord {
} }
tokenize() { tokenize() {
return this.text throw new Error("Unimplemented");
.replace(/;\s*$/, "")
.split(/;\s*/);
} }
getKeyValues() { getKeyValues() {
const result = []; const result = [];
for (const token of this.tokenize()) { for (const token of this.tokenize()) {
const [key, value] = token.split(/\s*=\s*/); const key = token.match(/^\w*/)[0];
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(`Tag "${key}" is missing a value`); throw new ValidationError(`Field "${key}" is missing a value`);
} }
result.push({ key, value }); result.push({ key, value });
@ -35,7 +43,7 @@ export class TagListRecord {
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(`Tag "${field.key}" is required`); throw new ValidationError(`Field "${field.key}" is required`);
} }
} }
@ -45,13 +53,13 @@ export class TagListRecord {
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 tag: ${input.key}`); throw new ValidationError(`Unknown field: ${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(`Tag "${lastField.key}" must come after "${field.key}"`); throw new ValidationError(`Field "${lastField.key}" must come after "${field.key}"`);
} }
field.validate(input.value); field.validate(input.value);
@ -61,14 +69,4 @@ export class TagListRecord {
return true; return true;
} }
static fieldsToString() {
const tokens = this.fields
.filter(field => !field.isHidden)
.filter(field => !field.defaultValue || field.getInputValue() !== field.defaultValue)
.filter(field => field.getInputValue())
.map(field => field.key + "=" + field.getInputValue());
return tokens.join("; ");
}
} }

View File

@ -0,0 +1,57 @@
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+/);
}
}

View File

@ -1,50 +0,0 @@
import { DmarcRecord } from "../records/DmarcRecord.js";
import { SpfRecord } from "../records/SpfRecord.js";
const records = {
"/dmarc-creator": DmarcRecord,
"/spf-creator": SpfRecord,
};
const Record = records[location.pathname];
addFields(document.getElementById("form"), Record.fields.filter(field => !field.categoryName));
const categories = Record.fields.map(field => field.categoryName).filter(isUnique).filter(val => val);
for (const category of categories) {
const details = document.createElement("details");
details.innerHTML = `<summary>${Record.categories[category]}</summary>`;
document.getElementById("form").appendChild(details);
addFields(details, Record.fields.filter(field => field.categoryName === category));
}
function addFields(elem, fields) {
for (const field of fields) {
if (field.isHidden || !field.getInputHtml()) continue;
elem.innerHTML += `
<label for="${field.key}">${field.displayName}</label>
<p class="description">${field.description ?? ""}</p>
${field.getInputHtml()}
`;
}
}
document.getElementById("form").onchange = generate;
generate();
function generate(event) {
document.getElementById("record").value = Record.fieldsToString();
}
document.getElementById("record").onclick = (e) => {
e.target.select();
document.execCommand("copy");
}
function isUnique(value, index, array) {
return array.indexOf(value) === index;
}

View File

@ -1,12 +1,12 @@
import { DmarcRecord } from "../records/DmarcRecord.js"; import { DmarcTool } from "./tools/DmarcTool.js";
import { SpfRecord } from "../records/SpfRecord.js"; import { SpfTool } from "./tools/SpfTool.js";
const records = { const tools = {
"/dmarc-validator": DmarcRecord, "/dmarc-validator": DmarcTool,
"/spf-validator": SpfRecord, "/spf-validator": SpfTool,
}; };
const Record = records[location.pathname]; const Tool = tools[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 record = new Record(value); const tool = new Tool(value);
try { try {
record.validate(); tool.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";

View File

@ -24,7 +24,6 @@ body {
font-family: "Open Sans", sans-serif; font-family: "Open Sans", sans-serif;
max-width: 800px; max-width: 800px;
margin: auto; margin: auto;
padding: 1rem;
} }
h1, h2 { h1, h2 {
@ -76,20 +75,6 @@ a {
color: #039BE5; color: #039BE5;
} }
code {
font-family: "JetBrains Mono", monospace;
background-color: #EEE;
border-radius: 3px;
padding: 0 0.25rem;
}
blockquote {
color: #757575;
padding-left: 1rem;
margin-left: 1rem;
border-left: 2px solid #EEE;
}
hr { hr {
border: none; border: none;
border-bottom: 1px solid #BDBDBD; border-bottom: 1px solid #BDBDBD;

View File

@ -2,10 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DMARC Record Creator - Generate DMARC DNS Records</title> <title>DMARC Record Creator - Generate DMARC DNS Records</title>
<link rel="stylesheet" href="/assets/styles/main.css"> <link rel="stylesheet" href="/assets/styles/main.css">
<script type="module" src="/assets/scripts/ui/creator.js"></script> <script type="module" src="/assets/scripts/creator.js"></script>
</head> </head>
<body> <body>
<h1>DMARC Record Creator</h1> <h1>DMARC Record Creator</h1>
@ -34,8 +33,7 @@
<center> <center>
<h3>More tools:</h3> <h3>More tools:</h3>
<a href="/dmarc-validator">DMARC Validator Tool</a> &bull; <a href="/dmarc-validator">DMARC Validator Tool</a>
<a href="/spf-creator">SPF Creator Tool</a>
</center> </center>
</main> </main>
</body> </body>

View File

@ -2,10 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DMARC Record Validator - Validate DMARC DNS Records</title> <title>DMARC Record Validator - Validate DMARC DNS Records</title>
<link rel="stylesheet" href="/assets/styles/main.css"> <link rel="stylesheet" href="/assets/styles/main.css">
<script type="module" src="/assets/scripts/ui/validator.js"></script> <script type="module" src="/assets/scripts/validator.js"></script>
</head> </head>
<body> <body>
<h1>DMARC Record Validator</h1> <h1>DMARC Record Validator</h1>
@ -29,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 email servers to tell how to handle validation errors in SPF and DKIM. DMARC is a standard for web 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>
@ -74,8 +73,7 @@
<center> <center>
<h3>More tools:</h3> <h3>More tools:</h3>
<a href="/dmarc-creator">DMARC Creator Tool</a> &bull; <a href="/dmarc-creator">DMARC Creator Tool</a>
<a href="/spf-validator">SPF Validator Tool</a>
</center> </center>
</main> </main>
</body> </body>

View File

@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPF Record Creator - Generate SPF DNS Records</title>
<link rel="stylesheet" href="/assets/styles/main.css">
<script type="module" src="/assets/scripts/ui/creator.js"></script>
</head>
<body>
<h1>SPF Record Creator</h1>
<label for="record">DNS Record</label><br>
<input id="record" type="text" readonly>
<main>
<h2>Create an SPF DNS Record</h2>
<p>
Customize the options below to generate an SPF record, which will be shown in the input field above.
The qualifiers that can be used are:
</p>
<p>
<b>Pass</b> (Default): Selected IP addresses are authorized to send emails from this domain<br>
<b>Fail</b>: Selected IP addresses are NOT authorized to send emails from this domain<br>
<b>Soft fail</b>: Selected IP addresses might not be authorized to send emails from this domain (actual behavior differs)<br>
<b>Neutral</b>: Do not explicitly state whether the IP addresses are authorized or not (can be used for overriding other qualifiers)
</p>
<form id="form"></form>
<hr>
<p>
This tool allows you to create SPF DNS records, which are used to authorize emails, and are necessary
to be able to send emails to most providers, such as Gmail, Outlook and Yahoo Mail.
</p>
<p>
It works by specifying which IP addresses are authorized or not authorized to send emails.
You can specify IP addresses from a domain, from your MX records, or explicitly.
When specifying IP addresses, you then say whether these should pass or fail the validation.
</p>
<p>
For advanced usage, domain fields may contain macros. These start with a percentage sign and will expand
to a dynamic value. For example, <b>%{d}</b> expands to the current domain and <b>%{i}</b> to the
current IP address. See the <a href="/spf-macro-guide">Macro Guide</a> for a list of all macros.
</p>
<center>
<h3>More tools:</h3>
<a href="/spf-validator">SPF Validator Tool</a> &bull;
<a href="/dmarc-creator">DMARC Creator Tool</a>
</center>
</main>
</body>
</html>

View File

@ -1,153 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPF Macro Guide - Explanation of all SPF macros with examples</title>
<link rel="stylesheet" href="/assets/styles/main.css">
<script type="module" src="/assets/scripts/ui/validator.js"></script>
</head>
<body>
<h1>SPF Macro Guide</h1>
<main>
<h2>Overview of SPF macros</h2>
<p>
Using SPF, you can specify which IP addresses are authorized to send emails from a mail server.
Many of the directives you can use allow you to specify a domain name, but here SPF comes with an
extra feature: Macros.
</p>
<p>
Macros allow you to insert dynamic values into the values of SPF directives, which can be used for
e.g. per-user authentication and more. This guide will go through all macros, along with some examples.
</p>
<p>
The mechanisms and modifiers that allow macros are: <b>include</b>, <b>a</b>, <b>mx</b>, <b>ptr</b>,
<b>exists</b>, <b>redirect</b> and <b>exp</b>.
</p>
<h3>List of macros</h3>
<ul>
<li>
<p><code>%{s}</code> - Sender email address</p>
<p>Expands to the email address which the current email is being sent from, e.g. <code>john@example.com</code></p>
</li>
<li>
<p><code>%{o}</code> - Sender domain</p>
<p>Expands to only the domain part of the sender email address, e.g. <code>example.com</code></p>
</li>
<li>
<p><code>%{l}</code> - Sender username</p>
<p>Expands to only the local part of the sender email address, e.g. <code>john</code></p>
</li>
<li>
<p><code>%{d}</code> - Current domain</p>
<p>
This starts out identical to the sender domain, but when hitting an <b>include</b> mechanism or
a <b>redirect</b> modifier, this value will change to the domain specified in that term
during the processing of it.
</p>
</li>
<li>
<p><code>%{i}</code> - IP address</p>
<p>
Expands to the IP address of the email client that is sending the mail.
This can both be an IPv4 and IPv6 address.
</p>
</li>
<li>
<p><code>%{v}</code> - IP version</p>
</li>
<li>
<p>Expands to the string <b>"in-addr"</b> if the sender address is IPv4, or <b>"ip6"</b> if it is IPv6.</p>
</li>
<li>
<p><code>%{p}</code> - Validated domain name</p>
<p>
Does a reverse DNS lookup of the sender IP address, and validates that the resulting domain
is a subdomain of the current domain. Expands to the validated domain, or the string "unknown".
<b>NOTE:</b> It is not recommended to use this macro. From the specification:
</p>
<blockquote>
This mechanism is slow, it is not as reliable as other
mechanisms in cases of DNS errors, and it places a large burden on
the .arpa name servers. If used, proper PTR records have to be in
place for the domain's hosts and the "ptr" mechanism SHOULD be one of
the last mechanisms checked. After many years of SPF deployment
experience, it has been concluded that it is unnecessary and more
reliable alternatives should be used instead.
</blockquote>
</li>
<li>
<p><code>%{h}</code> - HELO/EHLO domain</p>
<p>Expands to the domain given on the SMTP HELO/EHLO commands.</p>
</li>
</ul>
<h3>Transformers</h3>
<p>SPF macros can be transformed in a few different ways, by adding another character after the macro letter.</p>
<ul>
<li>
<p>Reverse transformer (r)</p>
<p>
Adding "r" after a macro will reverse the domain name or IP address.
E.g. if <code>%{d}</code> expands to <b>example.com</b>, <code>%{dr}</code> will be
<b>com.example</b>.
Likewise, if <code>%{i}</code> expands to <b>192.0.2.1</b>, <code>%{ir}</code> will become
<b>1.2.0.192</b>.
</p>
</li>
<li>
<p>Digit transformer (1-9)</p>
<p>
Adding a number after a macro, will take that amount of right-hand parts of the domain name /
IP address. This may be combined with reversing. E.g. for the domain <b>mail.example.com</b>,
<code>%{d2}</code> will expand to <b>example.com</b>.
</p>
</li>
</ul>
<h3>Explanations</h3>
<p>
SPF allows you to set custom error messages in case of failed validations using the <b>exp</b> modifier.
The message is retrieved from the TXT records of the domain name defined by the modifier.
This error message also supports macros, and has extended support for a few more than the ones above:
</p>
<ul>
<li>
<p><code>{%c}</code> - SMTP client IP (easily readable format)</p>
</li>
<li>
<p><code>{%r}</code> - Domain name of host performing the check</p>
</li>
<li>
<p><code>{%t}</code> - Current timestamp</p>
</li>
</ul>
<center>
<h3>SPF tools:</h3>
<a href="/spf-validator">SPF Validator Tool</a> &bull;
<a href="/spf-creator">SPF Creator Tool</a>
</center>
</main>
</body>
</html>

View File

@ -2,10 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPF Record Validator - Validate SPF DNS Records</title> <title>SPF Record Validator - Validate SPF DNS Records</title>
<link rel="stylesheet" href="/assets/styles/main.css"> <link rel="stylesheet" href="/assets/styles/main.css">
<script type="module" src="/assets/scripts/ui/validator.js"></script> <script type="module" src="/assets/scripts/validator.js"></script>
</head> </head>
<body> <body>
<h1>SPF Record Validator</h1> <h1>SPF Record Validator</h1>
@ -28,44 +27,10 @@
<p id="result-placeholder" class="validation-result"></p> <p id="result-placeholder" class="validation-result"></p>
<p> <p>Insert SPF description</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>
<a href="/spf-creator">SPF Creator Tool</a> &bull;
<a href="/dmarc-validator">DMARC Validator Tool</a> <a href="/dmarc-validator">DMARC Validator Tool</a>
</center> </center>
</main> </main>