2021-08-17 19:44:02 -07:00

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