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
This commit is contained in:
fliu 2021-07-20 05:28:11 +00:00 committed by Sunil Mohan Adapa
parent 2bd1ad4533
commit 62c501e9c7
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
10 changed files with 194 additions and 48 deletions

View File

@ -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():

View File

@ -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
)

View File

@ -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']

View File

@ -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')

View File

@ -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 }}
<h3>{% trans "Alias Management" %}</h3>
{% if error %}
<div class="alert alert-danger" role="alert">
<p>
{% trans "There was a problem with your request. Please try again." %}
</p>
{% for message in error %}
<p>{{ message }}</p>
{% endfor %}
</div>
{% endif %}
{{ block.super }}
{% if no_alias %}
<p>{% trans "You have no email aliases." %}</p>

View File

@ -3,8 +3,12 @@
{% load i18n %}
{% block configuration %}
{% block content %}
{{ tabs|safe }}
{{ block.super }}
{% endblock %}
{% block extra_content %}
<p>
<a href="/rspamd/">
{% trans "Visit Rspamd administration interface" %}

View File

@ -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 %}
<div class="alert alert-danger" role="alert">
<p>
{% trans "There was a problem with your request. Please try again." %}
</p>
{% for message in error %}
<p>{{ message }}</p>
{% endfor %}
</div>
{% endif %}
{% endblock %}

View File

@ -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 %}
<form action="{{ request.path }}" method="post">
<div class="alert alert-warning" role="alert">
<p>
<strong>{% trans "You do not have a home directory." %}</strong>
<span>
{% trans "Create one to begin receiving emails." %}
</span>
</p>
{% csrf_token %}
<button class="btn btn-primary" type="submit" name="btn_mkhome">
{% trans "Create home directory" %}
</button>
</div>
</form>
{% endif %}
<p>
<a href="/roundcube/">Roundcube login</a>
</p>
{% endblock %}

View File

@ -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())),
]

View File

@ -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('<ul class="nav nav-tabs">')
for page_name, link_text in tabs:
for page_name, link_text in tab_data:
if request.path.endswith('/' + page_name):
cls = 'active'
else: