mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
- Try to mark class variables in component classes. - Leave typing hints generic, such as 'list' and 'dict' where content is usually not filled, too complex, or context is unimportant. - backups: Handle failure for tarfile extraction so that methods are not called on potentially None valued variables. - backups: Prevent potentially passing a keyword argument twice. - dynamicdns: Deal properly with outcome of urlparsing. - ejabberd: Deal with failed regex match - email: Fix a mypy compliant when iterating a filtered list. - tor: Don't reuse variables for different typed values. - tor: Don't reuse variables for different typed values. - operation: Return None explicitly. - operation: Ensure that keyword argument is not repeated. Tests: - Where only typing hints were modified and no syntax error came up, additional testing was not done. - `mypy --ignore-missing-imports .` run successfully. - Generate developer documentation. - Service runs without errors upon start up. - backups: Listing and restoring specific apps from a backup works. - backups: Mounting a remote backup repository works. - NOT TESTED: dynamicdns: Migrating from old style configuration works. - ejabberd: Verify that setting coturn configuration works. - email: Test that showing configuration from postfix works. - tor: Orport value is properly shown. - transmission: Configuration values are properly set. - users: Running unit tests as root works. - operation: Operation status messages are show properly during app install. - ./setup.py install runs Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
131 lines
3.5 KiB
Python
131 lines
3.5 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""
|
|
Set and get postfix configuration using postconf.
|
|
|
|
See: http://www.postfix.org/postconf.1.html
|
|
See: http://www.postfix.org/master.5.html
|
|
See: http://www.postfix.org/postconf.5.html
|
|
"""
|
|
|
|
import re
|
|
import subprocess
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass
|
|
class Service: # NOQA, pylint: disable=too-many-instance-attributes
|
|
"""Representation of a postfix daemon and its options."""
|
|
service: str
|
|
type_: str
|
|
private: str
|
|
unpriv: str
|
|
chroot: str
|
|
wakeup: str
|
|
maxproc: str
|
|
command: str
|
|
options: dict[str, str]
|
|
|
|
def __str__(self) -> str:
|
|
parts = [
|
|
self.service, self.type_, self.private, self.unpriv, self.chroot,
|
|
self.wakeup, self.maxproc, self.command
|
|
]
|
|
for key, value in self.options.items():
|
|
_validate_key(key)
|
|
_validate_value(value)
|
|
parts.extend(['-o', f'{key}={value}'])
|
|
|
|
return ' '.join(parts)
|
|
|
|
|
|
def get_config(keys: list) -> dict:
|
|
"""Get postfix configuration using the postconf command."""
|
|
for key in keys:
|
|
_validate_key(key)
|
|
|
|
args = ['/sbin/postconf']
|
|
for key in keys:
|
|
args.append(key)
|
|
|
|
output = _run(args)
|
|
result = {}
|
|
lines: list[str] = list(filter(None, output.split('\n')))
|
|
for line in lines:
|
|
key, sep, value = line.partition('=')
|
|
if not sep:
|
|
raise ValueError('Invalid output detected')
|
|
|
|
result[key.strip()] = value.strip()
|
|
|
|
if set(keys) != set(result.keys()):
|
|
raise ValueError('Some keys were missing from the output')
|
|
|
|
return result
|
|
|
|
|
|
def set_config(config: dict, flag=None):
|
|
"""Set postfix configuration using the postconf command."""
|
|
if not config:
|
|
return
|
|
|
|
for key, value in config.items():
|
|
_validate_key(key)
|
|
_validate_value(value)
|
|
|
|
args = ['/sbin/postconf']
|
|
if flag:
|
|
args.append(flag)
|
|
|
|
for key, value in config.items():
|
|
args.append('{}={}'.format(key, value))
|
|
|
|
_run(args)
|
|
|
|
|
|
def set_master_config(service: Service):
|
|
"""Set daemons and their options in postfix master.cf."""
|
|
service_key = service.service + '/' + service.type_
|
|
set_config({service_key: str(service)}, '-M')
|
|
|
|
|
|
def parse_maps(raw_value):
|
|
"""Parse postfix configuration values that are maps."""
|
|
if '{' in raw_value or '}' in raw_value:
|
|
raise ValueError('Unsupported map list format')
|
|
|
|
value_list = []
|
|
for segment in raw_value.split(','):
|
|
for sub_segment in segment.strip().split(' '):
|
|
sub_segment = sub_segment.strip()
|
|
if sub_segment:
|
|
value_list.append(sub_segment)
|
|
|
|
return value_list
|
|
|
|
|
|
def _run(args):
|
|
"""Run process. Capture and return standard output as a string.
|
|
|
|
Raise a RuntimeError on non-zero exit codes.
|
|
"""
|
|
try:
|
|
result = subprocess.run(args, check=True, capture_output=True)
|
|
return result.stdout.decode()
|
|
except subprocess.SubprocessError as subprocess_error:
|
|
raise RuntimeError('Subprocess failed') from subprocess_error
|
|
except UnicodeDecodeError as unicode_error:
|
|
raise RuntimeError('Unicode decoding failed') from unicode_error
|
|
|
|
|
|
def _validate_key(key):
|
|
"""Validate postconf key format or raise ValueError."""
|
|
if not re.match(r'^[a-zA-Z][a-zA-Z0-9_/]*$', key):
|
|
raise ValueError('Invalid postconf key format')
|
|
|
|
|
|
def _validate_value(value):
|
|
"""Validate postconf value format or raise ValueError."""
|
|
for char in value:
|
|
if ord(char) < 32:
|
|
raise ValueError('Value contains control characters')
|