Compare commits
No commits in common. "f8e10f32d0c29dc7fcdc0f63af0663fa00e68603" and "82c4579c585a0647b4e5a668536dd54791ea45f5" have entirely different histories.
f8e10f32d0
...
82c4579c58
51
assets/scripts/creator.js
Normal file
51
assets/scripts/creator.js
Normal file
@ -0,0 +1,51 @@
|
||||
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,7 +1,9 @@
|
||||
import { Tag } from "./Tag.js";
|
||||
import { Field } from "./Field.js";
|
||||
import { ValidationError } from "../ValidationError.js";
|
||||
|
||||
export class ConstantTag extends Tag {
|
||||
export class ConstantField extends Field {
|
||||
separator = "=";
|
||||
|
||||
constructor(key, value) {
|
||||
super(key);
|
||||
this.value = value;
|
||||
@ -1,7 +1,9 @@
|
||||
import { Tag } from "./Tag.js";
|
||||
import { Field } from "./Field.js";
|
||||
import { ValidationError } from "../ValidationError.js";
|
||||
|
||||
export class DmarcUriListTag extends Tag {
|
||||
export class DmarcUriListField extends Field {
|
||||
separator = "=";
|
||||
|
||||
constructor(key) {
|
||||
super(key);
|
||||
}
|
||||
@ -15,7 +17,7 @@ export class DmarcUriListTag extends Tag {
|
||||
try {
|
||||
new URL(uri);
|
||||
} catch(e) {
|
||||
throw new ValidationError(`Invalid URI for tag "${this.key}": ${uri}`);
|
||||
throw new ValidationError(`Invalid URI for field "${this.key}": ${uri}`);
|
||||
}
|
||||
}
|
||||
|
||||
17
assets/scripts/fields/DomainField.js
Normal file
17
assets/scripts/fields/DomainField.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
import { Tag } from "./Tag.js";
|
||||
import { Field } from "./Field.js";
|
||||
import { ValidationError } from "../ValidationError.js";
|
||||
|
||||
export class EnumTag extends Tag {
|
||||
export class EnumField extends Field {
|
||||
separator = "=";
|
||||
|
||||
constructor(key, values) {
|
||||
super(key);
|
||||
this.values = values;
|
||||
@ -12,7 +14,7 @@ export class EnumTag extends Tag {
|
||||
if (this.values.includes(value))
|
||||
return true;
|
||||
|
||||
throw new ValidationError(`Invalid value for tag "${this.key}" - must be one of: ${this.values.join(", ")}`);
|
||||
throw new ValidationError(`Invalid value for field "${this.key}" - must be one of: ${this.values.join(", ")}`);
|
||||
}
|
||||
|
||||
getInputHtml() {
|
||||
@ -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 {
|
||||
separator = null;
|
||||
isRequired = false;
|
||||
allowMultiple = false;
|
||||
defaultValue = null;
|
||||
displayName = null;
|
||||
description = null;
|
||||
categoryName = null;
|
||||
isHidden = false;
|
||||
position = null;
|
||||
|
||||
constructor(key) {
|
||||
this.key = key;
|
||||
@ -15,6 +15,10 @@ export class Field {
|
||||
|
||||
// Virtual methods
|
||||
|
||||
validate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getInputHtml() {
|
||||
return null;
|
||||
}
|
||||
@ -25,6 +29,21 @@ export class Field {
|
||||
|
||||
// Builder methods
|
||||
|
||||
required() {
|
||||
this.isRequired = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
multiple() {
|
||||
this.allowMultiple = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
default(value) {
|
||||
this.defaultValue = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
label(label) {
|
||||
this.displayName = label;
|
||||
return this;
|
||||
@ -40,8 +59,8 @@ export class Field {
|
||||
return this;
|
||||
}
|
||||
|
||||
hidden() {
|
||||
this.isHidden = true;
|
||||
pos(i) {
|
||||
this.position = i;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
import { Tag } from "./Tag.js";
|
||||
import { Field } from "./Field.js";
|
||||
import { ValidationError } from "../ValidationError.js";
|
||||
|
||||
export class IntTag extends Tag {
|
||||
export class IntField extends Field {
|
||||
separator = "=";
|
||||
|
||||
constructor(key, min, max) {
|
||||
super(key);
|
||||
this.min = min;
|
||||
@ -1,166 +0,0 @@
|
||||
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,17 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
import { Mechanism } from "./Mechanism.js";
|
||||
|
||||
export class IPv6Mechanism extends Mechanism {
|
||||
placeholder = "2001:db8::1";
|
||||
|
||||
constructor(key) {
|
||||
super(key);
|
||||
}
|
||||
|
||||
validate(value) {
|
||||
// TODO validate
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
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}">`}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
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">`;
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
export const ValueRequirement = {
|
||||
REQUIRED: "required",
|
||||
OPTIONAL: "optional",
|
||||
PROHIBITED: "prohibited",
|
||||
};
|
||||
@ -1,21 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
// 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,35 +0,0 @@
|
||||
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,58 +1,61 @@
|
||||
import { ConstantTag } from "../tags/ConstantTag.js";
|
||||
import { EnumTag } from "../tags/EnumTag.js";
|
||||
import { IntTag } from "../tags/IntTag.js";
|
||||
import { Tag } from "../tags/Tag.js";
|
||||
import { DmarcUriListTag } from "../tags/DmarcUriListTag.js";
|
||||
import { TagListRecord } from "./TagListRecord.js";
|
||||
import { ConstantField } from "../fields/ConstantField.js";
|
||||
import { EnumField } from "../fields/EnumField.js";
|
||||
import { IntField } from "../fields/IntField.js";
|
||||
import { Field } from "../fields/Field.js";
|
||||
import { DmarcUriListField } from "../fields/DmarcUriListField.js";
|
||||
import { DnsTool } from "./DnsTool.js";
|
||||
import { ValidationError } from "../ValidationError.js";
|
||||
|
||||
export class DmarcTool extends DnsTool {
|
||||
static allowWhitespaceAroundSeparator = true;
|
||||
|
||||
export class DmarcRecord extends TagListRecord {
|
||||
static fields = [
|
||||
new ConstantTag("v", "DMARC1")
|
||||
new ConstantField("v", "DMARC1")
|
||||
.required()
|
||||
.pos(0),
|
||||
|
||||
new EnumTag("p", ["none", "quarantine", "reject"])
|
||||
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()
|
||||
.pos(1),
|
||||
|
||||
new EnumTag("adkim", ["r", "s"])
|
||||
new EnumField("adkim", ["r", "s"])
|
||||
.label("DKIM")
|
||||
.desc("How strictly to handle DKIM validation")
|
||||
.options(["Relaxed", "Strict"])
|
||||
.default("r")
|
||||
.pos(2),
|
||||
|
||||
new EnumTag("aspf", ["r", "s"])
|
||||
new EnumField("aspf", ["r", "s"])
|
||||
.label("SPF")
|
||||
.desc("How strictly to handle SPF validation")
|
||||
.options(["Relaxed", "Strict"])
|
||||
.default("r")
|
||||
.pos(2),
|
||||
|
||||
new EnumTag("sp", ["none", "quarantine", "reject"])
|
||||
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"])
|
||||
.pos(2),
|
||||
|
||||
new IntTag("pct", 0, 100)
|
||||
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)
|
||||
.pos(2),
|
||||
|
||||
new DmarcUriListTag("ruf")
|
||||
new DmarcUriListField("ruf")
|
||||
.label("Send failure reports to")
|
||||
.desc("When DMARC validation fails, reports are sent to this email")
|
||||
.category("failure-reporting")
|
||||
.pos(2),
|
||||
|
||||
new EnumTag("fo", ["0", "1", "d", "s"])
|
||||
new EnumField("fo", ["0", "1", "d", "s"])
|
||||
.label("Failure reporting options")
|
||||
.desc("Define how reports will be generated")
|
||||
.category("failure-reporting")
|
||||
@ -65,19 +68,19 @@ export class DmarcRecord extends TagListRecord {
|
||||
.default("0")
|
||||
.pos(2),
|
||||
|
||||
new DmarcUriListTag("rua")
|
||||
new DmarcUriListField("rua")
|
||||
.label("Send aggregate feedback to")
|
||||
.desc("Aggregate reports will be sent to this email, if defined")
|
||||
.category("failure-reporting"),
|
||||
|
||||
new IntTag("ri", 0, 2 ** 32)
|
||||
new IntField("ri", 0, 2 ** 32)
|
||||
.label("Aggregate report interval")
|
||||
.desc("Interval (in seconds) between aggregate reports")
|
||||
.category("failure-reporting")
|
||||
.default(86400)
|
||||
.pos(2),
|
||||
|
||||
new Tag("rf").default("afrf"), // Other values not supported
|
||||
new Field("rf").default("afrf"), // Other values not supported
|
||||
];
|
||||
|
||||
static categories = {
|
||||
@ -88,4 +91,18 @@ export class DmarcRecord extends TagListRecord {
|
||||
constructor(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("; ");
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { ValidationError } from "../ValidationError.js";
|
||||
|
||||
/** Common class for DMARC/DKIM which both use semicolon-separated tag-value lists */
|
||||
export class TagListRecord {
|
||||
export class DnsTool {
|
||||
static allowWhitespaceAroundSeparator;
|
||||
static fields = [];
|
||||
|
||||
constructor(text) {
|
||||
@ -9,19 +9,27 @@ export class TagListRecord {
|
||||
}
|
||||
|
||||
tokenize() {
|
||||
return this.text
|
||||
.replace(/;\s*$/, "")
|
||||
.split(/;\s*/);
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
getKeyValues() {
|
||||
const result = [];
|
||||
|
||||
for (const token of this.tokenize()) {
|
||||
const [key, value] = token.split(/\s*=\s*/);
|
||||
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(`Tag "${key}" is missing a value`);
|
||||
throw new ValidationError(`Field "${key}" is missing a value`);
|
||||
}
|
||||
|
||||
result.push({ key, value });
|
||||
@ -35,7 +43,7 @@ export class TagListRecord {
|
||||
|
||||
for (const field of this.constructor.fields) {
|
||||
if (field.isRequired && !values.some(v => v.key === field.key)) {
|
||||
throw new ValidationError(`Tag "${field.key}" is required`);
|
||||
throw new ValidationError(`Field "${field.key}" is required`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,13 +53,13 @@ export class TagListRecord {
|
||||
const field = this.constructor.fields.find(f => f.key === input.key);
|
||||
|
||||
if (!field) {
|
||||
throw new ValidationError(`Unknown tag: ${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(`Tag "${lastField.key}" must come after "${field.key}"`);
|
||||
throw new ValidationError(`Field "${lastField.key}" must come after "${field.key}"`);
|
||||
}
|
||||
|
||||
field.validate(input.value);
|
||||
@ -61,14 +69,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("; ");
|
||||
}
|
||||
}
|
||||
57
assets/scripts/tools/SpfTool.js
Normal file
57
assets/scripts/tools/SpfTool.js
Normal file
@ -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+/);
|
||||
}
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
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 { DmarcRecord } from "../records/DmarcRecord.js";
|
||||
import { SpfRecord } from "../records/SpfRecord.js";
|
||||
import { DmarcTool } from "./tools/DmarcTool.js";
|
||||
import { SpfTool } from "./tools/SpfTool.js";
|
||||
|
||||
const records = {
|
||||
"/dmarc-validator": DmarcRecord,
|
||||
"/spf-validator": SpfRecord,
|
||||
const tools = {
|
||||
"/dmarc-validator": DmarcTool,
|
||||
"/spf-validator": SpfTool,
|
||||
};
|
||||
|
||||
const Record = records[location.pathname];
|
||||
const Tool = tools[location.pathname];
|
||||
|
||||
document.getElementById("record").oninput = event => validate(event.target.value);
|
||||
|
||||
@ -26,10 +26,10 @@ function validate(value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const record = new Record(value);
|
||||
const tool = new Tool(value);
|
||||
|
||||
try {
|
||||
record.validate();
|
||||
tool.validate();
|
||||
|
||||
document.getElementById("record").classList.add("valid");
|
||||
document.getElementById("success").style.display = "flex";
|
||||
@ -24,7 +24,6 @@ body {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
@ -76,20 +75,6 @@ a {
|
||||
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 {
|
||||
border: none;
|
||||
border-bottom: 1px solid #BDBDBD;
|
||||
|
||||
@ -2,10 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DMARC Record Creator - Generate DMARC DNS Records</title>
|
||||
<link rel="stylesheet" href="/assets/styles/main.css">
|
||||
<script type="module" src="/assets/scripts/ui/creator.js"></script>
|
||||
<script type="module" src="/assets/scripts/creator.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DMARC Record Creator</h1>
|
||||
@ -34,8 +33,7 @@
|
||||
|
||||
<center>
|
||||
<h3>More tools:</h3>
|
||||
<a href="/dmarc-validator">DMARC Validator Tool</a> •
|
||||
<a href="/spf-creator">SPF Creator Tool</a>
|
||||
<a href="/dmarc-validator">DMARC Validator Tool</a>
|
||||
</center>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
@ -2,10 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DMARC Record Validator - Validate DMARC DNS Records</title>
|
||||
<link rel="stylesheet" href="/assets/styles/main.css">
|
||||
<script type="module" src="/assets/scripts/ui/validator.js"></script>
|
||||
<script type="module" src="/assets/scripts/validator.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DMARC Record Validator</h1>
|
||||
@ -29,7 +28,7 @@
|
||||
<p id="result-placeholder" class="validation-result"></p>
|
||||
|
||||
<p>
|
||||
DMARC is a standard for email servers to tell how to handle validation errors in SPF and DKIM.
|
||||
DMARC is a standard for web 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
|
||||
failures etc.
|
||||
</p>
|
||||
@ -74,8 +73,7 @@
|
||||
|
||||
<center>
|
||||
<h3>More tools:</h3>
|
||||
<a href="/dmarc-creator">DMARC Creator Tool</a> •
|
||||
<a href="/spf-validator">SPF Validator Tool</a>
|
||||
<a href="/dmarc-creator">DMARC Creator Tool</a>
|
||||
</center>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
<!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>
|
||||
@ -1,153 +0,0 @@
|
||||
<!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,10 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SPF Record Validator - Validate SPF DNS Records</title>
|
||||
<link rel="stylesheet" href="/assets/styles/main.css">
|
||||
<script type="module" src="/assets/scripts/ui/validator.js"></script>
|
||||
<script type="module" src="/assets/scripts/validator.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>SPF Record Validator</h1>
|
||||
@ -28,44 +27,10 @@
|
||||
|
||||
<p id="result-placeholder" class="validation-result"></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>
|
||||
<p>Insert SPF description</p>
|
||||
|
||||
<center>
|
||||
<h3>More tools:</h3>
|
||||
<a href="/spf-creator">SPF Creator Tool</a> •
|
||||
<a href="/dmarc-validator">DMARC Validator Tool</a>
|
||||
</center>
|
||||
</main>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user