WIP add SPF validator
This commit is contained in:
parent
75cf0abb1a
commit
82c4579c58
@ -41,6 +41,11 @@ function generate() {
|
|||||||
document.getElementById("record").value = tool.fieldsToString();
|
document.getElementById("record").value = tool.fieldsToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.getElementById("record").onclick = (e) => {
|
||||||
|
e.target.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
}
|
||||||
|
|
||||||
function isUnique(value, index, array) {
|
function isUnique(value, index, array) {
|
||||||
return array.indexOf(value) === index;
|
return array.indexOf(value) === index;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { Field } from "./Field.js";
|
|||||||
import { ValidationError } from "../ValidationError.js";
|
import { ValidationError } from "../ValidationError.js";
|
||||||
|
|
||||||
export class ConstantField extends Field {
|
export class ConstantField extends Field {
|
||||||
|
separator = "=";
|
||||||
|
|
||||||
constructor(key, value) {
|
constructor(key, value) {
|
||||||
super(key);
|
super(key);
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { Field } from "./Field.js";
|
|||||||
import { ValidationError } from "../ValidationError.js";
|
import { ValidationError } from "../ValidationError.js";
|
||||||
|
|
||||||
export class DmarcUriListField extends Field {
|
export class DmarcUriListField extends Field {
|
||||||
|
separator = "=";
|
||||||
|
|
||||||
constructor(key) {
|
constructor(key) {
|
||||||
super(key);
|
super(key);
|
||||||
}
|
}
|
||||||
@ -10,7 +12,7 @@ export class DmarcUriListField extends Field {
|
|||||||
const uris = value.split(",");
|
const uris = value.split(",");
|
||||||
|
|
||||||
for (let uri of uris) {
|
for (let uri of uris) {
|
||||||
uri = uri.replace(/!\d+[kmgt]$/);
|
uri = uri.replace(/!\d+[kmgt]?$/);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
new URL(uri);
|
new URL(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,8 @@ import { Field } from "./Field.js";
|
|||||||
import { ValidationError } from "../ValidationError.js";
|
import { ValidationError } from "../ValidationError.js";
|
||||||
|
|
||||||
export class EnumField extends Field {
|
export class EnumField extends Field {
|
||||||
|
separator = "=";
|
||||||
|
|
||||||
constructor(key, values) {
|
constructor(key, values) {
|
||||||
super(key);
|
super(key);
|
||||||
this.values = values;
|
this.values = values;
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
export class Field {
|
export class Field {
|
||||||
|
separator = null;
|
||||||
isRequired = false;
|
isRequired = false;
|
||||||
|
allowMultiple = false;
|
||||||
defaultValue = null;
|
defaultValue = null;
|
||||||
displayName = null;
|
displayName = null;
|
||||||
description = null;
|
description = null;
|
||||||
categoryName = null;
|
categoryName = null;
|
||||||
requiredIndex = null;
|
position = null;
|
||||||
afterFieldName = null;
|
|
||||||
|
|
||||||
constructor(key) {
|
constructor(key) {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
@ -33,6 +34,11 @@ export class Field {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
multiple() {
|
||||||
|
this.allowMultiple = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
default(value) {
|
default(value) {
|
||||||
this.defaultValue = value;
|
this.defaultValue = value;
|
||||||
return this;
|
return this;
|
||||||
@ -53,13 +59,8 @@ export class Field {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
atIndex(i) {
|
pos(i) {
|
||||||
this.requiredIndex = i;
|
this.position = i;
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
after(field) {
|
|
||||||
this.afterFieldName = field;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,6 +2,8 @@ import { Field } from "./Field.js";
|
|||||||
import { ValidationError } from "../ValidationError.js";
|
import { ValidationError } from "../ValidationError.js";
|
||||||
|
|
||||||
export class IntField extends Field {
|
export class IntField extends Field {
|
||||||
|
separator = "=";
|
||||||
|
|
||||||
constructor(key, min, max) {
|
constructor(key, min, max) {
|
||||||
super(key);
|
super(key);
|
||||||
this.min = min;
|
this.min = min;
|
||||||
|
|||||||
@ -7,47 +7,53 @@ import { DnsTool } from "./DnsTool.js";
|
|||||||
import { ValidationError } from "../ValidationError.js";
|
import { ValidationError } from "../ValidationError.js";
|
||||||
|
|
||||||
export class DmarcTool extends DnsTool {
|
export class DmarcTool extends DnsTool {
|
||||||
|
static allowWhitespaceAroundSeparator = true;
|
||||||
|
|
||||||
static fields = [
|
static fields = [
|
||||||
new ConstantField("v", "DMARC1")
|
new ConstantField("v", "DMARC1")
|
||||||
.required()
|
.required()
|
||||||
.atIndex(0),
|
.pos(0),
|
||||||
|
|
||||||
new EnumField("p", ["none", "quarantine", "reject"])
|
new EnumField("p", ["none", "quarantine", "reject"])
|
||||||
.label("Mail Receiver policy")
|
.label("Mail Receiver policy")
|
||||||
.desc("How to handle failed validations. The email may be quarantined (usually means sent to spam) or rejected")
|
.desc("How to handle failed validations. The email may be quarantined (usually means sent to spam) or rejected")
|
||||||
.options(["None", "Quarantine", "Reject"])
|
.options(["None", "Quarantine", "Reject"])
|
||||||
.required()
|
.required()
|
||||||
.atIndex(1)
|
.pos(1),
|
||||||
.after("v"),
|
|
||||||
|
|
||||||
new EnumField("adkim", ["r", "s"])
|
new EnumField("adkim", ["r", "s"])
|
||||||
.label("DKIM")
|
.label("DKIM")
|
||||||
.desc("How strictly to handle DKIM validation")
|
.desc("How strictly to handle DKIM validation")
|
||||||
.options(["Relaxed", "Strict"])
|
.options(["Relaxed", "Strict"])
|
||||||
.default("r"),
|
.default("r")
|
||||||
|
.pos(2),
|
||||||
|
|
||||||
new EnumField("aspf", ["r", "s"])
|
new EnumField("aspf", ["r", "s"])
|
||||||
.label("SPF")
|
.label("SPF")
|
||||||
.desc("How strictly to handle SPF validation")
|
.desc("How strictly to handle SPF validation")
|
||||||
.options(["Relaxed", "Strict"])
|
.options(["Relaxed", "Strict"])
|
||||||
.default("r"),
|
.default("r")
|
||||||
|
.pos(2),
|
||||||
|
|
||||||
new EnumField("sp", ["none", "quarantine", "reject"])
|
new EnumField("sp", ["none", "quarantine", "reject"])
|
||||||
.label("Mail Receiver policy (for subdomains)")
|
.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")
|
.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")
|
.category("advanced")
|
||||||
.options(["None", "Quarantine", "Reject"]),
|
.options(["None", "Quarantine", "Reject"])
|
||||||
|
.pos(2),
|
||||||
|
|
||||||
new IntField("pct", 0, 100)
|
new IntField("pct", 0, 100)
|
||||||
.label("Percentage")
|
.label("Percentage")
|
||||||
.desc("Percentage of emails to apply DMARC validation on. Useful for split-testing and continuous rollout")
|
.desc("Percentage of emails to apply DMARC validation on. Useful for split-testing and continuous rollout")
|
||||||
.category("advanced")
|
.category("advanced")
|
||||||
.default(100),
|
.default(100)
|
||||||
|
.pos(2),
|
||||||
|
|
||||||
new DmarcUriListField("ruf")
|
new DmarcUriListField("ruf")
|
||||||
.label("Send failure reports to")
|
.label("Send failure reports to")
|
||||||
.desc("When DMARC validation fails, reports are sent to this email")
|
.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"])
|
new EnumField("fo", ["0", "1", "d", "s"])
|
||||||
.label("Failure reporting options")
|
.label("Failure reporting options")
|
||||||
@ -59,7 +65,8 @@ export class DmarcTool extends DnsTool {
|
|||||||
"Generate DKIM failure report",
|
"Generate DKIM failure report",
|
||||||
"Generate SPF failure report",
|
"Generate SPF failure report",
|
||||||
])
|
])
|
||||||
.default("0"),
|
.default("0")
|
||||||
|
.pos(2),
|
||||||
|
|
||||||
new DmarcUriListField("rua")
|
new DmarcUriListField("rua")
|
||||||
.label("Send aggregate feedback to")
|
.label("Send aggregate feedback to")
|
||||||
@ -70,7 +77,8 @@ export class DmarcTool extends DnsTool {
|
|||||||
.label("Aggregate report interval")
|
.label("Aggregate report interval")
|
||||||
.desc("Interval (in seconds) between aggregate reports")
|
.desc("Interval (in seconds) between aggregate reports")
|
||||||
.category("failure-reporting")
|
.category("failure-reporting")
|
||||||
.default(86400),
|
.default(86400)
|
||||||
|
.pos(2),
|
||||||
|
|
||||||
new Field("rf").default("afrf"), // Other values not supported
|
new Field("rf").default("afrf"), // Other values not supported
|
||||||
];
|
];
|
||||||
@ -86,26 +94,10 @@ export class DmarcTool extends DnsTool {
|
|||||||
|
|
||||||
tokenize() {
|
tokenize() {
|
||||||
return this.text
|
return this.text
|
||||||
.replace(/;\s+$/, "")
|
.replace(/;\s*$/, "")
|
||||||
.split(/;\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() {
|
fieldsToString() {
|
||||||
let tokens = this.constructor.fields
|
let tokens = this.constructor.fields
|
||||||
.filter(field => !field.defaultValue || field.getInputValue() !== field.defaultValue)
|
.filter(field => !field.defaultValue || field.getInputValue() !== field.defaultValue)
|
||||||
|
|||||||
@ -1,44 +1,70 @@
|
|||||||
import { ValidationError } from "../ValidationError.js";
|
import { ValidationError } from "../ValidationError.js";
|
||||||
|
|
||||||
export class DnsTool {
|
export class DnsTool {
|
||||||
|
static allowWhitespaceAroundSeparator;
|
||||||
static fields = [];
|
static fields = [];
|
||||||
|
|
||||||
constructor(text) {
|
constructor(text) {
|
||||||
this.text = text;
|
this.text = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
getKeyValues() {
|
tokenize() {
|
||||||
throw new Error("Unimplemented");
|
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() {
|
validate() {
|
||||||
const values = this.getKeyValues();
|
const values = this.getKeyValues();
|
||||||
|
|
||||||
// Validate field order
|
|
||||||
for (const field of this.constructor.fields) {
|
for (const field of this.constructor.fields) {
|
||||||
const valueIdx = values.findIndex(v => v.key === field.key);
|
if (field.isRequired && !values.some(v => v.key === field.key)) {
|
||||||
|
|
||||||
if (field.isRequired && valueIdx === -1) {
|
|
||||||
throw new ValidationError(`Field "${field.key}" is required`);
|
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++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
const input = values[i];
|
const input = values[i];
|
||||||
const field = this.constructor.fields.find(f => f.key === input.key);
|
const field = this.constructor.fields.find(f => f.key === input.key);
|
||||||
|
|
||||||
if (!field) {
|
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);
|
field.validate(input.value);
|
||||||
|
|
||||||
|
lastPos = field.position;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
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,7 +1,9 @@
|
|||||||
import { DmarcTool } from "./tools/DmarcTool.js";
|
import { DmarcTool } from "./tools/DmarcTool.js";
|
||||||
|
import { SpfTool } from "./tools/SpfTool.js";
|
||||||
|
|
||||||
const tools = {
|
const tools = {
|
||||||
"/dmarc-validator": DmarcTool,
|
"/dmarc-validator": DmarcTool,
|
||||||
|
"/spf-validator": SpfTool,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Tool = tools[location.pathname];
|
const Tool = tools[location.pathname];
|
||||||
|
|||||||
@ -45,11 +45,7 @@ label {
|
|||||||
transition: all 200ms;
|
transition: all 200ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
#record:disabled {
|
#record:hover {
|
||||||
background-color: #F9F9F9;
|
|
||||||
}
|
|
||||||
|
|
||||||
#record:not(:disabled):hover {
|
|
||||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
<h1>DMARC Record Creator</h1>
|
<h1>DMARC Record Creator</h1>
|
||||||
|
|
||||||
<label for="record">DNS Record</label><br>
|
<label for="record">DNS Record</label><br>
|
||||||
<input id="record" type="text" disabled>
|
<input id="record" type="text" readonly>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<h2>Create a DMARC DNS Record</h2>
|
<h2>Create a DMARC DNS Record</h2>
|
||||||
|
|||||||
38
spf-validator/index.html
Normal file
38
spf-validator/index.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>SPF Record Validator - Validate SPF DNS Records</title>
|
||||||
|
<link rel="stylesheet" href="/assets/styles/main.css">
|
||||||
|
<script type="module" src="/assets/scripts/validator.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>SPF Record Validator</h1>
|
||||||
|
|
||||||
|
<label for="record">DNS Record</label><br>
|
||||||
|
<input id="record" type="text" placeholder="v=spf1 ..." autocapitalize="off" spellcheck="false">
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h2>Paste an SPF DNS record into the input field to check if it is valid.</h2>
|
||||||
|
|
||||||
|
<p id="error" class="validation-result">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path fill="currentColor" d="M320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM231 231C240.4 221.6 255.6 221.6 264.9 231L319.9 286L374.9 231C384.3 221.6 399.5 221.6 408.8 231C418.1 240.4 418.2 255.6 408.8 264.9L353.8 319.9L408.8 374.9C418.2 384.3 418.2 399.5 408.8 408.8C399.4 418.1 384.2 418.2 374.9 408.8L319.9 353.8L264.9 408.8C255.5 418.2 240.3 418.2 231 408.8C221.7 399.4 221.6 384.2 231 374.9L286 319.9L231 264.9C221.6 255.5 221.6 240.3 231 231z"/></svg>
|
||||||
|
<span id="error-message"></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p id="success" class="validation-result">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path fill="currentColor" d="M320 576C178.6 576 64 461.4 64 320C64 178.6 178.6 64 320 64C461.4 64 576 178.6 576 320C576 461.4 461.4 576 320 576zM438 209.7C427.3 201.9 412.3 204.3 404.5 215L285.1 379.2L233 327.1C223.6 317.7 208.4 317.7 199.1 327.1C189.8 336.5 189.7 351.7 199.1 361L271.1 433C276.1 438 282.9 440.5 289.9 440C296.9 439.5 303.3 435.9 307.4 430.2L443.3 243.2C451.1 232.5 448.7 217.5 438 209.7z"/></svg>
|
||||||
|
<span>Validation Success</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p id="result-placeholder" class="validation-result"></p>
|
||||||
|
|
||||||
|
<p>Insert SPF description</p>
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<h3>More tools:</h3>
|
||||||
|
<a href="/dmarc-validator">DMARC Validator Tool</a>
|
||||||
|
</center>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user