SMTP Relay feature rework (#23)
This commit is contained in:
parent
2c975e43cc
commit
d557885aab
5 changed files with 495 additions and 74 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue