mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2025-01-23 12:37:05 +00:00
93d1055869
using "primary" to describe the domain of the box / mail server is confusing when working with multiple domains. Usually the box domain is different from the domain you want to host your mail for.
217 lines
6.7 KiB
Python
217 lines
6.7 KiB
Python
import os.path
|
|
|
|
# DO NOT import non-standard modules. This module is imported by
|
|
# migrate.py which runs on fresh machines before anything is installed
|
|
# besides Python.
|
|
|
|
# THE ENVIRONMENT FILE AT /etc/mailinabox.conf
|
|
|
|
def load_environment():
|
|
# Load settings from /etc/mailinabox.conf.
|
|
return load_env_vars_from_file("/etc/mailinabox.conf")
|
|
|
|
def load_env_vars_from_file(fn):
|
|
# Load settings from a KEY=VALUE file.
|
|
import collections
|
|
env = collections.OrderedDict()
|
|
with open(fn, encoding="utf-8") as f:
|
|
for line in f:
|
|
env.setdefault(*line.strip().split("=", 1))
|
|
return env
|
|
|
|
def save_environment(env):
|
|
with open("/etc/mailinabox.conf", "w", encoding="utf-8") as f:
|
|
for k, v in env.items():
|
|
f.write(f"{k}={v}\n")
|
|
|
|
# THE SETTINGS FILE AT STORAGE_ROOT/settings.yaml.
|
|
|
|
def write_settings(config, env):
|
|
import rtyaml
|
|
fn = os.path.join(env['STORAGE_ROOT'], 'settings.yaml')
|
|
with open(fn, "w", encoding="utf-8") as f:
|
|
f.write(rtyaml.dump(config))
|
|
|
|
def load_settings(env):
|
|
import rtyaml
|
|
fn = os.path.join(env['STORAGE_ROOT'], 'settings.yaml')
|
|
try:
|
|
with open(fn, encoding="utf-8") as f:
|
|
config = rtyaml.load(f)
|
|
if not isinstance(config, dict): raise ValueError # caught below
|
|
return config
|
|
except:
|
|
return { }
|
|
|
|
# UTILITIES
|
|
|
|
def safe_domain_name(name):
|
|
# Sanitize a domain name so it is safe to use as a file name on disk.
|
|
import urllib.parse
|
|
return urllib.parse.quote(name, safe='')
|
|
|
|
def sort_domains(domain_names, env):
|
|
# Put domain names in a nice sorted order.
|
|
|
|
# The nice order will group domain names by DNS zone, i.e. the top-most
|
|
# domain name that we serve that ecompasses a set of subdomains. Map
|
|
# each of the domain names to the zone that contains them. Walk the domains
|
|
# from shortest to longest since zones are always shorter than their
|
|
# subdomains.
|
|
zones = { }
|
|
for domain in sorted(domain_names, key=len):
|
|
for z in zones.values():
|
|
if domain.endswith("." + z):
|
|
# We found a parent domain already in the list.
|
|
zones[domain] = z
|
|
break
|
|
else:
|
|
# 'break' did not occur: there is no parent domain, so it is its
|
|
# own zone.
|
|
zones[domain] = domain
|
|
|
|
# Sort the zones.
|
|
zone_domains = sorted(zones.values(),
|
|
key = lambda d : (
|
|
# BOX_HOSTNAME or the zone that contains it is always first.
|
|
not (d == env['BOX_HOSTNAME'] or env['BOX_HOSTNAME'].endswith("." + d)),
|
|
|
|
# Then just dumb lexicographically.
|
|
d,
|
|
))
|
|
|
|
# Now sort the domain names that fall within each zone.
|
|
return sorted(domain_names,
|
|
key = lambda d : (
|
|
# First by zone.
|
|
zone_domains.index(zones[d]),
|
|
|
|
# BOX_HOSTNAME is always first within the zone that contains it.
|
|
d != env['BOX_HOSTNAME'],
|
|
|
|
# Followed by any of its subdomains.
|
|
not d.endswith("." + env['BOX_HOSTNAME']),
|
|
|
|
# Then in right-to-left lexicographic order of the .-separated parts of the name.
|
|
list(reversed(d.split("."))),
|
|
))
|
|
|
|
|
|
def sort_email_addresses(email_addresses, env):
|
|
email_addresses = set(email_addresses)
|
|
domains = {email.split("@", 1)[1] for email in email_addresses if "@" in email}
|
|
ret = []
|
|
for domain in sort_domains(domains, env):
|
|
domain_emails = {email for email in email_addresses if email.endswith("@" + domain)}
|
|
ret.extend(sorted(domain_emails))
|
|
email_addresses -= domain_emails
|
|
ret.extend(sorted(email_addresses)) # whatever is left
|
|
return ret
|
|
|
|
def shell(method, cmd_args, env=None, capture_stderr=False, return_bytes=False, trap=False, input=None):
|
|
# A safe way to execute processes.
|
|
# Some processes like apt-get require being given a sane PATH.
|
|
import subprocess
|
|
|
|
if env is None:
|
|
env = {}
|
|
env.update({ "PATH": "/sbin:/bin:/usr/sbin:/usr/bin" })
|
|
kwargs = {
|
|
'env': env,
|
|
'stderr': None if not capture_stderr else subprocess.STDOUT,
|
|
}
|
|
if method == "check_output" and input is not None:
|
|
kwargs['input'] = input
|
|
|
|
if not trap:
|
|
ret = getattr(subprocess, method)(cmd_args, **kwargs)
|
|
else:
|
|
try:
|
|
ret = getattr(subprocess, method)(cmd_args, **kwargs)
|
|
code = 0
|
|
except subprocess.CalledProcessError as e:
|
|
ret = e.output
|
|
code = e.returncode
|
|
if not return_bytes and isinstance(ret, bytes): ret = ret.decode("utf8")
|
|
if not trap:
|
|
return ret
|
|
else:
|
|
return code, ret
|
|
|
|
def create_syslog_handler():
|
|
import logging.handlers
|
|
handler = logging.handlers.SysLogHandler(address='/dev/log')
|
|
handler.setLevel(logging.WARNING)
|
|
return handler
|
|
|
|
def du(path):
|
|
# Computes the size of all files in the path, like the `du` command.
|
|
# Based on http://stackoverflow.com/a/17936789. Takes into account
|
|
# soft and hard links.
|
|
total_size = 0
|
|
seen = set()
|
|
for dirpath, _dirnames, filenames in os.walk(path):
|
|
for f in filenames:
|
|
fp = os.path.join(dirpath, f)
|
|
try:
|
|
stat = os.lstat(fp)
|
|
except OSError:
|
|
continue
|
|
if stat.st_ino in seen:
|
|
continue
|
|
seen.add(stat.st_ino)
|
|
total_size += stat.st_size
|
|
return total_size
|
|
|
|
def wait_for_service(port, public, env, timeout):
|
|
# Block until a service on a given port (bound privately or publicly)
|
|
# is taking connections, with a maximum timeout.
|
|
import socket, time
|
|
start = time.perf_counter()
|
|
while True:
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.settimeout(timeout/3)
|
|
try:
|
|
s.connect(("127.0.0.1" if not public else env['PUBLIC_IP'], port))
|
|
return True
|
|
except OSError:
|
|
if time.perf_counter() > start+timeout:
|
|
return False
|
|
time.sleep(min(timeout/4, 1))
|
|
|
|
def get_ssh_port():
|
|
port_value = get_ssh_config_value("port")
|
|
|
|
if port_value:
|
|
return int(port_value)
|
|
|
|
return None
|
|
|
|
def get_ssh_config_value(parameter_name):
|
|
# Returns ssh configuration value for the provided parameter
|
|
import subprocess
|
|
try:
|
|
output = shell('check_output', ['sshd', '-T'])
|
|
except FileNotFoundError:
|
|
# sshd is not installed. That's ok.
|
|
return None
|
|
except subprocess.CalledProcessError:
|
|
# error while calling shell command
|
|
return None
|
|
|
|
for line in output.split("\n"):
|
|
if " " not in line: continue # there's a blank line at the end
|
|
key, values = line.split(" ", 1)
|
|
if key == parameter_name:
|
|
return values # space-delimited if there are multiple values
|
|
|
|
# Did not find the parameter!
|
|
return None
|
|
|
|
if __name__ == "__main__":
|
|
from web_update import get_web_domains
|
|
env = load_environment()
|
|
domains = get_web_domains(env)
|
|
for domain in domains:
|
|
print(domain)
|