+ ${field.getInputHtml()}
+ `;
+ }
+}
+
+document.getElementById("form").onchange = () => generate();
+
+generate();
+
+function generate() {
+ const tool = new Tool();
+
+ document.getElementById("record").value = tool.fieldsToString();
+}
+
+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 1c069f1..50bd693 100644
--- a/assets/scripts/fields/ConstantField.js
+++ b/assets/scripts/fields/ConstantField.js
@@ -10,4 +10,8 @@ export class ConstantField extends Field {
validate(value) {
if (this.value !== value) throw new ValidationError(`Field ${this.key} must be "${this.value}"`)
}
+
+ getInputValue() {
+ return this.value;
+ }
}
diff --git a/assets/scripts/fields/DmarcUriListField.js b/assets/scripts/fields/DmarcUriListField.js
index 21ac4a4..b90552d 100644
--- a/assets/scripts/fields/DmarcUriListField.js
+++ b/assets/scripts/fields/DmarcUriListField.js
@@ -21,4 +21,16 @@ export class DmarcUriListField extends Field {
return true;
}
+
+ getInputHtml() {
+ return ``;
+ }
+
+ getInputValue() {
+ if (!document.getElementById(this.id).value) {
+ return null;
+ }
+
+ return "mailto:" + document.getElementById(this.id).value;
+ }
}
diff --git a/assets/scripts/fields/EnumField.js b/assets/scripts/fields/EnumField.js
index f3d7b1a..55e64ef 100644
--- a/assets/scripts/fields/EnumField.js
+++ b/assets/scripts/fields/EnumField.js
@@ -5,6 +5,7 @@ export class EnumField extends Field {
constructor(key, values) {
super(key);
this.values = values;
+ this.optionLabels = values;
}
validate(value) {
@@ -13,4 +14,24 @@ export class EnumField extends Field {
throw new ValidationError(`Invalid value for field "${this.key}" - must be one of: ${this.values.join(", ")}`);
}
+
+ getInputHtml() {
+ return ``;
+ }
+
+ getInputValue() {
+ return document.getElementById(this.id).value;
+ }
+
+ options(options) {
+ this.optionLabels = options;
+ return this;
+ }
}
diff --git a/assets/scripts/fields/Field.js b/assets/scripts/fields/Field.js
index 44242c8..b54614f 100644
--- a/assets/scripts/fields/Field.js
+++ b/assets/scripts/fields/Field.js
@@ -1,17 +1,33 @@
export class Field {
isRequired = false;
defaultValue = null;
+ displayName = null;
+ description = null;
+ categoryName = null;
requiredIndex = null;
afterFieldName = null;
constructor(key) {
this.key = key;
+ this.id = "field-" + key;
}
+ // Virtual methods
+
validate() {
return true;
}
+ getInputHtml() {
+ return null;
+ }
+
+ getInputValue() {
+ return null;
+ }
+
+ // Builder methods
+
required() {
this.isRequired = true;
return this;
@@ -22,6 +38,21 @@ export class Field {
return this;
}
+ label(label) {
+ this.displayName = label;
+ return this;
+ }
+
+ desc(description) {
+ this.description = description;
+ return this;
+ }
+
+ category(category) {
+ this.categoryName = category;
+ return this;
+ }
+
atIndex(i) {
this.requiredIndex = i;
return this;
diff --git a/assets/scripts/fields/IntField.js b/assets/scripts/fields/IntField.js
index 0c08fe7..dddaa68 100644
--- a/assets/scripts/fields/IntField.js
+++ b/assets/scripts/fields/IntField.js
@@ -21,4 +21,12 @@ export class IntField extends Field {
return true;
}
+
+ getInputHtml() {
+ return ``;
+ }
+
+ getInputValue() {
+ return document.getElementById(this.id).value;
+ }
}
diff --git a/assets/scripts/tools/DmarcTool.js b/assets/scripts/tools/DmarcTool.js
index 3e01920..c8cbd71 100644
--- a/assets/scripts/tools/DmarcTool.js
+++ b/assets/scripts/tools/DmarcTool.js
@@ -7,35 +7,94 @@ import { DnsTool } from "./DnsTool.js";
import { ValidationError } from "../ValidationError.js";
export class DmarcTool extends DnsTool {
- fields = [
- new ConstantField("v", "DMARC1").required().atIndex(0),
- new EnumField("p", ["none", "quarantine", "reject"]).required().atIndex(1).after("v"),
- new EnumField("sp", ["none", "quarantine", "reject"]),
- new IntField("pct", 0, 100).default(100),
- new EnumField("adkim", ["r", "s"]).default("r"),
- new EnumField("aspf", ["r", "s"]).default("r"),
- new EnumField("fo", ["0", "1", "d", "s"]).default("0"),
- new Field("rf").default("afrf"),
- new IntField("ri", 0, 2 ** 32).default(86400),
- new DmarcUriListField("rua"),
- new DmarcUriListField("ruf"),
+ static fields = [
+ new ConstantField("v", "DMARC1")
+ .required()
+ .atIndex(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"),
+
+ new EnumField("adkim", ["r", "s"])
+ .label("DKIM")
+ .desc("How strictly to handle DKIM validation")
+ .options(["Relaxed", "Strict"])
+ .default("r"),
+
+ new EnumField("aspf", ["r", "s"])
+ .label("SPF")
+ .desc("How strictly to handle SPF validation")
+ .options(["Relaxed", "Strict"])
+ .default("r"),
+
+ 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"]),
+
+ 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),
+
+ new DmarcUriListField("ruf")
+ .label("Send failure reports to")
+ .desc("When DMARC validation fails, reports are sent to this email")
+ .category("failure-reporting"),
+
+ new EnumField("fo", ["0", "1", "d", "s"])
+ .label("Failure reporting options")
+ .desc("Define how reports will be generated")
+ .category("failure-reporting")
+ .options([
+ "Generate DMARC failure report if any fail",
+ "Generate DMARC failure report if all fail",
+ "Generate DKIM failure report",
+ "Generate SPF failure report",
+ ])
+ .default("0"),
+
+ new DmarcUriListField("rua")
+ .label("Send aggregate feedback to")
+ .desc("Aggregate reports will be sent to this email, if defined")
+ .category("failure-reporting"),
+
+ new IntField("ri", 0, 2 ** 32)
+ .label("Aggregate report interval")
+ .desc("Interval (in seconds) between aggregate reports")
+ .category("failure-reporting")
+ .default(86400),
+
+ new Field("rf").default("afrf"), // Other values not supported
];
+ static categories = {
+ "advanced": "Advanced",
+ "failure-reporting": "Failure reporting",
+ };
+
constructor(text) {
super(text);
}
tokenize() {
- return this.text.split(/;\s*/);
+ return this.text
+ .replace(/;\s+$/, "")
+ .split(/;\s*/);
}
getKeyValues() {
const result = [];
for (const token of this.tokenize()) {
- if (token === "") continue;
-
- const [key, value] = token.split("=");
+ const [key, value] = token.split(/\s*=\s*/);
if (!value) {
throw new ValidationError(`Field "${key}" is missing a value`);
@@ -46,4 +105,12 @@ export class DmarcTool extends DnsTool {
return result;
}
+
+ 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("; ");
+ }
}
diff --git a/assets/scripts/tools/DnsTool.js b/assets/scripts/tools/DnsTool.js
index 81f1e98..f198c24 100644
--- a/assets/scripts/tools/DnsTool.js
+++ b/assets/scripts/tools/DnsTool.js
@@ -1,7 +1,7 @@
import { ValidationError } from "../ValidationError.js";
export class DnsTool {
- fields = [];
+ static fields = [];
constructor(text) {
this.text = text;
@@ -15,7 +15,7 @@ export class DnsTool {
const values = this.getKeyValues();
// Validate field order
- for (const field of this.fields) {
+ for (const field of this.constructor.fields) {
const valueIdx = values.findIndex(v => v.key === field.key);
if (field.isRequired && valueIdx === -1) {
@@ -32,7 +32,7 @@ export class DnsTool {
// Validate field values
for (let i = 0; i < values.length; i++) {
const input = values[i];
- const field = this.fields.find(f => f.key === input.key);
+ const field = this.constructor.fields.find(f => f.key === input.key);
if (!field) {
throw new ValidationError(`Unknown field: ${input.key}`)
diff --git a/assets/scripts/validator.js b/assets/scripts/validator.js
index 1090592..0a1e7e6 100644
--- a/assets/scripts/validator.js
+++ b/assets/scripts/validator.js
@@ -6,14 +6,14 @@ const tools = {
const Tool = tools[location.pathname];
-document.getElementById("input").oninput = event => validate(event.target.value);
+document.getElementById("record").oninput = event => validate(event.target.value);
-if (document.getElementById("input").value !== "") {
- validate(document.getElementById("input").value);
+if (document.getElementById("record").value !== "") {
+ validate(document.getElementById("record").value);
}
function validate(value) {
- document.getElementById("input").classList.remove("valid", "invalid");
+ document.getElementById("record").classList.remove("valid", "invalid");
document.getElementById("error").style.display = "none"
document.getElementById("success").style.display = "none";
@@ -29,10 +29,10 @@ function validate(value) {
try {
tool.validate();
- document.getElementById("input").classList.add("valid");
+ document.getElementById("record").classList.add("valid");
document.getElementById("success").style.display = "flex";
} catch (e) {
- document.getElementById("input").classList.add("invalid");
+ document.getElementById("record").classList.add("invalid");
document.getElementById("error").style.display = "flex";
document.getElementById("error-message").innerText = e.message;
}
diff --git a/assets/styles/main.css b/assets/styles/main.css
index ae2fc63..8b7f7c8 100644
--- a/assets/styles/main.css
+++ b/assets/styles/main.css
@@ -26,7 +26,7 @@ body {
margin: auto;
}
-h1 {
+h1, h2 {
text-align: center;
}
@@ -35,7 +35,7 @@ label {
font-size: 0.8rem;
}
-#input {
+#record {
font-family: "JetBrains Mono", monospace;
border: 2px solid black;
font-size: 1rem;
@@ -45,20 +45,24 @@ label {
transition: all 200ms;
}
-#input:hover {
+#record:disabled {
+ background-color: #F9F9F9;
+}
+
+#record:not(:disabled):hover {
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
}
-#input:focus {
+#record:focus {
outline: none;
- box-shadow: 0 4px 4px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 4px 4px rgba(0, 0, 0, 0.1) !important;
}
-#input.valid {
+#record.valid {
background-color: #C8E6C9;
}
-#input.invalid {
+#record.invalid {
background-color: #FFCDD2;
}
@@ -66,7 +70,7 @@ main {
background-color: white;
border: 1px solid #BDBDBD;
padding: 1rem 2rem;
- margin-top: 2rem;
+ margin: 2rem auto;
color: #424242;
font-size: 0.8rem;
}
@@ -75,6 +79,12 @@ a {
color: #039BE5;
}
+hr {
+ border: none;
+ border-bottom: 1px solid #BDBDBD;
+ margin: 2rem auto;
+}
+
li:not(:first-of-type) {
margin-top: 0.75rem;
}
@@ -100,3 +110,46 @@ li:not(:first-of-type) {
#result-placeholder {
display: flex;
}
+
+form label {
+ color: black;
+ font-weight: bold;
+ display: block;
+ margin-top: 1.5rem;
+ margin-bottom: 0.2rem;
+}
+
+form input {
+ border: 1px solid #BDBDBD;
+ padding: 0.5rem 1rem;
+ transition: border 200ms;
+}
+
+form input:focus {
+ border-color: black;
+ outline: none;
+}
+
+form select {
+ appearance: none;
+ padding: 0.5rem 2rem 0.5rem 1rem;
+ background-color: #F9F9F9;
+ border: 1px solid #BDBDBD;
+ background-image: url('data:image/svg+xml;utf8,');
+ background-repeat: no-repeat;
+ background-position: right 6px center;
+}
+
+form .description {
+ margin: 0 0 0.5rem 0;
+}
+
+summary {
+ padding-top: 1rem;
+ transition: color 200ms;
+ cursor: pointer;
+}
+
+summary:hover {
+ color: black;
+}
diff --git a/dmarc-creator/index.html b/dmarc-creator/index.html
new file mode 100644
index 0000000..73d533d
--- /dev/null
+++ b/dmarc-creator/index.html
@@ -0,0 +1,40 @@
+
+
+
+
+ DMARC Record Creator - Generate DMARC DNS Records
+
+
+
+
+
DMARC Record Creator
+
+
+
+
+
+
Create a DMARC DNS Record
+
+
Customize the options below to generate a DMARC record, which will be shown in the input field above.
+
+
+
+
+
+
+ This tool allows you to create DMARC DNS records, which can be used when you are hosting an email-server
+ and want to provide an extra layer of security, so other email providers will trust your emails.
+
+
+
+ Select the options you want and copy the text. The DNS record should be a TXT record in the _dmarc
+ subdomain (e.g. _dmarc.example.com) with the content being the text above.
+
+
+
+
diff --git a/dmarc-validator/index.html b/dmarc-validator/index.html
index efad72e..cfd81fd 100644
--- a/dmarc-validator/index.html
+++ b/dmarc-validator/index.html
@@ -2,19 +2,18 @@
- DMARC Validator
+ DMARC Record Validator - Validate DMARC DNS Records
-
DMARC Validator
+
DMARC Record Validator
-
-
+
+
-
-
Paste a DMARC DNS record into the input field to check if it is valid.
+
Paste a DMARC DNS record into the input field to check if it is valid.
@@ -40,20 +39,20 @@
- The content is a list of fields, separated by a semicolon. It must always start with "v=DMARC1" so the
+ The content is a list of tags, separated by a semicolon. It must always start with "v=DMARC1" so the
DMARC record can be easily identified.
- The most important fields are:
+ The most important tags are:
p: Tells what to do with emails that fail validation. Values can be "none" (do nothing),
"quarantine" (treat email as suspicious - e.g. place it in the spam folder) or
- "reject" (outright discard the email upon receival). This field is required and must be placed after
- the "v=DMARC1" field.
+ "reject" (outright discard the email upon receival). This tag is required and must be placed after
+ the "v=DMARC1" tag.