From 62c501e9c7d2849c80761f61871f21d7e9246414 Mon Sep 17 00:00:00 2001 From: fliu <10025-fliu@users.noreply.salsa.debian.org> Date: Tue, 20 Jul 2021 05:28:11 +0000 Subject: [PATCH] email: Add UI for creating the home directory email_server: - `-i` option passes all remaining arguments to action - delete unused "touch file" option Views: - delete broken links - add tabs to every page - separate admin tabs from user tabs --- actions/email_server | 19 ++--- plinth/modules/email_server/__init__.py | 2 +- plinth/modules/email_server/audit/__init__.py | 5 +- plinth/modules/email_server/audit/home.py | 71 +++++++++++++++++++ .../modules/email_server/templates/alias.html | 18 +---- .../email_server/templates/email_server.html | 6 +- .../email_server/templates/form_base.html | 19 +++++ .../email_server/templates/my_mail.html | 32 +++++++++ plinth/modules/email_server/urls.py | 6 +- plinth/modules/email_server/views.py | 64 +++++++++++++---- 10 files changed, 194 insertions(+), 48 deletions(-) create mode 100644 plinth/modules/email_server/audit/home.py create mode 100644 plinth/modules/email_server/templates/form_base.html create mode 100644 plinth/modules/email_server/templates/my_mail.html diff --git a/actions/email_server b/actions/email_server index 39b39610c..1cc122e5d 100755 --- a/actions/email_server +++ b/actions/email_server @@ -32,9 +32,9 @@ def main(): parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group(required=True) - group.add_argument('-i', nargs=2, dest='ipc') - group.add_argument('-t', nargs=1, dest='touch_file') + group.add_argument('-i', nargs='+', dest='ipc') + # Select the first non-empty dict item adict = vars(parser.parse_args()) generator = (kv for kv in adict.items() if kv[1] is not None) subcommand, arguments = next(generator) @@ -49,7 +49,7 @@ def main(): @reserved_for_root -def subcommand_ipc(module_name, action_name): +def subcommand_ipc(module_name, action_name, *args): import plinth.modules.email_server.audit as audit # We only run actions defined in the audit module @@ -63,18 +63,7 @@ def subcommand_ipc(module_name, action_name): logger.critical('Bad action: %s/%r', module_name, action_name) sys.exit(EXIT_SYNTAX) - function() - - -def subcommand_touch_file(path): - import pathlib - - if os.getuid() == 0: - logger.critical('Do not run the `-t` option as root') - sys.exit(EXIT_PERM) - - # mode is influenced by umask - pathlib.Path(path).touch(mode=0o660, exist_ok=True) + function(*args) def _log_additional_info(): diff --git a/plinth/modules/email_server/__init__.py b/plinth/modules/email_server/__init__.py index 15fe9df30..4a8137cae 100644 --- a/plinth/modules/email_server/__init__.py +++ b/plinth/modules/email_server/__init__.py @@ -63,7 +63,7 @@ class EmailServerApp(plinth.app.App): name=info.name, short_description=info.short_description, icon='roundcube', - url=reverse_lazy('email_server:my_aliases'), + url=reverse_lazy('email_server:my_mail'), clients=manifest.clients, login_required=True ) diff --git a/plinth/modules/email_server/audit/__init__.py b/plinth/modules/email_server/audit/__init__.py index 41f1b99c6..4adc7489b 100644 --- a/plinth/modules/email_server/audit/__init__.py +++ b/plinth/modules/email_server/audit/__init__.py @@ -3,8 +3,9 @@ Provides diagnosis and repair of email server configuration issues """ -from . import ldap from . import domain +from . import home +from . import ldap from . import spam -__all__ = ['ldap', 'domain', 'spam'] +__all__ = ['domain', 'home', 'ldap', 'spam'] diff --git a/plinth/modules/email_server/audit/home.py b/plinth/modules/email_server/audit/home.py new file mode 100644 index 000000000..dc14a61c4 --- /dev/null +++ b/plinth/modules/email_server/audit/home.py @@ -0,0 +1,71 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later + +import logging +import os +import pwd +import subprocess + +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ +from plinth.actions import superuser_run +from plinth.errors import ActionError + +logger = logging.getLogger(__name__) + + +def exists_nam(username): + """Returns True if the user's home directory exists""" + try: + passwd = pwd.getpwnam(username) + except KeyError as e: + raise ValidationError(_('User does not exist')) from e + return _exists(passwd) + + +def exists_uid(uid_number): + """Returns True if the user's home directory exists""" + try: + passwd = pwd.getpwuid(uid_number) + except KeyError as e: + raise ValidationError(_('User does not exist')) from e + return _exists(passwd) + + +def _exists(passwd): + return os.path.exists(passwd.pw_dir) + + +def put_nam(username): + """Create a home directory for the user (identified by username)""" + _put('nam', username) + + +def put_uid(uid_number): + """Create a home directory for the user (identified by UID)""" + _put('uid', str(uid_number)) + + +def _put(arg_type, user_info): + try: + args = ['-i', 'home', 'mk', arg_type, user_info] + superuser_run('email_server', args) + except ActionError as e: + raise RuntimeError('Action script failure') from e + + +def action_mk(arg_type, user_info): + if arg_type == 'nam': + passwd = pwd.getpwnam(user_info) + elif arg_type == 'uid': + passwd = pwd.getpwuid(int(user_info)) + else: + raise ValueError('Unknown arg_type') + + args = ['sudo', '-n', '--user=#' + str(passwd.pw_uid)] + args.extend(['/bin/sh', '-c', 'mkdir -p ~']) + completed = subprocess.run(args, capture_output=True) + if completed.returncode != 0: + logger.critical('Subprocess returned %d', completed.returncode) + logger.critical('Stdout: %r', completed.stdout) + logger.critical('Stderr: %r', completed.stderr) + raise OSError('Could not create home directory') diff --git a/plinth/modules/email_server/templates/alias.html b/plinth/modules/email_server/templates/alias.html index cab40ab5c..1980b66f6 100644 --- a/plinth/modules/email_server/templates/alias.html +++ b/plinth/modules/email_server/templates/alias.html @@ -1,24 +1,12 @@ {# SPDX-License-Identifier: AGPL-3.0-or-later #} -{% extends "app.html" %} +{% extends "form_base.html" %} {% load bootstrap %} {% load i18n %} -{% block configuration %} +{% block content %} - {{ tabs|safe }} -

{% trans "Alias Management" %}

- - {% if error %} - - {% endif %} + {{ block.super }} {% if no_alias %}

{% trans "You have no email aliases." %}

diff --git a/plinth/modules/email_server/templates/email_server.html b/plinth/modules/email_server/templates/email_server.html index 89557bdb5..9f0a3125d 100644 --- a/plinth/modules/email_server/templates/email_server.html +++ b/plinth/modules/email_server/templates/email_server.html @@ -3,8 +3,12 @@ {% load i18n %} -{% block configuration %} +{% block content %} {{ tabs|safe }} + {{ block.super }} +{% endblock %} + +{% block extra_content %}

{% trans "Visit Rspamd administration interface" %} diff --git a/plinth/modules/email_server/templates/form_base.html b/plinth/modules/email_server/templates/form_base.html new file mode 100644 index 000000000..390c0936b --- /dev/null +++ b/plinth/modules/email_server/templates/form_base.html @@ -0,0 +1,19 @@ +{# SPDX-License-Identifier: AGPL-3.0-or-later #} +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} + {{ tabs|safe }} + {{ block.super }} + {% if error %} +

+ {% endif %} +{% endblock %} diff --git a/plinth/modules/email_server/templates/my_mail.html b/plinth/modules/email_server/templates/my_mail.html new file mode 100644 index 000000000..7bb928151 --- /dev/null +++ b/plinth/modules/email_server/templates/my_mail.html @@ -0,0 +1,32 @@ +{# SPDX-License-Identifier: AGPL-3.0-or-later #} +{% extends "form_base.html" %} + +{% load bootstrap %} +{% load i18n %} + +{% block content %} + + {{ block.super }} + + {% if not has_homedir %} +
+ +
+ {% endif %} + +

+ Roundcube login +

+ +{% endblock %} diff --git a/plinth/modules/email_server/urls.py b/plinth/modules/email_server/urls.py index eae2a2d7c..0a5e208d8 100644 --- a/plinth/modules/email_server/urls.py +++ b/plinth/modules/email_server/urls.py @@ -6,6 +6,10 @@ from . import views urlpatterns = [ path('apps/email_server/', views.EmailServerView.as_view(), name='index'), + path('apps/email_server/security', views.TLSView.as_view()), + + path('apps/email_server/my_mail', + non_admin_view(views.MyMailView.as_view()), name='my_mail'), path('apps/email_server/my_aliases', - non_admin_view(views.AliasView.as_view()), name='my_aliases') + non_admin_view(views.AliasView.as_view())), ] diff --git a/plinth/modules/email_server/views.py b/plinth/modules/email_server/views.py index f940a3e21..7f3ab837d 100644 --- a/plinth/modules/email_server/views.py +++ b/plinth/modules/email_server/views.py @@ -3,6 +3,7 @@ import io import itertools import pwd +import plinth.utils import plinth.views from django.core.exceptions import ValidationError @@ -10,35 +11,64 @@ from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ from django.views.generic.base import TemplateView -from . import forms from . import aliases +from . import audit +from . import forms -tabs = [ +admin_tabs = [ ('', _('Home')), - ('alias', _('Alias')), - ('relay', _('Relay')), - ('security', _('Security')) + ('my_mail', _('My Mail')), + ('my_aliases', _('My Aliases')) +] + +user_tabs = [ + ('my_mail', _('Home')), + ('my_aliases', _('My Aliases')) ] class EmailServerView(plinth.views.AppView): """Server configuration page""" app_id = 'email_server' - form_class = forms.EmailServerForm template_name = 'email_server.html' - def form_valid(self, form): - # old_settings = form.initial - # new_status = form.cleaned_data - # plinth.actions.superuser_run('email_server', ['--help']) - return super().form_valid(form) - def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context['tabs'] = render_tabs(self.request) return context +class MyMailView(TemplateView): + template_name = 'my_mail.html' + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context['tabs'] = render_tabs(self.request) + + nam = self.request.user.username + context['has_homedir'] = audit.home.exists_nam(nam) + + return context + + def post(self, request): + try: + return self._post(request) + except ValidationError as validation_error: + context = self.get_context_data() + context['error'] = validation_error + return self.render_to_response(context, status=400) + except RuntimeError as runtime_error: + context = self.get_context_data() + context['error'] = [str(runtime_error)] + return self.render_to_response(context, status=500) + + def _post(self, request): + if not 'btn_mkhome' in request.POST: + raise ValidationError('Bad post data') + audit.home.put_nam(request.user.username) + return self.render_to_response(self.get_context_data()) + + class AliasView(TemplateView): class Checkboxes: def __init__(self, post=None, initial=None): @@ -103,6 +133,7 @@ class AliasView(TemplateView): def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context['form'] = forms.AliasCreationForm() + context['tabs'] = render_tabs(self.request) uid = pwd.getpwnam(self.request.user.username).pw_uid models = aliases.get(uid) @@ -173,9 +204,16 @@ class AliasView(TemplateView): def render_tabs(request): + if plinth.utils.is_user_admin(request): + return _render_tabs(request, admin_tabs) + else: + return _render_tabs(request, user_tabs) + + +def _render_tabs(request, tab_data): sb = io.StringIO() sb.write('