diff --git a/assets/scripts/creator.js b/assets/scripts/creator.js
index 97150ff..ae749fb 100644
--- a/assets/scripts/creator.js
+++ b/assets/scripts/creator.js
@@ -41,6 +41,11 @@ function generate() {
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;
}
diff --git a/assets/scripts/fields/ConstantField.js b/assets/scripts/fields/ConstantField.js
index 50bd693..5ef2fcb 100644
--- a/assets/scripts/fields/ConstantField.js
+++ b/assets/scripts/fields/ConstantField.js
@@ -2,6 +2,8 @@ import { Field } from "./Field.js";
import { ValidationError } from "../ValidationError.js";
export class ConstantField extends Field {
+ separator = "=";
+
constructor(key, value) {
super(key);
this.value = value;
diff --git a/assets/scripts/fields/DmarcUriListField.js b/assets/scripts/fields/DmarcUriListField.js
index b90552d..0f1f412 100644
--- a/assets/scripts/fields/DmarcUriListField.js
+++ b/assets/scripts/fields/DmarcUriListField.js
@@ -2,6 +2,8 @@ import { Field } from "./Field.js";
import { ValidationError } from "../ValidationError.js";
export class DmarcUriListField extends Field {
+ separator = "=";
+
constructor(key) {
super(key);
}
@@ -10,7 +12,7 @@ export class DmarcUriListField extends Field {
const uris = value.split(",");
for (let uri of uris) {
- uri = uri.replace(/!\d+[kmgt]$/);
+ uri = uri.replace(/!\d+[kmgt]?$/);
try {
new URL(uri);
diff --git a/assets/scripts/fields/DomainField.js b/assets/scripts/fields/DomainField.js
new file mode 100644
index 0000000..634a152
--- /dev/null
+++ b/assets/scripts/fields/DomainField.js
@@ -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;
+ }
+}
diff --git a/assets/scripts/fields/EnumField.js b/assets/scripts/fields/EnumField.js
index 55e64ef..8a5f2cc 100644
--- a/assets/scripts/fields/EnumField.js
+++ b/assets/scripts/fields/EnumField.js
@@ -2,6 +2,8 @@ import { Field } from "./Field.js";
import { ValidationError } from "../ValidationError.js";
export class EnumField extends Field {
+ separator = "=";
+
constructor(key, values) {
super(key);
this.values = values;
diff --git a/assets/scripts/fields/Field.js b/assets/scripts/fields/Field.js
index b54614f..abdd651 100644
--- a/assets/scripts/fields/Field.js
+++ b/assets/scripts/fields/Field.js
@@ -1,11 +1,12 @@
export class Field {
+ separator = null;
isRequired = false;
+ allowMultiple = false;
defaultValue = null;
displayName = null;
description = null;
categoryName = null;
- requiredIndex = null;
- afterFieldName = null;
+ position = null;
constructor(key) {
this.key = key;
@@ -33,6 +34,11 @@ export class Field {
return this;
}
+ multiple() {
+ this.allowMultiple = true;
+ return this;
+ }
+
default(value) {
this.defaultValue = value;
return this;
@@ -53,13 +59,8 @@ export class Field {
return this;
}
- atIndex(i) {
- this.requiredIndex = i;
+ pos(i) {
+ this.position = i;
return this;
}
-
- after(field) {
- this.afterFieldName = field;
- return this;
- }
-}
\ No newline at end of file
+}
diff --git a/assets/scripts/fields/IntField.js b/assets/scripts/fields/IntField.js
index dddaa68..4d29b2c 100644
--- a/assets/scripts/fields/IntField.js
+++ b/assets/scripts/fields/IntField.js
@@ -2,6 +2,8 @@ import { Field } from "./Field.js";
import { ValidationError } from "../ValidationError.js";
export class IntField extends Field {
+ separator = "=";
+
constructor(key, min, max) {
super(key);
this.min = min;
diff --git a/assets/scripts/tools/DmarcTool.js b/assets/scripts/tools/DmarcTool.js
index c8cbd71..3037c49 100644
--- a/assets/scripts/tools/DmarcTool.js
+++ b/assets/scripts/tools/DmarcTool.js
@@ -7,47 +7,53 @@ import { DnsTool } from "./DnsTool.js";
import { ValidationError } from "../ValidationError.js";
export class DmarcTool extends DnsTool {
+ static allowWhitespaceAroundSeparator = true;
+
static fields = [
new ConstantField("v", "DMARC1")
.required()
- .atIndex(0),
+ .pos(0),
new EnumField("p", ["none", "quarantine", "reject"])
.label("Mail Receiver policy")
.desc("How to handle failed validations. The email may be quarantined (usually means sent to spam) or rejected")
.options(["None", "Quarantine", "Reject"])
.required()
- .atIndex(1)
- .after("v"),
+ .pos(1),
new EnumField("adkim", ["r", "s"])
.label("DKIM")
.desc("How strictly to handle DKIM validation")
.options(["Relaxed", "Strict"])
- .default("r"),
+ .default("r")
+ .pos(2),
new EnumField("aspf", ["r", "s"])
.label("SPF")
.desc("How strictly to handle SPF validation")
.options(["Relaxed", "Strict"])
- .default("r"),
+ .default("r")
+ .pos(2),
new EnumField("sp", ["none", "quarantine", "reject"])
.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")
.category("advanced")
- .options(["None", "Quarantine", "Reject"]),
+ .options(["None", "Quarantine", "Reject"])
+ .pos(2),
new IntField("pct", 0, 100)
.label("Percentage")
.desc("Percentage of emails to apply DMARC validation on. Useful for split-testing and continuous rollout")
.category("advanced")
- .default(100),
+ .default(100)
+ .pos(2),
new DmarcUriListField("ruf")
.label("Send failure reports to")
.desc("When DMARC validation fails, reports are sent to this email")
- .category("failure-reporting"),
+ .category("failure-reporting")
+ .pos(2),
new EnumField("fo", ["0", "1", "d", "s"])
.label("Failure reporting options")
@@ -59,7 +65,8 @@ export class DmarcTool extends DnsTool {
"Generate DKIM failure report",
"Generate SPF failure report",
])
- .default("0"),
+ .default("0")
+ .pos(2),
new DmarcUriListField("rua")
.label("Send aggregate feedback to")
@@ -70,7 +77,8 @@ export class DmarcTool extends DnsTool {
.label("Aggregate report interval")
.desc("Interval (in seconds) between aggregate reports")
.category("failure-reporting")
- .default(86400),
+ .default(86400)
+ .pos(2),
new Field("rf").default("afrf"), // Other values not supported
];
@@ -86,26 +94,10 @@ export class DmarcTool extends DnsTool {
tokenize() {
return this.text
- .replace(/;\s+$/, "")
+ .replace(/;\s*$/, "")
.split(/;\s*/);
}
- getKeyValues() {
- const result = [];
-
- for (const token of this.tokenize()) {
- const [key, value] = token.split(/\s*=\s*/);
-
- if (!value) {
- throw new ValidationError(`Field "${key}" is missing a value`);
- }
-
- result.push({ key, value });
- }
-
- return result;
- }
-
fieldsToString() {
let tokens = this.constructor.fields
.filter(field => !field.defaultValue || field.getInputValue() !== field.defaultValue)
diff --git a/assets/scripts/tools/DnsTool.js b/assets/scripts/tools/DnsTool.js
index f198c24..16e4b6a 100644
--- a/assets/scripts/tools/DnsTool.js
+++ b/assets/scripts/tools/DnsTool.js
@@ -1,44 +1,70 @@
import { ValidationError } from "../ValidationError.js";
export class DnsTool {
+ static allowWhitespaceAroundSeparator;
static fields = [];
constructor(text) {
this.text = text;
}
- getKeyValues() {
+ tokenize() {
throw new Error("Unimplemented");
}
+ getKeyValues() {
+ const result = [];
+
+ for (const token of this.tokenize()) {
+ 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) {
+ throw new ValidationError(`Field "${key}" is missing a value`);
+ }
+
+ result.push({ key, value });
+ }
+
+ return result;
+ }
+
validate() {
const values = this.getKeyValues();
- // Validate field order
for (const field of this.constructor.fields) {
- const valueIdx = values.findIndex(v => v.key === field.key);
-
- if (field.isRequired && valueIdx === -1) {
+ if (field.isRequired && !values.some(v => v.key === field.key)) {
throw new ValidationError(`Field "${field.key}" is required`);
}
-
- if (field.requiredIndex && field.requiredIndex !== valueIdx) {
- if (field.requiredIndex === 0) throw new ValidationError(`Field "${field.key}" must come first`);
- if (field.afterFieldName) throw new ValidationError(`Field "${field.key}" must come after "${field.afterFieldName}"`);
- throw new ValidationError(`Field "${field.key}" must be at position ${field.requiredIndex + 1}`);
- }
}
- // Validate field values
+ let lastPos = 0;
for (let i = 0; i < values.length; i++) {
const input = values[i];
const field = this.constructor.fields.find(f => f.key === input.key);
if (!field) {
- throw new ValidationError(`Unknown field: ${input.key}`)
+ throw new ValidationError(`Unknown field: ${input.key}`);
+ }
+
+ if (field.position < lastPos) {
+ const lastField = this.constructor.fields.find(f => f.key === values[i-1].key);
+
+ throw new ValidationError(`Field "${lastField.key}" must come after "${field.key}"`);
}
field.validate(input.value);
+
+ lastPos = field.position;
}
return true;
diff --git a/assets/scripts/tools/SpfTool.js b/assets/scripts/tools/SpfTool.js
new file mode 100644
index 0000000..ab0c396
--- /dev/null
+++ b/assets/scripts/tools/SpfTool.js
@@ -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+/);
+ }
+}
diff --git a/assets/scripts/validator.js b/assets/scripts/validator.js
index 0a1e7e6..7ce3b1e 100644
--- a/assets/scripts/validator.js
+++ b/assets/scripts/validator.js
@@ -1,7 +1,9 @@
import { DmarcTool } from "./tools/DmarcTool.js";
+import { SpfTool } from "./tools/SpfTool.js";
const tools = {
"/dmarc-validator": DmarcTool,
+ "/spf-validator": SpfTool,
};
const Tool = tools[location.pathname];
diff --git a/assets/styles/main.css b/assets/styles/main.css
index 8b7f7c8..a881e6c 100644
--- a/assets/styles/main.css
+++ b/assets/styles/main.css
@@ -45,11 +45,7 @@ label {
transition: all 200ms;
}
-#record:disabled {
- background-color: #F9F9F9;
-}
-
-#record:not(:disabled):hover {
+#record:hover {
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
}
diff --git a/dmarc-creator/index.html b/dmarc-creator/index.html
index 73d533d..4d87a9b 100644
--- a/dmarc-creator/index.html
+++ b/dmarc-creator/index.html
@@ -10,7 +10,7 @@
DMARC Record Creator
-
+
Create a DMARC DNS Record
diff --git a/spf-validator/index.html b/spf-validator/index.html
new file mode 100644
index 0000000..80829c2
--- /dev/null
+++ b/spf-validator/index.html
@@ -0,0 +1,38 @@
+
+
+
+
+ SPF Record Validator - Validate SPF DNS Records
+
+
+
+
+ SPF Record Validator
+
+
+
+
+
+ Paste an SPF DNS record into the input field to check if it is valid.
+
+
+
+
+
+
+
+
+ Validation Success
+
+
+
+
+ Insert SPF description
+
+
+ More tools:
+ DMARC Validator Tool
+
+
+
+