mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-20 10:34:30 +00:00
187 lines
5.0 KiB
Python
187 lines
5.0 KiB
Python
"""Configure email domains"""
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import io
|
|
import json
|
|
import os
|
|
import re
|
|
import select
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from django.core.exceptions import ValidationError
|
|
from plinth.errors import ActionError
|
|
from plinth.actions import superuser_run
|
|
|
|
from . import models
|
|
from plinth.modules.email_server import interproc, postconf
|
|
|
|
EXIT_VALIDATION = 40
|
|
|
|
managed_keys = ['_mailname', 'mydomain', 'myhostname', 'mydestination']
|
|
|
|
|
|
class ClientError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def get():
|
|
# Stub
|
|
return [models.Diagnosis('Email domains')]
|
|
|
|
|
|
def repair():
|
|
# Stub
|
|
raise RuntimeError()
|
|
|
|
|
|
def get_domain_config():
|
|
fields = []
|
|
|
|
# Special keys
|
|
mailname = SimpleNamespace(key='_mailname', name='/etc/mailname')
|
|
with open('/etc/mailname', 'r') as fd:
|
|
mailname.value = fd.readline().strip()
|
|
fields.append(mailname)
|
|
|
|
# Postconf keys
|
|
postconf_keys = [k for k in managed_keys if not k.startswith('_')]
|
|
result_dict = postconf.get_many(postconf_keys)
|
|
for key, value in result_dict.items():
|
|
field = SimpleNamespace(key=key, value=value, name='$' + key)
|
|
fields.append(field)
|
|
|
|
return fields
|
|
|
|
|
|
def set_keys(raw):
|
|
# Serialize the keys we know
|
|
config_dict = {k: v for (k, v) in raw.items() if k in managed_keys}
|
|
if not config_dict:
|
|
raise ClientError('To update a key, specify a new value')
|
|
|
|
ipc = b'%s\n' % json.dumps(config_dict).encode('utf8')
|
|
if len(ipc) > 4096:
|
|
raise ClientError('POST data exceeds max line length')
|
|
|
|
try:
|
|
superuser_run('email_server', ['-i', 'domain', 'set_keys'], input=ipc)
|
|
except ActionError as e:
|
|
stdout = e.args[1]
|
|
if not stdout.startswith('ClientError:'):
|
|
raise RuntimeError('Action script failure') from e
|
|
else:
|
|
raise ValidationError(stdout) from e
|
|
|
|
|
|
def action_set_keys():
|
|
try:
|
|
_action_set_keys()
|
|
except ClientError as e:
|
|
print('ClientError:', end=' ')
|
|
print(e.args[0])
|
|
sys.exit(EXIT_VALIDATION)
|
|
|
|
|
|
def _action_set_keys():
|
|
line = _stdin_readline()
|
|
if not line.startswith('{') or not line.endswith('}\n'):
|
|
raise ClientError('Bad stdin data')
|
|
|
|
clean_dict = {}
|
|
# Input validation
|
|
for key, value in json.loads(line).items():
|
|
if key not in managed_keys:
|
|
raise ClientError('Key not allowed: %r' % key)
|
|
if not isinstance(value, str):
|
|
raise ClientError('Bad value type from key: %r' % key)
|
|
clean_function = globals()['clean_' + key.lstrip('_')]
|
|
clean_dict[key] = clean_function(value)
|
|
|
|
# Apply changes (postconf)
|
|
postconf_dict = dict(filter(lambda kv: not kv[0].startswith('_'),
|
|
clean_dict.items()))
|
|
postconf.set_many(postconf_dict)
|
|
|
|
# Apply changes (special)
|
|
for key, value in clean_dict.items():
|
|
if key.startswith('_'):
|
|
set_function = globals()['su_set' + key]
|
|
set_function(value)
|
|
|
|
# Important: reload postfix after acquiring lock
|
|
with postconf.mutex.lock_all():
|
|
# systemctl reload postfix
|
|
args = ['systemctl', 'reload', 'postfix']
|
|
completed = subprocess.run(args, capture_output=True)
|
|
if completed.returncode != 0:
|
|
interproc.log_subprocess(completed)
|
|
raise OSError('Could not reload postfix')
|
|
|
|
|
|
def clean_mailname(mailname):
|
|
mailname = mailname.lower().strip()
|
|
if not re.match('^[a-z0-9-\\.]+$', mailname):
|
|
raise ClientError('Invalid character in host/domain/mail name')
|
|
# XXX: need more verification
|
|
return mailname
|
|
|
|
|
|
def clean_mydomain(raw):
|
|
return clean_mailname(raw)
|
|
|
|
|
|
def clean_myhostname(raw):
|
|
return clean_mailname(raw)
|
|
|
|
|
|
def clean_mydestination(raw):
|
|
ascii_code = (ord(c) for c in raw)
|
|
valid = all(32 <= a <= 126 for a in ascii_code)
|
|
if not valid:
|
|
raise ClientError('Bad input for $mydestination')
|
|
else:
|
|
return raw
|
|
|
|
|
|
def su_set_mailname(cleaned):
|
|
with interproc.atomically_rewrite('/etc/mailname') as fd:
|
|
fd.write(cleaned)
|
|
fd.write('\n')
|
|
|
|
|
|
def _stdin_readline():
|
|
membuf = io.BytesIO()
|
|
bytes_saved = 0
|
|
fd = sys.stdin.buffer
|
|
time_started = time.monotonic()
|
|
|
|
# Reading stdin with timeout
|
|
# https://stackoverflow.com/a/21429655
|
|
os.set_blocking(fd.fileno(), False)
|
|
|
|
while bytes_saved < 4096:
|
|
rlist, wlist, xlist = select.select([fd], [], [], 1.0)
|
|
if fd in rlist:
|
|
data = os.read(fd.fileno(), 4096)
|
|
membuf.write(data)
|
|
bytes_saved += len(data)
|
|
if len(data) == 0 or b'\n' in data: # end of file or line
|
|
break
|
|
if time.monotonic() - time_started > 5:
|
|
raise TimeoutError()
|
|
|
|
# Read a line
|
|
membuf.seek(0)
|
|
line = membuf.readline()
|
|
if not line.endswith(b'\n'):
|
|
raise ClientError('Line was too long')
|
|
|
|
try:
|
|
return line.decode('utf8')
|
|
except ValueError as e:
|
|
raise ClientError('UTF-8 decode failed') from e
|