322 lines
10 KiB
HTML
322 lines
10 KiB
HTML
<style>
|
|
</style>
|
|
|
|
<h2>SMTP Relays</h2>
|
|
|
|
<p>SMTP Relays are third-party services that can deliver email on your behalf. They
|
|
can be useful when, for example, port 25 is blocked, the cloud provider/ISP doesn't provide Reverse DNS, or the IP
|
|
address
|
|
has a low reputation, among other situations where deliverablity isn't great.</p>
|
|
|
|
<p>These services are governed by their own terms and as such limits can be imposed in the usage of those services.</p>
|
|
|
|
<p>Here, you can configure an authenticated SMTP relay and authorize it's associated servers to send mail for you.</p>
|
|
|
|
<div id="smtp_relay_config">
|
|
<form class="form-horizontal" role="form" onsubmit="set_smtp_relay_config(); return false;">
|
|
<h3>SMTP Relay Configuration</h3>
|
|
<div id="smtp-relays" class="col-12">
|
|
|
|
<div class="input-group mb-3">
|
|
<label for="use_relay" class="input-group-text"><b>Use Relay?</b></label>
|
|
<div class="input-group-text">
|
|
<div class="form-switch">
|
|
<input type="checkbox" role="switch" id="use_relay" class="form-check-input" value=false
|
|
onclick="checkfields();">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="input-group mb-3">
|
|
<label class="input-group-text">Where to?</label>
|
|
<input type="text" class="form-control" id="relay_host" placeholder="relay.example.com">
|
|
<label class="input-group-text">:</label>
|
|
<div style="width: 20%">
|
|
<input type="number" class="form-control" id="relay_port" placeholder="Port (465)">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="input-group mb-3">
|
|
<label class="input-group-text">Username</label>
|
|
<input type="text" class="form-control" id="relay_auth_user" placeholder="">
|
|
</div>
|
|
|
|
<div>
|
|
<div class="input-group">
|
|
<label class="input-group-text">Password/Key</label>
|
|
<input type="password" class="form-control" id="relay_auth_pass" placeholder="">
|
|
</div>
|
|
<p class="small">If you've already set up a relay before on this box, you can leave this field blank if
|
|
you don't want to change it's password.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Authorized Servers</h3>
|
|
<p>The relay service should specify the servers where the email will be sent from, please add them below. These
|
|
will probably be published in the form of a SPF record. <b>Failure to do so will potentially have your email
|
|
sent to spam or even rejected altogether by recipients.</b>
|
|
</p>
|
|
|
|
<p>You can use the button below to attempt to localize the SPF record associated with the service you're using.
|
|
</p>
|
|
|
|
<button id="smtp_relay_autospf_btn" type="button" class="btn btn-secondary" onclick="autodetect_spf()">Detect
|
|
SPF
|
|
Records</button>
|
|
|
|
<div id="smtp_relay_autospf"></div>
|
|
<br>
|
|
<div class="form-group">
|
|
<h4>Add your SPF configuration/authorized servers here</h4>
|
|
<input type="text" class="form-control" id="relay_authorized_servers"
|
|
placeholder="mail1.example.net mail2.example.net">
|
|
<p class="small">You can separate multiple servers with commas or spaces. You can also add IP addresses or
|
|
subnets using <code>10.20.30.40</code> or <code>10.0.0.0/8</code>. You can "import" SPF records using
|
|
<code>spf:example.com</code>. If your provider gave you an SPF record to add to your DNS, you can also paste it here.
|
|
</p>
|
|
</div>
|
|
|
|
<h3>DKIM Configuration</h3>
|
|
<p>DKIM allows receivers to verify that the email was sent by the relay you configured (this is, somebody you
|
|
trust). <b>Not doing so will have your email sent to spam.</b></p>
|
|
|
|
<div class="col-lg-6 col-md-8 col-12">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="relay_dkim_selector" placeholder="selector"></td>
|
|
<label class="input-group-text" for="relay_dkim_selector">._domainkey.{{hostname}}</label>
|
|
</div>
|
|
</div>
|
|
|
|
<h4>Paste the DKIM key here:</h4>
|
|
<p><textarea id="relay_dkim_key" class="form-control" style="width: 100%; height: 8em"
|
|
placeholder="k=algo;p=K3y/C0N7ent5/dGhpcyBpcyBub3QgYSByZWFsIGtleSwgc28gYmV3YXJlIDrigb4"></textarea>
|
|
</p>
|
|
|
|
<div>
|
|
<button type="submit" class="btn btn-primary">Update</button>
|
|
</div>
|
|
|
|
<h3>After configuration</h3>
|
|
<p>By that time you should be good to go. If your relay provider provides their own custom DNS verification
|
|
methods (<b>including custom DMARC configurations</b>), feel free to publish them on DNS.</p>
|
|
</form>
|
|
</div>
|
|
|
|
<script>
|
|
const use_relay = document.getElementById("use_relay")
|
|
const relay_host = document.getElementById("relay_host")
|
|
const relay_port = document.getElementById("relay_port")
|
|
const relay_auth_user = document.getElementById("relay_auth_user")
|
|
const relay_auth_pass = document.getElementById("relay_auth_pass")
|
|
|
|
const relay_authorized_servers = document.getElementById("relay_authorized_servers")
|
|
const relay_spf_discover = document.getElementById("smtp_relay_autospf_btn")
|
|
const relay_spf_discover_results = document.getElementById("smtp_relay_autospf")
|
|
|
|
const relay_dkim_sel = document.getElementById("relay_dkim_selector")
|
|
const relay_dkim_key = document.getElementById("relay_dkim_key")
|
|
|
|
function checkfields() {
|
|
let relay_enabled = use_relay.checked
|
|
|
|
relay_host.disabled = !relay_enabled
|
|
relay_port.disabled = !relay_enabled
|
|
relay_auth_user.disabled = !relay_enabled
|
|
relay_auth_pass.disabled = !relay_enabled
|
|
relay_authorized_servers.disabled = !relay_enabled
|
|
|
|
relay_spf_discover.disabled = !relay_enabled
|
|
relay_spf_discover_results.innerHTML = ""
|
|
|
|
relay_dkim_sel.disabled = !relay_enabled
|
|
relay_dkim_key.disabled = !relay_enabled
|
|
}
|
|
|
|
function show_smtp_relays() {
|
|
api(
|
|
"/system/smtp/relay",
|
|
"GET",
|
|
{},
|
|
data => {
|
|
use_relay.checked = data.enabled
|
|
relay_host.value = data.host
|
|
relay_port.value = data.port
|
|
relay_auth_user.value = data.user
|
|
relay_auth_pass.value = ""
|
|
relay_authorized_servers.value = ""
|
|
|
|
if (data.spf_record) {
|
|
relay_authorized_servers.value = data.spf_record
|
|
}else if (data.authorized_servers) {
|
|
data.authorized_servers.forEach(element => {
|
|
relay_authorized_servers.value += `${element} `
|
|
});
|
|
}
|
|
|
|
if (data.dkim_selector) {
|
|
relay_dkim_sel.value = data.dkim_selector
|
|
relay_dkim_key.value = data.dkim_rr
|
|
}
|
|
|
|
checkfields()
|
|
}
|
|
)
|
|
}
|
|
|
|
function set_smtp_relay_config() {
|
|
let relay_configuration = {
|
|
enabled: use_relay.checked,
|
|
host: relay_host.value,
|
|
port: relay_port.value,
|
|
user: relay_auth_user.value,
|
|
key: relay_auth_pass.value,
|
|
dkim_selector: relay_dkim_sel.value,
|
|
dkim_rr: relay_dkim_key.value
|
|
}
|
|
if (relay_authorized_servers.value.substr(0, 7) === "v=spf1 ") {
|
|
relay_configuration.spf_record = relay_authorized_servers.value
|
|
} else {
|
|
relay_configuration.authorized_servers = relay_authorized_servers.value
|
|
}
|
|
|
|
api(
|
|
"/system/smtp/relay",
|
|
"POST",
|
|
relay_configuration,
|
|
() => {
|
|
show_modal_error("Done!", "The configuration has been updated and Postfix was restarted successfully. Please make sure everything is functioning as intended.", () => {
|
|
return false
|
|
})
|
|
},
|
|
(e) => {
|
|
show_modal_error("Error!", e, () => { return false })
|
|
}
|
|
)
|
|
}
|
|
|
|
function add_spf(domain) {
|
|
document.getElementById(`smtp_relay_spfinclude_${domain}`).disabled = true
|
|
|
|
let impl = false
|
|
relay_authorized_servers.value.split(/[\s,]+/).forEach(s => {
|
|
if (s === `spf:${domain}`) {
|
|
impl = true
|
|
}
|
|
});
|
|
|
|
if (!impl) {
|
|
relay_authorized_servers.value += ` spf:${domain}`
|
|
}
|
|
}
|
|
|
|
async function autodetect_spf() {
|
|
let btn = $("#smtp_relay_autospf_btn")
|
|
let results = $("#smtp_relay_autospf")
|
|
|
|
let host = relay_host.value
|
|
if (host.trim() == "") {
|
|
results.html("<b>Error:</b> No hostname specified.")
|
|
return
|
|
}
|
|
|
|
let hmatches = host.match(/([^\s.\\\/]+\.)+([^\s.\\\/]+)/)
|
|
if (!hmatches || hmatches[0] != host) {
|
|
results.html(`<b>Error: <code>${host}</code></b> is not a valid hostname.`)
|
|
return
|
|
}
|
|
|
|
btn.html("Working...")
|
|
btn.prop("disabled", true)
|
|
|
|
results.html("")
|
|
|
|
let base_host = hmatches[hmatches.length - 2] + hmatches[hmatches.length - 1]
|
|
|
|
function record_html(name, rec) {
|
|
if (rec.error) {
|
|
return `<b>${name}</b> - ${rec.error} Error (${rec.msg})`
|
|
} else {
|
|
return `<button id="smtp_relay_spfinclude_${name}" type="button" class="btn btn-secondary" onclick="add_spf('${name}')">Include</button> <b>${name}</b> <code>${rec.msg}</code>`
|
|
}
|
|
}
|
|
|
|
let records = []
|
|
|
|
await Promise.all([
|
|
query_spf(base_host).then(rr => { records[0] = record_html(base_host, rr) }),
|
|
query_spf(`spf.${base_host}`).then(rr => { records[1] = record_html(`spf.${base_host}`, rr) }),
|
|
query_spf(`_spf.${base_host}`).then(rr => { records[2] = record_html(`_spf.${base_host}`, rr) })
|
|
])
|
|
|
|
let txt = "<h4>Here's what I've found:</h4><ul>"
|
|
records.forEach((r) => {
|
|
txt += `<li>${r}</li>`
|
|
})
|
|
|
|
txt += "</ul><small>Only some common subdomains are tested here, so it's possible I've missed some. You <b>SHOULD NOT</b> include records you do not recognize, else there will be servers that can send mail as you, but that you will not use.</small>"
|
|
|
|
results.html(txt)
|
|
|
|
btn.html("Detect SPF Records")
|
|
btn.prop("disabled", false)
|
|
}
|
|
|
|
function query_spf(hostname) {
|
|
// We use Cloudflare's DNS servers for this (DNS over HTTPS)
|
|
return new Promise((resolve, _) => {
|
|
$.ajax({
|
|
url: "https://cloudflare-dns.com/dns-query",
|
|
headers: {
|
|
accept: "application/dns-json"
|
|
},
|
|
method: "GET",
|
|
data: {
|
|
name: hostname,
|
|
type: "TXT",
|
|
do: false,
|
|
cd: false
|
|
},
|
|
success: (data) => {
|
|
const RRTXT = 16
|
|
const status_description = [
|
|
// From https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6
|
|
// (last checked: 3rd June 2021)
|
|
"Success",
|
|
"Format Error",
|
|
"Server Failure",
|
|
"Non-Existent Domain",
|
|
"Not Implemented",
|
|
"Query Refused",
|
|
"Name exists when it should not",
|
|
"RR set exists when it should not",
|
|
"RR set that should exist does not",
|
|
"Server is not authoritative for zone",
|
|
"Not Authorized",
|
|
"Name not contained in zone",
|
|
"DSO-TYPE Not Implemented"
|
|
]
|
|
if (data.Status != 0) {
|
|
if (data.Status > 11) {
|
|
return resolve({ error: "DNS", msg: "Unknown Error" })
|
|
} else {
|
|
return resolve({ error: "DNS", msg: status_description[data.Status] })
|
|
}
|
|
}
|
|
|
|
if (data.Answer) {
|
|
data.Answer.forEach(ans => {
|
|
if (ans.type == 16 && ans.name == hostname && ans.data.substring(1, 7) == "v=spf1") {
|
|
return resolve({ msg: ans.data.substring(1, ans.data.length - 1) })
|
|
}
|
|
})
|
|
}
|
|
|
|
return resolve({ error: "DNS", msg: "No SPF record found" })
|
|
},
|
|
error: (_, __, err) => {
|
|
resolve({ error: "HTTP", msg: err })
|
|
}
|
|
})
|
|
})
|
|
}
|
|
</script>
|