diff --git a/assets/scripts/Field.js b/assets/scripts/Field.js
index df80f1a..3a3551e 100644
--- a/assets/scripts/Field.js
+++ b/assets/scripts/Field.js
@@ -5,9 +5,12 @@
export class Field {
displayName = null;
description = null;
+ categoryName = null;
+ isHidden = false;
constructor(key) {
this.key = key;
+ this.id = "field-" + key;
}
// Virtual methods
@@ -31,4 +34,14 @@ export class Field {
this.description = description;
return this;
}
+
+ category(category) {
+ this.categoryName = category;
+ return this;
+ }
+
+ hidden() {
+ this.isHidden = true;
+ return this;
+ }
}
diff --git a/assets/scripts/records/SpfRecord.js b/assets/scripts/records/SpfRecord.js
index a152c31..332cc9d 100644
--- a/assets/scripts/records/SpfRecord.js
+++ b/assets/scripts/records/SpfRecord.js
@@ -14,44 +14,73 @@ export class SpfRecord {
.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")
+ .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;
}
@@ -121,4 +150,17 @@ export class SpfRecord {
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(" ");
+ }
}
diff --git a/assets/scripts/records/TagListRecord.js b/assets/scripts/records/TagListRecord.js
index 4706cce..3a2b073 100644
--- a/assets/scripts/records/TagListRecord.js
+++ b/assets/scripts/records/TagListRecord.js
@@ -62,11 +62,13 @@ export class TagListRecord {
return true;
}
- fieldsToString() {
- let tokens = this.constructor.fields
+ 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("; ");
}
}
diff --git a/assets/scripts/spf/DomainMechanism.js b/assets/scripts/spf/DomainMechanism.js
index 08f0b5f..124086a 100644
--- a/assets/scripts/spf/DomainMechanism.js
+++ b/assets/scripts/spf/DomainMechanism.js
@@ -1,20 +1,16 @@
import { ValidationError } from "../ValidationError.js";
import { Mechanism } from "./Mechanism.js";
+import { validateSpfDomain } from "./utils.js";
export class DomainMechanism extends Mechanism {
- separator = ":";
+ placeholder = "example.com";
constructor(key) {
super(key);
}
validate(value) {
- // https://www.rfc-editor.org/rfc/rfc7208#section-7.1
- const valid = value
- .split(".")
- .every(segment => segment.match(/^([^%]|%_|%%|%-|%\{[slodiphcrtv]\d*r?[-.+,\/_=]*})+$/));
-
- if (!valid) throw new ValidationError(`Value for "${this.key}" is not a valid domain name`);
+ if (!validateSpfDomain(value)) throw new ValidationError(`Value for "${this.key}" is not a valid domain name`);
return true;
}
diff --git a/assets/scripts/spf/IPv4Mechanism.js b/assets/scripts/spf/IPv4Mechanism.js
index e6c3880..3c51d72 100644
--- a/assets/scripts/spf/IPv4Mechanism.js
+++ b/assets/scripts/spf/IPv4Mechanism.js
@@ -2,7 +2,7 @@ import { ValidationError } from "../ValidationError.js";
import { Mechanism } from "./Mechanism.js";
export class IPv4Mechanism extends Mechanism {
- separator = ":";
+ placeholder = "0.0.0.0";
constructor(key) {
super(key);
diff --git a/assets/scripts/spf/IPv6Mechanism.js b/assets/scripts/spf/IPv6Mechanism.js
index a6c370a..b794a7d 100644
--- a/assets/scripts/spf/IPv6Mechanism.js
+++ b/assets/scripts/spf/IPv6Mechanism.js
@@ -1,6 +1,8 @@
import { Mechanism } from "./Mechanism.js";
export class IPv6Mechanism extends Mechanism {
+ placeholder = "2001:db8::1";
+
constructor(key) {
super(key);
}
diff --git a/assets/scripts/spf/Mechanism.js b/assets/scripts/spf/Mechanism.js
index e71ae53..4ebd487 100644
--- a/assets/scripts/spf/Mechanism.js
+++ b/assets/scripts/spf/Mechanism.js
@@ -1,9 +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 `
+
+ ${noValue ? "" : ``}
+ `;
+ }
}
diff --git a/assets/scripts/spf/Modifier.js b/assets/scripts/spf/Modifier.js
index 639e072..890f0c3 100644
--- a/assets/scripts/spf/Modifier.js
+++ b/assets/scripts/spf/Modifier.js
@@ -1,5 +1,6 @@
import { Term } from "./Term.js";
import { ValidationError } from "../ValidationError.js";
+import { validateSpfDomain } from "./utils.js";
export class Modifier extends Term {
separator = "=";
@@ -8,8 +9,17 @@ export class Modifier extends Term {
super(key);
}
- validate() {
- // TODO validate
+ 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 ``;
+ }
}
diff --git a/assets/scripts/spf/Term.js b/assets/scripts/spf/Term.js
index a4a1db0..7777b15 100644
--- a/assets/scripts/spf/Term.js
+++ b/assets/scripts/spf/Term.js
@@ -12,6 +12,12 @@ export class Term extends Field {
super(key)
}
+ // Virtual methods
+
+ getInputQualifier() {
+ return "";
+ }
+
// Builder methods
required() {
diff --git a/assets/scripts/spf/VersionTerm.js b/assets/scripts/spf/VersionTerm.js
index 039220a..0e1d134 100644
--- a/assets/scripts/spf/VersionTerm.js
+++ b/assets/scripts/spf/VersionTerm.js
@@ -14,4 +14,8 @@ export class VersionTerm extends Term {
return true;
}
+
+ getInputValue() {
+ return this.version;
+ }
}
diff --git a/assets/scripts/spf/utils.js b/assets/scripts/spf/utils.js
new file mode 100644
index 0000000..9feb269
--- /dev/null
+++ b/assets/scripts/spf/utils.js
@@ -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?[-.+,\/_=]*})+$/));
+}
diff --git a/assets/scripts/tags/Tag.js b/assets/scripts/tags/Tag.js
index 9214b94..cce9899 100644
--- a/assets/scripts/tags/Tag.js
+++ b/assets/scripts/tags/Tag.js
@@ -4,12 +4,10 @@ import { Field } from "../Field.js";
export class Tag extends Field {
isRequired = false;
defaultValue = null;
- categoryName = null;
position = null;
constructor(key) {
super(key);
- this.id = "tag-" + key;
}
// Virtual methods
@@ -30,11 +28,6 @@ export class Tag extends Field {
return this;
}
- category(category) {
- this.categoryName = category;
- return this;
- }
-
pos(i) {
this.position = i;
return this;
diff --git a/assets/scripts/creator.js b/assets/scripts/ui/creator.js
similarity index 75%
rename from assets/scripts/creator.js
rename to assets/scripts/ui/creator.js
index b7ef1b3..723e688 100644
--- a/assets/scripts/creator.js
+++ b/assets/scripts/ui/creator.js
@@ -1,7 +1,9 @@
-import { DmarcRecord } from "./records/DmarcRecord.js";
+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];
@@ -21,7 +23,7 @@ for (const category of categories) {
function addFields(elem, fields) {
for (const field of fields) {
- if (!field.getInputHtml()) continue;
+ if (field.isHidden || !field.getInputHtml()) continue;
elem.innerHTML += `
@@ -31,14 +33,11 @@ function addFields(elem, fields) {
}
}
-document.getElementById("form").onchange = () => generate();
-
+document.getElementById("form").onchange = generate;
generate();
-function generate() {
- const record = new Record();
-
- document.getElementById("record").value = record.fieldsToString();
+function generate(event) {
+ document.getElementById("record").value = Record.fieldsToString();
}
document.getElementById("record").onclick = (e) => {
diff --git a/assets/scripts/validator.js b/assets/scripts/ui/validator.js
similarity index 90%
rename from assets/scripts/validator.js
rename to assets/scripts/ui/validator.js
index 24757b5..f293867 100644
--- a/assets/scripts/validator.js
+++ b/assets/scripts/ui/validator.js
@@ -1,5 +1,5 @@
-import { DmarcRecord } from "./records/DmarcRecord.js";
-import { SpfRecord } from "./records/SpfRecord.js";
+import { DmarcRecord } from "../records/DmarcRecord.js";
+import { SpfRecord } from "../records/SpfRecord.js";
const records = {
"/dmarc-validator": DmarcRecord,
diff --git a/dmarc-creator/index.html b/dmarc-creator/index.html
index 4d87a9b..d7be9c2 100644
--- a/dmarc-creator/index.html
+++ b/dmarc-creator/index.html
@@ -4,7 +4,7 @@
DMARC Record Creator - Generate DMARC DNS Records
-
+
DMARC Record Creator
diff --git a/dmarc-validator/index.html b/dmarc-validator/index.html
index f5d9f20..cf6d504 100644
--- a/dmarc-validator/index.html
+++ b/dmarc-validator/index.html
@@ -4,7 +4,7 @@
DMARC Record Validator - Validate DMARC DNS Records
-
+
DMARC Record Validator
diff --git a/spf-creator/index.html b/spf-creator/index.html
new file mode 100644
index 0000000..8107615
--- /dev/null
+++ b/spf-creator/index.html
@@ -0,0 +1,59 @@
+
+
+
+
+ SPF Record Creator - Generate SPF DNS Records
+
+
+
+
+
SPF Record Creator
+
+
+
+
+
+
Create an SPF DNS Record
+
+
+ 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:
+
+
+
+ Pass (Default): Selected IP addresses are authorized to send emails from this domain
+ Fail: Selected IP addresses are NOT authorized to send emails from this domain
+ Soft fail: Selected IP addresses might not be authorized to send emails from this domain (actual behavior differs)
+ Neutral: Do not explicitly state whether the IP addresses are authorized or not (can be used for overriding other qualifiers)
+
+
+
+
+
+
+
+ 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.
+
+
+
+ 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.
+
+
+
+ For advanced usage, domain fields may contain macros. These start with a percentage sign and will expand
+ to a dynamic value. For example, %{d} expands to the current domain and %{i} to the
+ current IP address. See
+ the SPF specification
+ for a list of macros you can use.
+