SMTP Relay feature rework (#23)

This commit is contained in:
David Duque 2021-08-23 02:06:10 +01:00 committed by GitHub
parent 2c975e43cc
commit d557885aab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 495 additions and 74 deletions

View file

@ -681,43 +681,138 @@ def privacy_status_set():
@authorized_personnel_only
def smtp_relay_get():
config = utils.load_settings(env)
dkim_rrtxt = ""
rr = config.get("SMTP_RELAY_DKIM_RR", None)
if rr is not None:
if rr.get("p") is None:
raise ValueError("Key doesn't exist!")
for c, d in (("v", "DKIM1"), ("h", None), ("k", "rsa"), ("n", None), ("s", None), ("t", None)):
txt = rr.get(c, d)
if txt is None:
continue
else:
dkim_rrtxt += f"{c}={txt}; "
dkim_rrtxt += f"p={rr.get('p')}"
return {
"enabled": config.get("SMTP_RELAY_ENABLED", True),
"enabled": config.get("SMTP_RELAY_ENABLED", False),
"host": config.get("SMTP_RELAY_HOST", ""),
"auth_enabled": config.get("SMTP_RELAY_AUTH", False),
"user": config.get("SMTP_RELAY_USER", "")
"port": config.get("SMTP_RELAY_PORT", None),
"user": config.get("SMTP_RELAY_USER", ""),
"authorized_servers": config.get("SMTP_RELAY_AUTHORIZED_SERVERS", []),
"dkim_selector": config.get("SMTP_RELAY_DKIM_SELECTOR", None),
"dkim_rr": dkim_rrtxt
}
@app.route('/system/smtp/relay', methods=["POST"])
@authorized_personnel_only
def smtp_relay_set():
from editconf import edit_conf
from os import chmod
import re, socket, ssl
config = utils.load_settings(env)
newconf = request.form
# Is DKIM configured?
sel = newconf.get("dkim_selector")
if sel is None or sel.strip() == "":
config["SMTP_RELAY_DKIM_SELECTOR"] = None
config["SMTP_RELAY_DKIM_RR"] = None
elif re.fullmatch(r"[a-z\d\._]+", sel.strip()) is None:
return ("The DKIM selector is invalid!", 400)
elif sel.strip() == config.get("local_dkim_selector", "mail"):
return (f"The DKIM selector {sel.strip()} is already in use by the box!", 400)
else:
# DKIM selector looks good, try processing the RR
rr = newconf.get("dkim_rr", "")
if rr.strip() == "":
return ("Cannot publish a selector with an empty key!", 400)
components = {}
for r in re.split(r"[;\s]+", rr):
sp = re.split(r"\=", r)
if len(sp) != 2:
return ("DKIM public key RR is malformed!", 400)
components[sp[0]] = sp[1]
if not components.get("p"):
return ("The DKIM public key doesn't exist!", 400)
config["SMTP_RELAY_DKIM_SELECTOR"] = sel
config["SMTP_RELAY_DKIM_RR"] = components
relay_on = False
implicit_tls = False
if newconf.get("enabled") == "true":
relay_on = True
# Try negotiating TLS directly. We need to know this because we need to configure Postfix
# to be aware of this detail.
try:
ctx = ssl.create_default_context()
with socket.create_connection((newconf.get("host"), int(newconf.get("port"))), 5) as sock:
with ctx.wrap_socket(sock, server_hostname=newconf.get("host")):
implicit_tls = True
except ssl.SSLError as sle:
# Couldn't connect via TLS, configure Postfix to send via STARTTLS
print(sle.reason)
except (socket.herror, socket.gaierror) as he:
return (f"Unable to resolve hostname (it probably is incorrect): {he.strerror}", 400)
except socket.timeout:
return ("We couldn't connect to the server. Is it down or did you write the wrong port number?", 400)
pw_file = "/etc/postfix/sasl_passwd"
modify_password = True
# Check that if the provided password is empty, that there was a password saved before
if (newconf.get("key", "") == ""):
if os.path.isfile(pw_file):
modify_password = False
else:
return ("Please provide a password/key (there is no existing password to retain).", 400)
try:
# Write on daemon settings
config["SMTP_RELAY_ENABLED"] = (newconf.get("enabled") == "true")
config["SMTP_RELAY_ENABLED"] = relay_on
config["SMTP_RELAY_HOST"] = newconf.get("host")
config["SMTP_RELAY_AUTH"] = (newconf.get("auth_enabled") == "true")
config["SMTP_RELAY_PORT"] = int(newconf.get("port"))
config["SMTP_RELAY_USER"] = newconf.get("user")
config["SMTP_RELAY_AUTHORIZED_SERVERS"] = [s.strip() for s in re.split(r"[, ]+", newconf.get("authorized_servers", []) or "") if s.strip() != ""]
utils.write_settings(config, env)
# Write on Postfix configs
edit_conf("/etc/postfix/main.cf", [
"relayhost=" + (f"[{config['SMTP_RELAY_HOST']}]:587" if config["SMTP_RELAY_ENABLED"] else ""),
"smtp_sasl_auth_enable=" + ("yes" if config["SMTP_RELAY_AUTH"] else "no"),
"smtp_sasl_security_options=" + ("noanonymous" if config["SMTP_RELAY_AUTH"] else "anonymous"),
"smtp_sasl_tls_security_options=" + ("noanonymous" if config["SMTP_RELAY_AUTH"] else "anonymous")
"relayhost=" + (f"[{config['SMTP_RELAY_HOST']}]:{config['SMTP_RELAY_PORT']}" if config["SMTP_RELAY_ENABLED"] else ""),
f"smtp_tls_wrappermode={'yes' if implicit_tls else 'no'}"
], delimiter_re=r"\s*=\s*", delimiter="=", comment_char="#")
if config["SMTP_RELAY_AUTH"]:
# Edit the sasl password
with open("/etc/postfix/sasl_passwd", "w") as f:
f.write(f"[{config['SMTP_RELAY_HOST']}]:587 {config['SMTP_RELAY_USER']}:{newconf.get('key')}\n")
utils.shell("check_output", ["/usr/bin/chmod", "600", "/etc/postfix/sasl_passwd"], capture_stderr=True)
utils.shell("check_output", ["/usr/sbin/postmap", "/etc/postfix/sasl_passwd"], capture_stderr=True)
# Edit the sasl password (still will edit the file, but keep the pw)
with open(pw_file, "a+") as f:
f.seek(0)
pwm = re.match(r"\[.+\]\:[0-9]+\s.+\:(.*)", f.readline())
if (pwm is None or len(pwm.groups()) != 1) and not modify_password:
# Well if this isn't a bruh moment
return ("Please provide a password/key (there is no existing password to retain).", 400)
f.truncate(0)
f.write(
f"[{config['SMTP_RELAY_HOST']}]:{config['SMTP_RELAY_PORT']} {config['SMTP_RELAY_USER']}:{newconf.get('key') if modify_password else pwm[1]}\n"
)
chmod(pw_file, 0o600)
utils.shell("check_output", ["/usr/sbin/postmap", pw_file], capture_stderr=True)
# Regenerate DNS (to apply whatever changes need to be made)
from dns_update import do_dns_update
do_dns_update(env)
# Restart Postfix
return utils.shell("check_output", ["/usr/bin/systemctl", "restart", "postfix"], capture_stderr=True)
return utils.shell("check_output", ["/usr/sbin/postfix", "reload"], capture_stderr=True)
except Exception as e:
return (str(e), 500)
return (str(e), 400)
# PGP

View file

@ -7,9 +7,10 @@
import sys, os, os.path, urllib.parse, datetime, re, hashlib, base64
import ipaddress
import rtyaml
import idna
import dns.resolver
from utils import shell, load_env_vars_from_file, safe_domain_name, sort_domains
from utils import shell, load_env_vars_from_file, safe_domain_name, sort_domains, load_settings
from ssl_certificates import get_ssl_certificates, check_certificate
# From https://stackoverflow.com/questions/3026957/how-to-validate-a-domain-name-using-regex-php/16491074#16491074
@ -168,6 +169,36 @@ def build_zones(env):
def build_zone(domain, domain_properties, additional_records, env, is_zone=True):
records = []
# Are there any other authorized servers for this domain?
settings = load_settings(env)
spf_extra = None
if settings.get("SMTP_RELAY_ENABLED", False):
spf_extra = ""
# Convert settings to spf elements
for r in settings.get("SMTP_RELAY_AUTHORIZED_SERVERS", []):
sr = ""
if r[0:4] == "spf:":
sr = f"include:{r[4:]}"
elif "/" in r:
net = ipaddress.ip_network(r)
if isinstance(net, ipaddress.IPv4Network):
sr = "ip4:" + net.compressed
elif isinstance(net, ipaddress.IPv6Network):
sr = "ip6:" + net.compressed
elif not (re.fullmatch(r"[0-9\.\:]+", r) is None):
addr = ipaddress.ip_address(r)
if isinstance(addr, ipaddress.IPv4Address):
sr = "ip4:" + addr.compressed
elif isinstance(addr, ipaddress.IPv6Address):
sr = "ip6:" + addr.compressed
elif idna.encode(r):
sr = "a:" + idna.encode(r).decode()
else:
raise ValueError(f"Unexpected entry on authorized servers: {r}")
spf_extra += f"{sr} "
if spf_extra.strip() == "":
spf_extra = None
# For top-level zones, define the authoritative name servers.
#
@ -293,10 +324,13 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True)
records.append((qname, rtype, value, explanation))
# SPF record: Permit the box ('mx', see above) to send mail on behalf of
# the domain, and no one else.
# the domain, and no one else (unless the user is using an SMTP relay and authorized other servers).
# Skip if the user has set a custom SPF record.
if not has_rec(None, "TXT", prefix="v=spf1 "):
records.append((None, "TXT", 'v=spf1 mx -all', "Recommended. Specifies that only the box is permitted to send @%s mail." % domain))
if spf_extra is None:
records.append((None, "TXT", 'v=spf1 mx -all', "Recommended. Specifies that only the box is permitted to send @%s mail." % domain))
else:
records.append((None, "TXT", f'v=spf1 mx {spf_extra}-all', "Recommended. Specifies that only the box and the server(s) you authorized are permitted to send @%s mail." % domain))
# Append the DKIM TXT record to the zone as generated by OpenDKIM.
# Skip if the user has set a DKIM record already.
@ -304,8 +338,25 @@ def build_zone(domain, domain_properties, additional_records, env, is_zone=True)
with open(opendkim_record_file) as orf:
m = re.match(r'(\S+)\s+IN\s+TXT\s+\( ((?:"[^"]+"\s+)+)\)', orf.read(), re.S)
val = "".join(re.findall(r'"([^"]+)"', m.group(2)))
if not has_rec(m.group(1), "TXT", prefix="v=DKIM1; "):
records.append((m.group(1), "TXT", val, "Recommended. Provides a way for recipients to verify that this machine sent @%s mail." % domain))
rname = f"{settings.get('local_dkim_selector', 'mail')}._domainkey"
if not has_rec(rname, "TXT", prefix="v=DKIM1; "):
records.append((rname, "TXT", val, "Recommended. Provides a way for recipients to verify that this machine sent @%s mail." % domain))
# Append the DKIM TXT record relative to the SMTP relay, if applicable.
# Skip if manually set by the user.
relay_ds = settings.get("SMTP_RELAY_DKIM_SELECTOR")
rr = settings.get("SMTP_RELAY_DKIM_RR", {})
if relay_ds is not None and not has_rec(f"{relay_ds}._domainkey", "TXT", prefix="v=DKIM1; ") and rr.get("p") is not None:
dkim_rrtxt = ""
for c, d in (("v", "DKIM1"), ("h", None), ("k", "rsa"), ("n", None), ("s", None), ("t", None)):
txt = rr.get(c, d)
if txt is None:
continue
else:
dkim_rrtxt += f"{c}={txt}; "
dkim_rrtxt += f"p={rr.get('p')}"
records.append((f"{relay_ds}._domainkey", "TXT", dkim_rrtxt, "Recommended. Provides a way for recipients to verify that the SMTP relay you set up sent @%s mail." % domain))
# Append a DMARC record.
# Skip if the user has set a DMARC record already.
@ -768,6 +819,7 @@ def write_opendkim_tables(domains, env):
# that we send mail from (zones and all subdomains).
opendkim_key_file = os.path.join(env['STORAGE_ROOT'], 'mail/dkim/mail.private')
config = load_settings(env)
if not os.path.exists(opendkim_key_file):
# Looks like OpenDKIM is not installed.
@ -792,8 +844,11 @@ def write_opendkim_tables(domains, env):
# signing domain must match the sender's From: domain.
"KeyTable":
"".join(
"{domain} {domain}:mail:{key_file}\n".format(domain=domain, key_file=opendkim_key_file)
for domain in domains
"{domain} {domain}:{selector}:{key_file}\n".format(
domain=domain,
key_file=opendkim_key_file,
selector = config.get("local_dkim_selector", "mail")
) for domain in domains
),
}

View file

@ -378,14 +378,85 @@ def run_network_checks(env, output):
# it might be needed.
config = load_settings(env)
if config.get("SMTP_RELAY_ENABLED"):
if config.get("SMTP_RELAY_AUTH"):
output.print_ok("An authenticated SMTP relay has been set up via port 587.")
else:
output.print_warning("A SMTP relay has been set up, but it is not authenticated.")
elif ret == 0:
output.print_na("No SMTP relay has been set up (but that's ok since port 25 is not blocked).")
test_smtp_relay(env, output)
else:
output.print_error("No SMTP relay has been set up. Since port 25 is blocked, you will probably not be able to send any mail.")
output.print_na("No SMTP relay has been set up.")
def test_smtp_relay(env, output):
# Test whether the relay configuration works - this is done by:
# 1. Connect to the relay endpoint
# 2. Try to log in with the credentials given
# 3. Successful? We're done. We can close.
config = load_settings(env)
import smtplib, ssl, socket
# Grab the password
pw = ""
try:
with open("/etc/postfix/sasl_passwd", "r") as cf:
matches = re.match(r"\[.+\]\:[0-9]+\s.+\:(.*)", cf.readline())
if matches is None or len(matches.groups()) != 1:
output.print_error("Couldn't fetch the relay password. Configuration may be broken or incomplete.")
return
else:
pw = matches[1]
except OSError:
output.print_error("Couldn't fetch the relay password. Configuration may be broken or incomplete.")
# Try first using implicit TLS, then STARTTLS
client = {}
login_attempted = False
try:
client["tls"] = smtplib.SMTP_SSL(config.get("SMTP_RELAY_HOST"), config.get("SMTP_RELAY_PORT"), timeout=5)
client["tls"].ehlo(env["PRIMARY_HOSTNAME"])
login_attempted = True
client["tls"].login(config.get("SMTP_RELAY_USER"), pw)
output.print_ok("The SMTP relay is configured correctly (uses implicit TLS).")
except (socket.gaierror, socket.timeout):
output.print_error("Unable to connect to SMTP Relay. The host may be down, or the hostname and/or port number are wrong.")
except (smtplib.SMTPConnectError, smtplib.SMTPServerDisconnected, ssl.SSLError):
# Some providers shut the connection down after an unsuccessful login attempt
if login_attempted:
output.print_error("Authentication on the SMTP relay failed. It's likely the configuration is incorrect, or credentials might have been revoked.")
return
# This endpoint doesn't seem to support implicit TLS, let's try STARTTLS instead
try:
client["starttls"] = smtplib.SMTP(config.get("SMTP_RELAY_HOST"), config.get("SMTP_RELAY_PORT"), timeout=5)
client["starttls"].starttls()
client["starttls"].ehlo(env["PRIMARY_HOSTNAME"])
login_attempted = True
client["starttls"].login(config.get("SMTP_RELAY_USER"), pw)
output.print_ok("SMTP Relay is configured correctly (uses STARTTLS).")
except (smtplib.SMTPConnectError, smtplib.SMTPServerDisconnected) as e:
if login_attempted:
output.print_error("Authentication on the SMTP relay failed. It's likely the configuration is incorrect, or credentials might have been revoked.")
return
output.print_error("Couldn't connect to the SMTP relay or connection was suddenly closed. The host may be down or the configuration might be incorrect.")
except smtplib.SMTPNotSupportedError as err:
if str(err)[0:8] == "STARTTLS":
output.print_error("The SMTP relay doesn't support either implicit TLS or STARTTLS.")
else:
output.print_error("The SMTP relay doesn't support username/password authentication.")
except smtplib.SMTPAuthenticationError:
output.print_error("Authentication on the SMTP relay failed. It's likely the configuration is incorrect, or credentials might have been revoked.")
except Exception as e:
output.print_error("Some unrecognized error happened while testing the SMTP relay configuration.")
output.print_line(str(e))
finally:
if not client.get("starttls") is None and client["starttls"].sock:
client["starttls"].quit()
except smtplib.SMTPNotSupportedError:
output.print_error("The SMTP relay doesn't support username/password authentication.")
except smtplib.SMTPAuthenticationError:
output.print_error("Authentication on the SMTP relay failed. It's likely the configuration is incorrect, or credentials might have been revoked.")
except Exception as e:
output.print_error("Some unrecognized error happened while testing the SMTP relay configuration.")
output.print_line(str(e))
finally:
if not client.get("tls") is None and client["tls"].sock:
client["tls"].quit()
def run_domain_checks(rounded_time, env, output, pool, domains_to_check=None):
# Get the list of domains we handle mail for.

View file

@ -3,77 +3,122 @@
<h2>SMTP Relays</h2>
<p>SMTP Relays are third-party services you can hand off the responsability of getting the mail delivered. They
can be useful when, for example, port 25 is blocked.</p>
<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>Here, you can configure an authenticated SMTP relay (for example, <a href="https://sendgrid.com/"
target="_blank">SendGrid</a>) over port 587.</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">
<h3>SMTP Relay Configuration</h3>
<form class="form-horizontal" role="form" onsubmit="set_smtp_relay_config(); return false;">
<h3>SMTP Relay Configuration</h3>
<div class="form-group">
<table id="smtp-relays" class="table" style="width: 600px">
<table id="smtp-relays" class="table" style="width: 100%">
<tr>
<td>
<label for="use_relay" class="col-sm-1 control-label">Use Relay?</label>
<td style="width: 15%;">
<label for="use_relay" class="col-sm-1 control-label"
style="vertical-align: middle; white-space: nowrap;"><b>Use Relay?</b></label>
</td>
<td>
<div class="col-sm-10">
<input type="checkbox" id="use_relay" name="use_relay" value="true"
onclick="checkfields();">
<input type="checkbox" id="use_relay" name="use_relay" value=false onclick="checkfields();">
</div>
</td>
</tr>
<tr>
<td>
<label for="relay_host" class="col-sm-1 control-label">Hostname</label>
<td style="width: 15%;">
<label for="relay_host" class="col-sm-1 control-label"
style="vertical-align: middle; white-space: nowrap;">Hostname</label>
</td>
<td>
<div class="col-sm-10">
<input type="text" class="form-control" id="relay_host" placeholder="host.domain.tld">
<div>
<input type="text" class="form-control" id="relay_host" placeholder="relay.example.net">
</div>
</td>
<td style="padding: 0; font-weight: bold;">:587</td>
</tr>
<tr>
<td>
<label for="relay_use_auth" class="col-sm-1 control-label">Authenticate</label>
</td>
<td>
<div class="col-sm-10">
<input checked type="checkbox" id="relay_use_auth" name="relay_use_auth" value="true"
onclick="checkfields();">
<td style="padding: 0; font-weight: bold; vertical-align: middle; white-space: nowrap;">:</td>
<td style="width: 20%;">
<div>
<input type="number" class="form-control" id="relay_port" min="1" max="65535" step="1"
value="465">
</div>
</td>
</tr>
<tr>
<td>
<label for="relay_auth_user" class="col-sm-1 control-label">Username</label>
<td style="width: 15%;">
<label for="relay_auth_user" class="col-sm-1 control-label"
style="vertical-align: middle; white-space: nowrap;">Username</label>
</td>
<td>
<div class="col-sm-10">
<div>
<input type="text" class="form-control" id="relay_auth_user" placeholder="user">
</div>
</td>
</tr>
<tr>
<td>
<label for="relay_auth_pass" class="col-sm-1 control-label">Password/Key</label>
<td style="width: 15%;">
<label for="relay_auth_pass" class="col-sm-1 control-label"
style="vertical-align: middle; white-space: nowrap;">Password/Key</label>
</td>
<td>
<div class="col-sm-10">
<div>
<input type="password" class="form-control" id="relay_auth_pass" placeholder="password">
</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>
</td>
</tr>
</table>
</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>.
</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="form-group">
<table style="width: 100%">
<tr>
<td style="width: 15%;"><input type="text" style="text-align: right;" class="form-control" id="relay_dkim_selector" placeholder="selector"></td>
<td><b>._domainkey.{{hostname}}</b></td>
</tr>
</table>
<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>
<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, feel free to publish them on DNS.</p>
<div>
<button type="submit" class="btn btn-primary">Update</button>
</div>
@ -83,18 +128,31 @@
<script>
const use_relay = document.getElementById("use_relay")
const relay_host = document.getElementById("relay_host")
const relay_use_auth = document.getElementById("relay_use_auth")
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
let auth_enabled = relay_use_auth.checked
relay_host.disabled = !relay_enabled
relay_use_auth.disabled = !relay_enabled
relay_auth_user.disabled = !(relay_enabled && auth_enabled)
relay_auth_pass.disabled = !(relay_enabled && auth_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() {
@ -105,9 +163,19 @@
data => {
use_relay.checked = data.enabled
relay_host.value = data.host
relay_use_auth.checked = data.auth_enabled
relay_port.value = data.port
relay_auth_user.value = data.user
relay_auth_pass.value = ""
relay_authorized_servers.value = ""
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()
}
@ -121,15 +189,147 @@
{
enabled: use_relay.checked,
host: relay_host.value,
auth_enabled: relay_use_auth.checked,
port: relay_port.value,
user: relay_auth_user.value,
key: relay_auth_pass.value
key: relay_auth_pass.value,
authorized_servers: relay_authorized_servers.value,
dkim_selector: relay_dkim_sel.value,
dkim_rr: relay_dkim_key.value
},
() => {
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})
}
)
}
</script>
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>

View file

@ -270,10 +270,10 @@ management/editconf.py /etc/postfix/main.cf \
# Store default configurations for SMTP relays:
management/editconf.py /etc/postfix/main.cf \
smtp_sasl_auth_enable=no \
smtp_sasl_auth_enable=yes \
smtp_sasl_password_maps="hash:/etc/postfix/sasl_passwd" \
smtp_sasl_security_options=anonymous \
smtp_sasl_tls_security_options=anonymous \
smtp_sasl_security_options=noanonymous \
smtp_sasl_tls_security_options=noanonymous \
smtp_tls_security_level=encrypt \
header_size_limit=4096000