email: Code cleanup

- Move render_tabs into plinth.views
- Move admin_tabs and user_tabs into the class
- New interproc module contains inter-process functions
- New helper methods in TabMixin
This commit is contained in:
fliu 2021-08-05 01:23:43 +00:00 committed by Sunil Mohan Adapa
parent e454e8ac1b
commit 85c6b91fbc
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
10 changed files with 113 additions and 94 deletions

View File

@ -1,7 +1,6 @@
"""Configure email domains""" """Configure email domains"""
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
import contextlib
import io import io
import json import json
import os import os
@ -9,7 +8,6 @@ import re
import select import select
import sys import sys
import time import time
import uuid
from types import SimpleNamespace from types import SimpleNamespace
@ -18,7 +16,7 @@ from plinth.errors import ActionError
from plinth.actions import superuser_run from plinth.actions import superuser_run
from . import models from . import models
from plinth.modules.email_server import postconf from plinth.modules.email_server import interproc, postconf
EXIT_VALIDATION = 40 EXIT_VALIDATION = 40
@ -140,7 +138,7 @@ def clean_mydestination(raw):
def su_set_mailname(cleaned): def su_set_mailname(cleaned):
with _atomically_rewrite('/etc/mailname', 'x') as fd: with interproc.atomically_rewrite('/etc/mailname') as fd:
fd.write(cleaned) fd.write(cleaned)
fd.write('\n') fd.write('\n')
@ -176,28 +174,3 @@ def _stdin_readline():
return line.decode('utf8') return line.decode('utf8')
except ValueError as e: except ValueError as e:
raise ClientError('UTF-8 decode failed') from e raise ClientError('UTF-8 decode failed') from e
@contextlib.contextmanager
def _atomically_rewrite(filepath, mode):
successful = False
tmp = '%s.%s.plinth-tmp' % (filepath, uuid.uuid4().hex)
fd = open(tmp, mode)
try:
# Let client write to a temporary file
yield fd
successful = True
finally:
fd.close()
try:
if successful:
# Invoke rename(2) to atomically replace the original
os.rename(tmp, filepath)
finally:
# Delete temp file
try:
os.unlink(tmp)
except FileNotFoundError:
pass

View File

@ -10,6 +10,8 @@ from django.utils.translation import ugettext_lazy as _
from plinth.actions import superuser_run from plinth.actions import superuser_run
from plinth.errors import ActionError from plinth.errors import ActionError
from plinth.modules.email_server import interproc
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -65,7 +67,5 @@ def action_mk(arg_type, user_info):
args.extend(['/bin/sh', '-c', 'mkdir -p ~']) args.extend(['/bin/sh', '-c', 'mkdir -p ~'])
completed = subprocess.run(args, capture_output=True) completed = subprocess.run(args, capture_output=True)
if completed.returncode != 0: if completed.returncode != 0:
logger.critical('Subprocess returned %d', completed.returncode) interproc.log_subprocess(completed)
logger.critical('Stdout: %r', completed.stdout)
logger.critical('Stderr: %r', completed.stderr)
raise OSError('Could not create home directory') raise OSError('Could not create home directory')

View File

@ -15,7 +15,7 @@ passdb {
userdb { userdb {
# UID number lookup (10001@example.com) # UID number lookup (10001@example.com)
driver = ldap driver = ldap
args = /etc/dovecot/freedombox-ldap-userdb-aliases.conf.ext args = /etc/dovecot/freedombox-ldap-userdb-uid.conf.ext
result_failure = continue result_failure = continue
result_internalfail = return-fail result_internalfail = return-fail
result_success = return-ok result_success = return-ok

View File

@ -11,8 +11,10 @@ user_attrs = \
=user=%{ldap:uid}, \ =user=%{ldap:uid}, \
=mail=maildir:~/Maildir:LAYOUT=index =mail=maildir:~/Maildir:LAYOUT=index
# Support user lookup by UID number
user_filter = \ user_filter = \
(&(objectClass=posixAccount)(!(uidNumber=0))(uidNumber=%n)(!(uid=%n))) (&(objectClass=posixAccount)(!(uidNumber=0))(uidNumber=%n))
# doveadm -A # doveadm -A

View File

@ -10,6 +10,8 @@ user_attrs = \
=gid=%{ldap:gidNumber}, \ =gid=%{ldap:gidNumber}, \
=mail=maildir:~/Maildir:LAYOUT=index =mail=maildir:~/Maildir:LAYOUT=index
# Support user lookup by username
user_filter = (&(objectClass=posixAccount)(uid=%Ln)(!(uidNumber=0))) user_filter = (&(objectClass=posixAccount)(uid=%Ln)(!(uidNumber=0)))
# For doveadm # For doveadm

View File

@ -0,0 +1,2 @@
DO NOT PUT PERSONAL ITEMS HERE!
This folder in its entirety is managed by FreedomBox.

View File

@ -0,0 +1,39 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import contextlib
import logging
import os
import uuid
logger = logging.getLogger(__name__)
def log_subprocess(result):
logger.critical('Subprocess returned %d', result.returncode)
logger.critical('Stdout: %r', result.stdout)
logger.critical('Stderr: %r', result.stderr)
@contextlib.contextmanager
def atomically_rewrite(filepath):
successful = False
tmp = '%s.%s.plinth-tmp' % (filepath, uuid.uuid4().hex)
fd = open(tmp, 'x')
try:
# Let client write to a temporary file
yield fd
successful = True
finally:
fd.close()
try:
if successful:
# Invoke rename(2) to atomically replace the original
os.rename(tmp, filepath)
finally:
# Delete temp file
try:
os.unlink(tmp)
except FileNotFoundError:
pass

View File

@ -8,6 +8,8 @@ import re
import subprocess import subprocess
import threading import threading
from . import interproc
lock_name_pattern = re.compile('^[0-9a-zA-Z_-]+$') lock_name_pattern = re.compile('^[0-9a-zA-Z_-]+$')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -89,9 +91,7 @@ class Mutex:
completed = subprocess.run(args, capture_output=True) completed = subprocess.run(args, capture_output=True)
if completed.returncode != 0: if completed.returncode != 0:
logger.critical('Subprocess returned %d', completed.returncode) interproc.log_subprocess(completed)
logger.critical('Stdout: %r', completed.stdout)
logger.critical('Stderr: %r', completed.stderr)
raise OSError('Could not create ' + self.lock_path) raise OSError('Could not create ' + self.lock_path)
def _checked_setresuid(self, ruid, euid, suid): def _checked_setresuid(self, ruid, euid, suid):

View File

@ -9,60 +9,39 @@ from django.core.exceptions import ValidationError
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic.base import TemplateView, View from django.views.generic.base import TemplateView, View
from plinth.views import AppView from plinth.views import AppView, render_tabs
from . import aliases from . import aliases
from . import audit from . import audit
from . import forms from . import forms
admin_tabs = [
('', _('Home')),
('my_mail', _('My Mail')),
('my_aliases', _('My Aliases')),
('security', _('Security')),
('domains', _('Domains'))
]
user_tabs = [
('my_mail', _('Home')),
('my_aliases', _('My Aliases'))
]
class TabMixin(View): class TabMixin(View):
admin_tabs = [
('', _('Home')),
('my_mail', _('My Mail')),
('my_aliases', _('My Aliases')),
('security', _('Security')),
('domains', _('Domains'))
]
user_tabs = [
('my_mail', _('Home')),
('my_aliases', _('My Aliases'))
]
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
# Retrieve context data from the next method in the MRO # Retrieve context data from the next method in the MRO
context = super().get_context_data(*args, **kwargs) context = super().get_context_data(*args, **kwargs)
# Populate context with customized data # Populate context with customized data
context['tabs'] = self.render_tabs() context['tabs'] = self.render_dynamic_tabs()
return context return context
def render_tabs(self): def render_dynamic_tabs(self):
if plinth.utils.is_user_admin(self.request): if plinth.utils.is_user_admin(self.request):
return self.__render_tabs(self.request.path, admin_tabs) return render_tabs(self.request.path, self.admin_tabs)
else: else:
return self.__render_tabs(self.request.path, user_tabs) return render_tabs(self.request.path, self.user_tabs)
@staticmethod
def __render_tabs(path, tab_data):
sb = io.StringIO()
sb.write('<ul class="nav nav-tabs">')
for page_name, link_text in tab_data:
cls = 'active' if path.endswith('/' + page_name) else ''
href = '#' if cls == 'active' else ('./' + page_name)
# -- Begin list
sb.write('<li class="nav-item">')
# -- Begin link
sb.write('<a class="nav-link {}" '.format(cls))
sb.write('href="{}">'.format(escape(href)))
sb.write('{}</a>'.format(escape(link_text)))
# -- End link
sb.write('</li>')
# -- End list
sb.write('</ul>')
return sb.getvalue()
def render_validation_error(self, validation_error, status=400): def render_validation_error(self, validation_error, status=400):
context = self.get_context_data() context = self.get_context_data()
@ -74,6 +53,14 @@ class TabMixin(View):
context['error'] = [str(exception)] context['error'] = [str(exception)]
return self.render_to_response(context, status=status) return self.render_to_response(context, status=status)
def catch_exceptions(self, function, request):
try:
return function(request)
except ValidationError as validation_error:
return self.render_validation_error(validation_error)
except Exception as error:
return self.render_exception(error)
def find_button(self, post): def find_button(self, post):
key_filter = (k for k in post.keys() if k.startswith('btn_')) key_filter = (k for k in post.keys() if k.startswith('btn_'))
lst = list(itertools.islice(key_filter, 2)) lst = list(itertools.islice(key_filter, 2))
@ -109,12 +96,7 @@ class MyMailView(TabMixin, TemplateView):
return context return context
def post(self, request): def post(self, request):
try: return self.catch_exceptions(self._post, request)
return self._post(request)
except ValidationError as validation_error:
return self.render_validation_error(validation_error)
except RuntimeError as runtime_error:
return self.render_exception(runtime_error)
def _post(self, request): def _post(self, request):
if 'btn_mkhome' not in request.POST: if 'btn_mkhome' not in request.POST:
@ -198,12 +180,7 @@ class AliasView(TabMixin, TemplateView):
return context return context
def post(self, request): def post(self, request):
try: return self.catch_exceptions(self._post, request)
return self._post(request)
except ValidationError as validation_error:
return self.render_validation_error(validation_error)
except Exception as exception:
return self.render_exception(exception)
def _post(self, request): def _post(self, request):
form = self.find_form(request.POST) form = self.find_form(request.POST)
@ -257,12 +234,7 @@ class DomainView(TabMixin, TemplateView):
return context return context
def post(self, request): def post(self, request):
try: return self.catch_exceptions(self._post, request)
return self._post(request)
except ValidationError as validation_error:
return self.render_validation_error(validation_error)
except RuntimeError as runtime_error:
return self.render_exception(runtime_error)
def _post(self, request): def _post(self, request):
changed = {} changed = {}

View File

@ -3,6 +3,7 @@
Main FreedomBox views. Main FreedomBox views.
""" """
import io
import time import time
import urllib.parse import urllib.parse
@ -11,6 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.http import Http404, HttpResponseBadRequest, HttpResponseRedirect from django.http import Http404, HttpResponseBadRequest, HttpResponseRedirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.urls import reverse from django.urls import reverse
from django.utils.html import escape
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
@ -321,3 +323,30 @@ def notification_dismiss(request, id):
notes[0].dismiss() notes[0].dismiss()
return HttpResponseRedirect(_get_redirect_url_from_param(request)) return HttpResponseRedirect(_get_redirect_url_from_param(request))
def render_tabs(request_path, tab_data):
"""Generate a Bootstrap tab group and return the raw HTML
:param request_path: value of `request.path`
:param tab_data: a list of (page_name, link_text) tuples
:returns: raw HTML of the tabs
"""
sb = io.StringIO()
sb.write('<ul class="nav nav-tabs">')
for page_name, link_text in tab_data:
cls = 'active' if request_path.endswith('/' + page_name) else ''
href = '#' if cls == 'active' else ('./' + page_name)
# -- Begin list
sb.write('<li class="nav-item">')
# -- Begin link
sb.write('<a class="nav-link {}" '.format(cls))
sb.write('href="{}">'.format(escape(href)))
sb.write('{}</a>'.format(escape(link_text)))
# -- End link
sb.write('</li>')
# -- End list
sb.write('</ul>')
return sb.getvalue()