Compare commits
5 Commits
82c4579c58
...
f8e10f32d0
| Author | SHA1 | Date | |
|---|---|---|---|
| f8e10f32d0 | |||
| 0276828a2c | |||
| 5b8a3d9266 | |||
| 00c8a999e7 | |||
| 80ad069793 |
@ -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;
|
||||||
position = null;
|
isHidden = false;
|
||||||
|
|
||||||
constructor(key) {
|
constructor(key) {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
@ -15,10 +15,6 @@ export class Field {
|
|||||||
|
|
||||||
// Virtual methods
|
// Virtual methods
|
||||||
|
|
||||||
validate() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
getInputHtml() {
|
getInputHtml() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -29,21 +25,6 @@ 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;
|
||||||
@ -59,8 +40,8 @@ export class Field {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
pos(i) {
|
hidden() {
|
||||||
this.position = i;
|
this.isHidden = true;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,51 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -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 = {
|
||||||
@ -91,18 +88,4 @@ export class DmarcTool extends DnsTool {
|
|||||||
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("; ");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
166
assets/scripts/records/SpfRecord.js
Normal file
166
assets/scripts/records/SpfRecord.js
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
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(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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) {
|
||||||
@ -9,27 +9,19 @@ export class DnsTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tokenize() {
|
tokenize() {
|
||||||
throw new Error("Unimplemented");
|
return this.text
|
||||||
|
.replace(/;\s*$/, "")
|
||||||
|
.split(/;\s*/);
|
||||||
}
|
}
|
||||||
|
|
||||||
getKeyValues() {
|
getKeyValues() {
|
||||||
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 +35,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 +45,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);
|
||||||
@ -69,4 +61,14 @@ export class DnsTool {
|
|||||||
|
|
||||||
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("; ");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
17
assets/scripts/spf/DomainMechanism.js
Normal file
17
assets/scripts/spf/DomainMechanism.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
assets/scripts/spf/IPv4Mechanism.js
Normal file
26
assets/scripts/spf/IPv4Mechanism.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
assets/scripts/spf/IPv6Mechanism.js
Normal file
14
assets/scripts/spf/IPv6Mechanism.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Mechanism } from "./Mechanism.js";
|
||||||
|
|
||||||
|
export class IPv6Mechanism extends Mechanism {
|
||||||
|
placeholder = "2001:db8::1";
|
||||||
|
|
||||||
|
constructor(key) {
|
||||||
|
super(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(value) {
|
||||||
|
// TODO validate
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
assets/scripts/spf/Mechanism.js
Normal file
36
assets/scripts/spf/Mechanism.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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=""><not set></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}">`}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
assets/scripts/spf/Modifier.js
Normal file
25
assets/scripts/spf/Modifier.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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">`;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
assets/scripts/spf/Term.js
Normal file
42
assets/scripts/spf/Term.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
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",
|
||||||
|
};
|
||||||
21
assets/scripts/spf/VersionTerm.js
Normal file
21
assets/scripts/spf/VersionTerm.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
assets/scripts/spf/utils.js
Normal file
6
assets/scripts/spf/utils.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// https://www.rfc-editor.org/rfc/rfc7208#section-7.1
|
||||||
|
export function validateSpfDomain(domain) {
|
||||||
|
return domain
|
||||||
|
.split(".")
|
||||||
|
.every(segment => segment.match(/^([^%]|%_|%%|%-|%\{[slodiphcrtv]\d*r?[-.+,\/_=]*})+$/));
|
||||||
|
}
|
||||||
@ -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;
|
||||||
35
assets/scripts/tags/Tag.js
Normal file
35
assets/scripts/tags/Tag.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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+/);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
50
assets/scripts/ui/creator.js
Normal file
50
assets/scripts/ui/creator.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@ -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";
|
||||||
@ -24,6 +24,7 @@ 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 {
|
||||||
@ -75,6 +76,20 @@ 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;
|
||||||
|
|||||||
@ -2,9 +2,10 @@
|
|||||||
<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/creator.js"></script>
|
<script type="module" src="/assets/scripts/ui/creator.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>DMARC Record Creator</h1>
|
<h1>DMARC Record Creator</h1>
|
||||||
@ -33,7 +34,8 @@
|
|||||||
|
|
||||||
<center>
|
<center>
|
||||||
<h3>More tools:</h3>
|
<h3>More tools:</h3>
|
||||||
<a href="/dmarc-validator">DMARC Validator Tool</a>
|
<a href="/dmarc-validator">DMARC Validator Tool</a> •
|
||||||
|
<a href="/spf-creator">SPF Creator Tool</a>
|
||||||
</center>
|
</center>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -2,9 +2,10 @@
|
|||||||
<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/validator.js"></script>
|
<script type="module" src="/assets/scripts/ui/validator.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>DMARC Record Validator</h1>
|
<h1>DMARC Record Validator</h1>
|
||||||
@ -28,7 +29,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>
|
||||||
@ -73,7 +74,8 @@
|
|||||||
|
|
||||||
<center>
|
<center>
|
||||||
<h3>More tools:</h3>
|
<h3>More tools:</h3>
|
||||||
<a href="/dmarc-creator">DMARC Creator Tool</a>
|
<a href="/dmarc-creator">DMARC Creator Tool</a> •
|
||||||
|
<a href="/spf-validator">SPF Validator Tool</a>
|
||||||
</center>
|
</center>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
59
spf-creator/index.html
Normal file
59
spf-creator/index.html
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<!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> •
|
||||||
|
<a href="/dmarc-creator">DMARC Creator Tool</a>
|
||||||
|
</center>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
153
spf-macro-guide/index.html
Normal file
153
spf-macro-guide/index.html
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<!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> •
|
||||||
|
<a href="/spf-creator">SPF Creator Tool</a>
|
||||||
|
</center>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -2,9 +2,10 @@
|
|||||||
<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/validator.js"></script>
|
<script type="module" src="/assets/scripts/ui/validator.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>SPF Record Validator</h1>
|
<h1>SPF Record Validator</h1>
|
||||||
@ -27,10 +28,44 @@
|
|||||||
|
|
||||||
<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>
|
||||||
|
<a href="/spf-creator">SPF Creator Tool</a> •
|
||||||
<a href="/dmarc-validator">DMARC Validator Tool</a>
|
<a href="/dmarc-validator">DMARC Validator Tool</a>
|
||||||
</center>
|
</center>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user