From 8730b7c199e09d36de2a19e80a932868825d3ec1 Mon Sep 17 00:00:00 2001
From: Reimar
Date: Fri, 16 Jan 2026 11:54:09 +0100
Subject: [PATCH] Separate field inputs from fields + other small fixes
---
assets/scripts/Field.js | 17 ++++++--
assets/scripts/FieldInput.js | 29 ++++++++++++++
assets/scripts/records/DmarcRecord.js | 5 ++-
assets/scripts/records/SpfRecord.js | 52 ++++++++++++-------------
assets/scripts/records/TagListRecord.js | 21 +++++-----
assets/scripts/spf/IPv6Mechanism.js | 2 +-
assets/scripts/spf/Mechanism.js | 14 +++----
assets/scripts/spf/Modifier.js | 8 ++--
assets/scripts/spf/Term.js | 24 +++++++++++-
assets/scripts/spf/VersionTerm.js | 2 +-
assets/scripts/tags/ConstantTag.js | 6 ++-
assets/scripts/tags/DmarcUriListTag.js | 12 +++---
assets/scripts/tags/EnumTag.js | 8 ++--
assets/scripts/tags/IntTag.js | 8 ++--
assets/scripts/tags/Tag.js | 16 ++++++++
assets/scripts/ui/creator.js | 23 +++++------
spf-creator/index.html | 6 +--
17 files changed, 167 insertions(+), 86 deletions(-)
create mode 100644 assets/scripts/FieldInput.js
diff --git a/assets/scripts/Field.js b/assets/scripts/Field.js
index 3a3551e..87bc400 100644
--- a/assets/scripts/Field.js
+++ b/assets/scripts/Field.js
@@ -1,16 +1,22 @@
+import { FieldInput } from "./FieldInput.js";
+
/**
- * Defines any key-value pair in any type of DNS-record
- * Used for input fields on DNS creator pages
+ * Represents any key-value pair in any type of DNS-record
+ * Used for validation and to create input fields on DNS creator pages
*/
export class Field {
displayName = null;
description = null;
categoryName = null;
isHidden = false;
+ isDisabled = false;
constructor(key) {
this.key = key;
- this.id = "field-" + key;
+ }
+
+ createInput(parentElem) {
+ return new FieldInput(this, parentElem);
}
// Virtual methods
@@ -44,4 +50,9 @@ export class Field {
this.isHidden = true;
return this;
}
+
+ disabled() {
+ this.isDisabled = true;
+ return this;
+ }
}
diff --git a/assets/scripts/FieldInput.js b/assets/scripts/FieldInput.js
new file mode 100644
index 0000000..20ba004
--- /dev/null
+++ b/assets/scripts/FieldInput.js
@@ -0,0 +1,29 @@
+/**
+ * Represents the actual input element on the creator tool
+ * A field may have multiple inputs if it allows multiple values
+ */
+export class FieldInput {
+ constructor(field, parentElem) {
+ this.field = field;
+ this.id = "field-" + field.key;
+
+ if (!field.isHidden)
+ parentElem.innerHTML += `
+
+
${field.description ?? ""}
+ ${field.getInputHtml(this.id)}
+ `;
+ }
+
+ isValid() {
+ return this.field.isValidInput(this.id);
+ }
+
+ getValue() {
+ return this.field.getInputValue(this.id);
+ }
+
+ toString() {
+ return this.field.inputToString(this.id);
+ }
+}
diff --git a/assets/scripts/records/DmarcRecord.js b/assets/scripts/records/DmarcRecord.js
index 326a629..1c797a5 100644
--- a/assets/scripts/records/DmarcRecord.js
+++ b/assets/scripts/records/DmarcRecord.js
@@ -9,6 +9,7 @@ export class DmarcRecord extends TagListRecord {
static fields = [
new ConstantTag("v", "DMARC1")
.required()
+ .hidden()
.pos(0),
new EnumTag("p", ["none", "quarantine", "reject"])
@@ -77,7 +78,9 @@ export class DmarcRecord extends TagListRecord {
.default(86400)
.pos(2),
- new Tag("rf").default("afrf"), // Other values not supported
+ new Tag("rf")
+ .disabled()
+ .default("afrf"), // Other values not supported
];
static categories = {
diff --git a/assets/scripts/records/SpfRecord.js b/assets/scripts/records/SpfRecord.js
index 6705997..fd17a4b 100644
--- a/assets/scripts/records/SpfRecord.js
+++ b/assets/scripts/records/SpfRecord.js
@@ -11,46 +11,47 @@ export class SpfRecord {
static fields = [
new VersionTerm("v", "spf1")
.required()
+ .hidden()
.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)")
+ .desc("Match the IP addresses from the A records of 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)")
+ .desc("Match 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()
+ .disabled()
.value(ValueRequirement.OPTIONAL)
.multiple()
.pos(2),
- new IPv4Mechanism("ipv4")
+ new IPv4Mechanism("ip4")
.label("IPv4 addresses")
- .desc("Select these IP addresses")
+ .desc("Match these IP addresses")
.multiple()
.pos(2),
- new IPv6Mechanism("ipv6")
+ new IPv6Mechanism("ip6")
.label("IPv6 addresses")
- .desc("Select these IP addresses")
+ .desc("Match these IP addresses")
.multiple()
.pos(2),
+ new DomainMechanism("include")
+ .label("Include")
+ .desc("Check the SPF record of another domain. If it passes, return with the selected result")
+ .multiple()
+ .pos(1),
+
new DomainMechanism("exists")
.label("Exists")
.desc("Apply only if this domain exists (can be used with macro expansions)")
@@ -66,7 +67,7 @@ export class SpfRecord {
new Modifier("redirect")
.label("Redirect")
- .desc("Redirect to the SPF record of this domain if no IP addresses matched")
+ .desc("Redirect to the SPF record of this domain if no matches were found")
.category("advanced")
.pos(4),
@@ -85,6 +86,16 @@ export class SpfRecord {
this.text = text;
}
+ static createFromFieldInputs(fieldInputs) {
+ const tokens = fieldInputs
+ .filter(input => !input.field.isDisabled && input.isValid())
+ .map(input => input.toString());
+
+ const text = tokens.join(" ");
+
+ return new this(text);
+ }
+
tokenize() {
return this.text.trim().split(/\s+/);
}
@@ -150,17 +161,4 @@ 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 3a2b073..5dbb2cc 100644
--- a/assets/scripts/records/TagListRecord.js
+++ b/assets/scripts/records/TagListRecord.js
@@ -8,6 +8,17 @@ export class TagListRecord {
this.text = text;
}
+ static createFromFieldInputs(fieldInputs) {
+ const tokens = fieldInputs
+ .filter(input => !input.field.isDisabled && input.isValid())
+ .filter(input => !input.field.defaultValue || input.getValue() !== input.field.defaultValue)
+ .map(input => input.toString());
+
+ const text = tokens.join("; ");
+
+ return new this(text);
+ }
+
tokenize() {
return this.text
.replace(/;\s*$/, "")
@@ -61,14 +72,4 @@ export class TagListRecord {
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("; ");
- }
}
diff --git a/assets/scripts/spf/IPv6Mechanism.js b/assets/scripts/spf/IPv6Mechanism.js
index b794a7d..b34ef9b 100644
--- a/assets/scripts/spf/IPv6Mechanism.js
+++ b/assets/scripts/spf/IPv6Mechanism.js
@@ -9,6 +9,6 @@ export class IPv6Mechanism extends Mechanism {
validate(value) {
// TODO validate
- return true;
+ return !!value;
}
}
diff --git a/assets/scripts/spf/Mechanism.js b/assets/scripts/spf/Mechanism.js
index 4ebd487..715707e 100644
--- a/assets/scripts/spf/Mechanism.js
+++ b/assets/scripts/spf/Mechanism.js
@@ -9,28 +9,28 @@ export class Mechanism extends Term {
super(key);
}
- getInputQualifier() {
- return document.getElementById(this.id + "-qualifier").value;
+ getInputQualifier(id) {
+ return document.getElementById(id + "-qualifier").value;
}
- getInputValue() {
- return document.getElementById(this.id + "-value")?.value;
+ getInputValue(id) {
+ return document.getElementById(id + "-value")?.value;
}
- getInputHtml() {
+ getInputHtml(id) {
const noValue = this.valueRequirement === ValueRequirement.PROHIBITED;
const placeholder = this.placeholder
+ (this.valueRequirement === ValueRequirement.OPTIONAL ? " (Optional)" : "");
return `
-
- 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)
+ Pass (Default): Matching IP addresses are authorized to send emails from this domain
+ Fail: Matching IP addresses are NOT authorized to send emails from this domain
+ Soft fail: Matching 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)