diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/Bug_Report.md similarity index 52% rename from .github/ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE/Bug_Report.md index b1c69ef2f..166f7db16 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE/Bug_Report.md @@ -1,8 +1,16 @@ +--- +name: 'Bug Report' +about: 'Report a reproducible bug in the software.' +title: '' +labels: '' +assignees: '' +--- + # Impacted versions * OS Type: Debian/Ubuntu, Centos/Redhat, Arch, Gentoo, Slack, ... * OS Version: Number or Name -* Database Type: PostgreSQL/MySQL +* Database Type: PostgreSQL / MySQL * Database version: X.y * Modoboa: X.X.x * installer used: Yes/No ? @@ -12,6 +20,13 @@ # Current behavior + + # Expected behavior # Video/Screenshot link (optional) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..5db193d51 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Modoboa Support + url: https://github.com/modoboa/modoboa/discussions + about: Please ask and answer questions here. + - name: Sponsoring + ur: https://modoboa.org/en/sponsoring/ + about: You love Modoboa and want to sponsor it diff --git a/.github/workflows/modoboa.yml b/.github/workflows/modoboa.yml index cfd765302..37be44fae 100644 --- a/.github/workflows/modoboa.yml +++ b/.github/workflows/modoboa.yml @@ -66,7 +66,9 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - sudo apt-get install postfix librrd-dev rrdtool libldap2-dev libsasl2-dev libssl-dev + sudo apt-get update -y \ + && sudo apt-get install -y \ + postfix librrd-dev rrdtool libldap2-dev libsasl2-dev libssl-dev python -m pip install --upgrade pip pip install -r requirements.txt pip install -r ldap-requirements.txt diff --git a/Dockerfile.dev b/Dockerfile.dev index 52eca494f..6fc20ecaa 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -13,6 +13,7 @@ RUN python3 -m venv $VIRTUAL_ENV WORKDIR /tmp COPY requirements.txt /tmp COPY test-requirements.txt /tmp +RUN python -m pip install -U pip RUN pip install -r requirements.txt -r test-requirements.txt FROM base as run diff --git a/doc/configuration.rst b/doc/configuration.rst index df9bb4b5c..e41a5caf7 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -31,7 +31,7 @@ The *admin* application exposes several parameters, they are presented below: |Authentication type |General |The backend used for |Local | | | |authentication | | +--------------------+---------------------+-------------------------------+--------------------+ -|Default password |General |Scheme used to crypt |crypt | +|Default password |General |Scheme used to crypt |sha512crypt | |scheme | |mailbox passwords | | +--------------------+---------------------+-------------------------------+--------------------+ |Rounds |General |Number of rounds (only used by |70000 | @@ -144,6 +144,13 @@ The *admin* application exposes several parameters, they are presented below: | | | | | +--------------------+---------------------+-------------------------------+--------------------+ +.. warning:: + + If Dovecot is not running on the same host than Modoboa, you will have + to define which password schemes are supported. To do so, open the + :file:`settings.py` file and add a ``DOVECOT_SUPPORTED_SCHEMES`` + variable with the output of the command: ``doveadm pw -l``. + .. note:: If you are not familiar with virtual domain hosting, you should @@ -202,7 +209,11 @@ Host configuration .. note:: This section is only relevant when Modoboa handles mailboxes - renaming and removal from the filesystem. + renaming and removal from the filesystem, which requires that + Dovecot is installed and running on this host. If it is installed + at a non-standard directory, paths to its binaries can be set in the + :file:`settings.py` file with the ``DOVECOT_LOOKUP_PATH`` and + ``DOVEADM_LOOKUP_PATH`` variables. To manipulate mailboxes on the filesystem, you must allow the user who runs Modoboa to execute commands as the user who owns mailboxes. diff --git a/doc/contributing/getting_started.rst b/doc/contributing/getting_started.rst index e5b841708..695e65960 100644 --- a/doc/contributing/getting_started.rst +++ b/doc/contributing/getting_started.rst @@ -24,6 +24,12 @@ setup, go to the next section. .. _venv_for_dev: +Prerequisites For Development +============================= +If you encounter errors about missing tools, please ensure you have them installed with the foollowing: + + $ sudo apt install librrd-dev + Prepare a virtual environment ============================= diff --git a/doc/manual_installation/modoboa.rst b/doc/manual_installation/modoboa.rst index 90ba53228..f939d3d21 100644 --- a/doc/manual_installation/modoboa.rst +++ b/doc/manual_installation/modoboa.rst @@ -55,9 +55,14 @@ following system packages according to your distribution: .. note:: Alternatively, you could rely on your distribution packages for the Modoboa - dependencies which require compilation - e.g. ``psycopg2`` - if the version + dependencies which require compilation - e.g. ``rrdtool`` - if the version is compatible. In this case, you have to create your virtual environment - with the ``--system-site-packages`` option. + with the ``--system-site-packages`` option, and the required system + packages will be: + + +---------------------------------------+ + | python3-wheel python3-rrdtool rrdtool | + +---------------------------------------+ Then, install Modoboa by running: @@ -184,6 +189,9 @@ you just have to run the following command: ``://[user:pass@][host:port]/dbname`` **OR** ``sqlite:////full/path/to/your/database/file.sqlite``. + Fox example, if you were using postgres, you could setup your command like this: + ``modoboa-admin.py deploy instance_name --collectstatic --domain example.com --dburl default:postgres://user:pass@[localhost]/modoboa`` + The command will ask you a few questions, answer them and you're done. diff --git a/modoboa/admin/api.py b/modoboa/admin/api.py index c3877ffc0..299c3c3f3 100644 --- a/modoboa/admin/api.py +++ b/modoboa/admin/api.py @@ -34,7 +34,7 @@ from . import lib, models, serializers summary="Create a new domain" ) ) -class DomainViewSet(viewsets.ModelViewSet): +class DomainViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet): """Domain viewset.""" permission_classes = [IsAuthenticated, DjangoModelPermissions, ] @@ -63,7 +63,8 @@ class DomainAliasFilterSet(dj_filters.FilterSet): fields = ["domain"] -class DomainAliasViewSet(lib_viewsets.ExpandableModelViewSet): +class DomainAliasViewSet(lib_viewsets.RevisionModelMixin, + lib_viewsets.ExpandableModelViewSet): """ViewSet for DomainAlias.""" filter_backends = (dj_filters.DjangoFilterBackend, ) @@ -83,7 +84,7 @@ class DomainAliasViewSet(lib_viewsets.ExpandableModelViewSet): return context -class AccountViewSet(viewsets.ModelViewSet): +class AccountViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet): """ViewSet for User/Mailbox.""" filter_backends = [filters.SearchFilter] @@ -178,7 +179,7 @@ class AccountViewSet(viewsets.ModelViewSet): return Response(body) -class AliasViewSet(viewsets.ModelViewSet): +class AliasViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet): """ create: Create a new alias instance. @@ -203,7 +204,8 @@ class AliasViewSet(viewsets.ModelViewSet): return queryset -class SenderAddressViewSet(viewsets.ModelViewSet): +class SenderAddressViewSet(lib_viewsets.RevisionModelMixin, + viewsets.ModelViewSet): """View set for SenderAddress model.""" permission_classes = [IsAuthenticated, DjangoModelPermissions, ] diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index e0ae48c6e..c6809ac91 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -147,14 +147,6 @@ class AdminParametersForm(param_forms.AdminParametersForm): ) ) - mailboxes_owner = forms.CharField( - label=ugettext_lazy("Mailboxes owner"), - initial="vmail", - help_text=ugettext_lazy( - "The UNIX account who owns mailboxes on the filesystem" - ) - ) - default_mailbox_quota = forms.IntegerField( label=ugettext_lazy("Default mailbox quota"), initial=0, @@ -200,8 +192,7 @@ class AdminParametersForm(param_forms.AdminParametersForm): # Visibility rules visibility_rules = { "valid_mxs": "enable_mx_checks=True", - "domains_must_have_authorized_mx": "enable_mx_checks=True", - "mailboxes_owner": "handle_mailboxes=True", + "domains_must_have_authorized_mx": "enable_mx_checks=True" } def __init__(self, *args, **kwargs): @@ -236,7 +227,6 @@ class AdminParametersForm(param_forms.AdminParametersForm): hide_fields = True if hide_fields: del self.fields["handle_mailboxes"] - del self.fields["mailboxes_owner"] def clean_default_domain_quota(self): """Ensure quota is a positive integer.""" diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index f2ca42690..51d246480 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -206,9 +206,8 @@ class AccountFormGeneral(forms.ModelForm): account.role = self.cleaned_data["role"] if hasattr(account, "mailbox"): # Update forward status according to account status - models.Alias.objects.filter( - address=account.email, internal=False).update( - enabled=account.is_active) + models.Alias.objects.filter(address=account.email).update( + enabled=account.is_active) return account diff --git a/modoboa/admin/lib.py b/modoboa/admin/lib.py index 3fe459e66..995ee514f 100644 --- a/modoboa/admin/lib.py +++ b/modoboa/admin/lib.py @@ -198,6 +198,8 @@ def get_dns_records(name, typ, resolver=None): logger.warning( _("DNS resolution timeout, unable to query %s at the moment") % name, exc_info=e) + except dns.name.NameTooLong as e: + logger.error(_("DNS name is too long: %s" % name), exc_info=e) else: return dns_answers return None diff --git a/modoboa/admin/migrations/0018_auto_20201204_0935.py b/modoboa/admin/migrations/0018_auto_20201204_0935.py new file mode 100644 index 000000000..35bdf2f62 --- /dev/null +++ b/modoboa/admin/migrations/0018_auto_20201204_0935.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.12 on 2020-12-04 08:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0017_alarm'), + ] + + operations = [ + migrations.AlterField( + model_name='alarm', + name='title', + field=models.TextField(), + ), + ] diff --git a/modoboa/admin/migrations/0019_update_disabled_accounts_aliases.py b/modoboa/admin/migrations/0019_update_disabled_accounts_aliases.py new file mode 100644 index 000000000..fe3f727d5 --- /dev/null +++ b/modoboa/admin/migrations/0019_update_disabled_accounts_aliases.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.17 on 2021-01-28 13:40 + +from django.db import migrations + + +def update_disabled_accounts_aliases(apps, schema_editor): + User = apps.get_model('core', 'User') + Alias = apps.get_model('admin', 'Alias') + + disabled_accounts = User.objects.filter( + mailbox__isnull=False, is_active=False) + + Alias.objects.filter( + address__in=list(disabled_accounts.values_list('email', flat=True)), + enabled=True, + ).update(enabled=False) + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0018_auto_20201204_0935'), + ('core', '0022_user_tfa_enabled'), + ] + + operations = [ + migrations.RunPython(update_disabled_accounts_aliases), + ] diff --git a/modoboa/admin/migrations/0020_auto_20210130_2226.py b/modoboa/admin/migrations/0020_auto_20210130_2226.py new file mode 100644 index 000000000..ea9e90eb7 --- /dev/null +++ b/modoboa/admin/migrations/0020_auto_20210130_2226.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.17 on 2021-01-30 22:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0019_update_disabled_accounts_aliases'), + ] + + operations = [ + migrations.AlterField( + model_name='domain', + name='name', + field=models.CharField(help_text='The domain name', max_length=253, unique=True, verbose_name='name'), + ), + ] diff --git a/modoboa/admin/models/alarm.py b/modoboa/admin/models/alarm.py index 12ea82d39..8168fe51d 100644 --- a/modoboa/admin/models/alarm.py +++ b/modoboa/admin/models/alarm.py @@ -33,7 +33,7 @@ class Alarm(models.Model): db_index=True ) - title = models.CharField(max_length=150) + title = models.TextField() internal_name = models.CharField(max_length=120) objects = models.Manager.from_queryset(AlarmQuerySet)() diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index 1a9b16d1a..c5aec17a3 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -23,7 +23,7 @@ from . import mixins class Domain(mixins.MessageLimitMixin, AdminObject): """Mail domain.""" - name = models.CharField(ugettext_lazy("name"), max_length=100, unique=True, + name = models.CharField(ugettext_lazy("name"), max_length=253, unique=True, help_text=ugettext_lazy("The domain name")) quota = models.PositiveIntegerField( default=0, diff --git a/modoboa/admin/models/mailbox.py b/modoboa/admin/models/mailbox.py index cbea1a775..1b5ae5625 100644 --- a/modoboa/admin/models/mailbox.py +++ b/modoboa/admin/models/mailbox.py @@ -1,8 +1,5 @@ """Models related to mailboxes management.""" -import os -import pwd - from reversion import revisions as reversion from django.contrib.contenttypes.models import ContentType @@ -153,14 +150,7 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject): if not admin_params.get("handle_mailboxes"): return None if self.__mail_home is None: - curuser = pwd.getpwuid(os.getuid()).pw_name - mbowner = admin_params["mailboxes_owner"] - options = {} - if curuser != mbowner: - options["sudo_user"] = mbowner - code, output = doveadm_cmd( - "user -f home %s" % self.full_address, **options - ) + code, output = doveadm_cmd("user -f home %s" % self.full_address) if code: raise lib_exceptions.InternalError( _("Failed to retrieve mailbox location (%s)") % output) diff --git a/modoboa/admin/tests/test_account.py b/modoboa/admin/tests/test_account.py index 4eb722c45..0c4bb27e7 100644 --- a/modoboa/admin/tests/test_account.py +++ b/modoboa/admin/tests/test_account.py @@ -119,8 +119,10 @@ class AccountTestCase(ModoTestCase): account = User.objects.get(username=values["username"]) self.assertEqual(account.language, "fr") - def test_forward_updated_on_edit(self): + def test_aliases_update_on_disable(self): + """Check if aliases are updated when account is disabled.""" account = User.objects.get(username="user@test.com") + internal = models.Alias.objects.get(address=account.email, internal=True) forward = models.Alias.objects.create(address=account.email, internal=False) values = { "username": "user@test.com", @@ -133,12 +135,16 @@ class AccountTestCase(ModoTestCase): self.ajax_post( reverse("admin:account_change", args=[account.id]), values ) + internal.refresh_from_db() + self.assertFalse(internal.enabled) forward.refresh_from_db() self.assertFalse(forward.enabled) values["is_active"] = True self.ajax_post( reverse("admin:account_change", args=[account.id]), values ) + internal.refresh_from_db() + self.assertTrue(internal.enabled) forward.refresh_from_db() self.assertTrue(forward.enabled) diff --git a/modoboa/admin/tests/test_api.py b/modoboa/admin/tests/test_api.py index 982ff5aa6..727154239 100644 --- a/modoboa/admin/tests/test_api.py +++ b/modoboa/admin/tests/test_api.py @@ -10,6 +10,7 @@ from django.test import override_settings from django.urls import reverse from rest_framework.authtoken.models import Token +from reversion.models import Version from modoboa.admin import models as admin_models from modoboa.core import factories as core_factories, models as core_models @@ -51,8 +52,9 @@ class DomainAPITestCase(ModoAPITestCase): url, {"name": "test3.com", "quota": 0, "default_mailbox_quota": 10} ) self.assertEqual(response.status_code, 201) - self.assertTrue( - models.Domain.objects.filter(name="test3.com").exists()) + domain = models.Domain.objects.get(name="test3.com") + self.assertEqual(Version.objects.get_for_object(domain).count(), 1) + response = self.client.post(url, {}) self.assertEqual(response.status_code, 400) self.assertIn("name", response.data) diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 10934d946..f4bc99eb2 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -51,9 +51,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): password_scheme = forms.ChoiceField( label=ugettext_lazy("Default password scheme"), - choices=[(hasher.name, ugettext_lazy(hasher.label)) - for hasher in PasswordHasher.get_password_hashers() - if hasher().scheme in get_dovecot_schemes()], + choices=[], initial="sha512crypt", help_text=ugettext_lazy("Scheme used to crypt mailbox passwords"), ) @@ -530,6 +528,11 @@ class GeneralParametersForm(param_forms.AdminParametersForm): self._add_visibilty_rules( sms_backends.get_all_backend_visibility_rules() ) + self.fields["password_scheme"].choices = [ + (hasher.name, ugettext_lazy(hasher.label)) + for hasher in PasswordHasher.get_password_hashers() + if hasher().scheme in get_dovecot_schemes() + ] def _add_dynamic_fields(self): new_fields = OrderedDict() diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index 3fa62cc66..e59276d7b 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -114,15 +114,15 @@ MIDDLEWARE = ( ) AUTHENTICATION_BACKENDS = ( - # 'modoboa.lib.authbackends.LDAPBackend', - # 'modoboa.lib.authbackends.SMTPBackend', + #'modoboa.lib.authbackends.LDAPBackend', + #'modoboa.lib.authbackends.SMTPBackend', 'django.contrib.auth.backends.ModelBackend', ) # SMTP authentication -# AUTH_SMTP_SERVER_ADDRESS = 'localhost' -# AUTH_SMTP_SERVER_PORT = 25 -# AUTH_SMTP_SECURED_MODE = None # 'ssl' or 'starttls' are accepted +#AUTH_SMTP_SERVER_ADDRESS = 'localhost' +#AUTH_SMTP_SERVER_PORT = 25 +#AUTH_SMTP_SECURED_MODE = None # 'ssl' or 'starttls' are accepted TEMPLATES = [ @@ -205,9 +205,17 @@ SPECTACULAR_SETTINGS = { } # Modoboa settings -# MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png") -# DOVECOT_LOOKUP_PATH = ('/path/to/dovecot', ) +#MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png") + +# Path to Dovecot binaries in case of a non-standard installation +#DOVECOT_LOOKUP_PATH = ('/path/to/dovecot', ) +#DOVEADM_LOOKUP_PATH = ('/path/to/doveadm', ) + +# List of supported schemes if doveadm is not available, given by: doveadm pw -l +#DOVECOT_SUPPORTED_SCHEMES = 'SHA512-CRYPT SHA256-CRYPT' + +# DOVECOT_USER = 'vmail' MODOBOA_API_URL = 'https://api.modoboa.org/1/' diff --git a/modoboa/core/password_hashers/__init__.py b/modoboa/core/password_hashers/__init__.py index dbb399731..b94bccae7 100644 --- a/modoboa/core/password_hashers/__init__.py +++ b/modoboa/core/password_hashers/__init__.py @@ -2,6 +2,7 @@ Password hashers for Modoboa. """ +from django.conf import settings from django.utils.encoding import smart_text from modoboa.core.password_hashers.advanced import ( # NOQA:F401 @@ -32,17 +33,25 @@ def get_password_hasher(scheme): def get_dovecot_schemes(): - """Return schemes supported by dovecot""" - supported_schemes = None - try: - _, schemes = doveadm_cmd("pw -l") - except OSError: - pass - else: - supported_schemes = ["{{{}}}".format(smart_text(scheme)) - for scheme in schemes.split()] + """Return schemes supported by Dovecot. - if not supported_schemes: - supported_schemes = ['{PLAIN}'] + It first try to get supported schemes fron the settings, then from the + doveadm output, and fallback to {MD5-CRYPT} and {PLAIN} if the command + is not found. - return supported_schemes + :return: A list of supported '{SCHEME}' + """ + schemes = getattr(settings, "DOVECOT_SUPPORTED_SCHEMES", None) + default_schemes = "MD5-CRYPT PLAIN" + + if not schemes: + try: + retcode, schemes = doveadm_cmd("pw -l") + except OSError: + schemes = default_schemes + else: + if retcode: + schemes = default_schemes + + return ["{{{}}}".format(smart_text(scheme)) + for scheme in schemes.split()] diff --git a/modoboa/core/tests/test_authentication.py b/modoboa/core/tests/test_authentication.py index 567edc01a..6467cf7ac 100644 --- a/modoboa/core/tests/test_authentication.py +++ b/modoboa/core/tests/test_authentication.py @@ -19,9 +19,9 @@ from modoboa.core.password_hashers import ( from modoboa.lib.tests import NO_SMTP, ModoTestCase from .. import factories, models +DOVEADM_TEST_PATH = "{}/doveadm".format(os.path.dirname(__file__)) + -@override_settings( - DOVEADM_LOOKUP_PATH=["{}/doveadm".format(os.path.dirname(__file__))]) class AuthenticationTestCase(ModoTestCase): """Validate authentication scenarios.""" @@ -89,6 +89,7 @@ class AuthenticationTestCase(ModoTestCase): self.assertEqual(response.status_code, 302) self.assertTrue(response.url.endswith(reverse("core:dashboard"))) + @override_settings(DOVEADM_LOOKUP_PATH=[DOVEADM_TEST_PATH]) def test_password_schemes(self): """Validate password scheme changes.""" username = "user@test.com" @@ -130,6 +131,7 @@ class AuthenticationTestCase(ModoTestCase): self.assertTrue(user.password.startswith(pw_hash.scheme)) @skipUnless(argon2, "argon2-cffi not installed") + @override_settings(DOVEADM_LOOKUP_PATH=[DOVEADM_TEST_PATH]) def test_password_argon2_parameter_change(self): """Validate hash parameter update on login works with argon2.""" username = "user@test.com" @@ -169,8 +171,16 @@ class AuthenticationTestCase(ModoTestCase): self.assertEqual(parameters.memory_cost, 1000) self.assertEqual(parameters.parallelism, 2) - def test_supported_schemes(self): - """Validate dovecot supported schemes.""" + @mock.patch("modoboa.lib.sysutils.exec_cmd") + def test_dovecot_supported_schemes(self, exec_cmd_mock): + """Validate Dovecot supported schemes with fake command output.""" + exec_cmd_mock.return_value = ( + 0, + "MD5 MD5-CRYPT SHA SHA1 SHA256 SHA512 SMD5 SSHA SSHA256 SSHA512 " + "PLAIN CLEAR CLEARTEXT PLAIN-TRUNC CRAM-MD5 SCRAM-SHA-1 HMAC-MD5 " + "DIGEST-MD5 PLAIN-MD4 PLAIN-MD5 LDAP-MD5 LANMAN NTLM OTP SKEY RPA " + "PBKDF2 CRYPT SHA256-CRYPT SHA512-CRYPT" + ) supported_schemes = get_dovecot_schemes() self.assertEqual(supported_schemes, ["{MD5}", @@ -204,6 +214,17 @@ class AuthenticationTestCase(ModoTestCase): "{SHA256-CRYPT}", "{SHA512-CRYPT}"]) + @override_settings(DOVECOT_SUPPORTED_SCHEMES="SHA1 SHA512-CRYPT") + def test_dovecot_supported_schemes_from_settings(self): + """Validate dovecot supported schemes from the settings.""" + supported_schemes = get_dovecot_schemes() + self.assertEqual(supported_schemes, ["{SHA1}", "{SHA512-CRYPT}"]) + + def test_dovecot_default_schemes(self): + """Check default scheme if doveadm is not found.""" + supported_schemes = get_dovecot_schemes() + self.assertEqual(supported_schemes, ["{MD5-CRYPT}", "{PLAIN}"]) + class PasswordResetTestCase(ModoTestCase): """Test password reset service.""" diff --git a/modoboa/ldapsync/locale/zh_CN/LC_MESSAGES/django.po b/modoboa/ldapsync/locale/zh_CN/LC_MESSAGES/django.po new file mode 100644 index 000000000..d8c7f23d3 --- /dev/null +++ b/modoboa/ldapsync/locale/zh_CN/LC_MESSAGES/django.po @@ -0,0 +1,38 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +# Translators: +# 李 正 <364109980@qq.com>, 2021 +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-05-06 13:15+0200\n" +"PO-Revision-Date: 2020-05-11 18:45+0000\n" +"Last-Translator: 李 正 <364109980@qq.com>, 2021\n" +"Language-Team: Chinese (China) (https://www.transifex.com/tonio/teams/13749/zh_CN/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_CN\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: ldapsync/lib.py:84 +msgid "Failed to create LDAP account: {}" +msgstr "无法创建LDAP帐户: {}" + +#: ldapsync/lib.py:124 +msgid "Failed to update LDAP account: {}" +msgstr "无法更新LDAP帐户: {}" + +#: ldapsync/lib.py:138 +msgid "Failed to delete LDAP account: {}" +msgstr "无法删除LDAP帐户: {}" + +#: ldapsync/lib.py:149 +msgid "Failed to disable LDAP account: {}" +msgstr "无法关闭LDAP帐户: {}" diff --git a/modoboa/lib/sysutils.py b/modoboa/lib/sysutils.py index 05e84c41f..d290a04a7 100644 --- a/modoboa/lib/sysutils.py +++ b/modoboa/lib/sysutils.py @@ -5,6 +5,7 @@ This module extra functions/shortcuts to communicate with the system import inspect import os +import pwd import re import subprocess @@ -42,8 +43,7 @@ def exec_cmd(cmd, sudo_user=None, pinput=None, capture_output=True, **kwargs): return process.returncode, output -def doveadm_cmd(params, sudo_user=None, pinput=None, - capture_output=True, **kwargs): +def doveadm_cmd(params, pinput=None, capture_output=True, **kwargs): """Execute doveadm command. Run doveadm command using the current user. Set :keyword:`sudo_user` if @@ -69,14 +69,17 @@ def doveadm_cmd(params, sudo_user=None, pinput=None, if os.path.isfile(fpath) and os.access(fpath, os.X_OK): dpath = fpath break + dovecot_user = getattr(settings, "DOVECOT_USER", "vmail") + curuser = pwd.getpwuid(os.getuid()).pw_name + sudo_user = dovecot_user if curuser != dovecot_user else None if dpath: return exec_cmd("{} {}".format(dpath, params), sudo_user=sudo_user, pinput=pinput, capture_output=capture_output, **kwargs) - else: - raise OSError("doveadm command not found") + + raise OSError("doveadm command not found") def guess_extension_name(): diff --git a/modoboa/lib/viewsets.py b/modoboa/lib/viewsets.py index 5140dede6..a391eb8d8 100644 --- a/modoboa/lib/viewsets.py +++ b/modoboa/lib/viewsets.py @@ -1,6 +1,7 @@ """ViewSet related mixins and tools.""" from rest_framework import viewsets +from reversion.views import RevisionMixin class ExpandableSerializerMixin: @@ -32,3 +33,11 @@ class ExpandableModelViewSet(ExpandableSerializerMixin, """ pass + + +class RevisionModelMixin(RevisionMixin): + """ + A mixin to wrap every request in a revision block. + """ + + pass diff --git a/modoboa/locale/de/LC_MESSAGES/djangojs.po b/modoboa/locale/de/LC_MESSAGES/djangojs.po index 3cfc96ec9..4eb8389b6 100644 --- a/modoboa/locale/de/LC_MESSAGES/djangojs.po +++ b/modoboa/locale/de/LC_MESSAGES/djangojs.po @@ -5,15 +5,16 @@ # # Translators: # Antoine Nguyen , 2020 +# Nicolas John, 2020 # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-03-09 16:48+0100\n" +"POT-Creation-Date: 2020-11-05 15:47+0100\n" "PO-Revision-Date: 2020-05-11 18:45+0000\n" -"Last-Translator: Antoine Nguyen , 2020\n" +"Last-Translator: Nicolas John, 2020\n" "Language-Team: German (https://www.transifex.com/tonio/teams/13749/de/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -41,7 +42,7 @@ msgstr "Warnung" msgid "Cancel" msgstr "Abbrechen" -#: static/js/global.js:342 +#: static/js/global.js:347 msgid "Internal error" msgstr "Interner Fehler" @@ -56,3 +57,11 @@ msgstr "Suche" #: static/js/searchbar.js:28 msgid "Search..." msgstr "Suchen..." + +#: static/js/twocols_nav.js:170 +msgid "Are you sure?" +msgstr "Sind Sie sicher?" + +#: static/js/twocols_nav.js:171 +msgid "This will invalidate your registered application." +msgstr "Dadurch wird Ihre registrierte Anwendung ungültig." diff --git a/modoboa/locale/zh_CN/LC_MESSAGES/djangojs.po b/modoboa/locale/zh_CN/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..2c9fa073a --- /dev/null +++ b/modoboa/locale/zh_CN/LC_MESSAGES/djangojs.po @@ -0,0 +1,66 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +# Translators: +# 李 正 <364109980@qq.com>, 2021 +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-11-05 15:47+0100\n" +"PO-Revision-Date: 2020-05-11 18:45+0000\n" +"Last-Translator: 李 正 <364109980@qq.com>, 2021\n" +"Language-Team: Chinese (China) (https://www.transifex.com/tonio/teams/13749/zh_CN/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_CN\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: bower_components/jquery-ui/jquery-ui.min.js:8 +msgid "'" +msgstr "'" + +#: bower_components/swagger-ui/dist/swagger-ui-bundle.js:133 +msgid "operation" +msgstr "操作" + +#: core/static/core/js/settings.js:16 +msgid "No more log entry to show" +msgstr "没有更多的日志可以显示" + +#: static/js/confirmation.js:44 +msgid "Warning" +msgstr "警告" + +#: static/js/confirmation.js:59 +msgid "Cancel" +msgstr "取消" + +#: static/js/global.js:347 +msgid "Internal error" +msgstr "内部错误" + +#: static/js/listing.js:21 +msgid "No more entry to show" +msgstr "没有更多的条目可以显示" + +#: static/js/listing.js:46 +msgid "Search" +msgstr "查找" + +#: static/js/searchbar.js:28 +msgid "Search..." +msgstr "查找..." + +#: static/js/twocols_nav.js:170 +msgid "Are you sure?" +msgstr "你确定吗?" + +#: static/js/twocols_nav.js:171 +msgid "This will invalidate your registered application." +msgstr "这会使您的注册申请无效。" diff --git a/modoboa/relaydomains/viewsets.py b/modoboa/relaydomains/viewsets.py index d7de1b82d..35841af6a 100644 --- a/modoboa/relaydomains/viewsets.py +++ b/modoboa/relaydomains/viewsets.py @@ -4,10 +4,11 @@ from rest_framework import viewsets from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated from modoboa.admin import models as admin_models +from modoboa.lib.viewsets import RevisionModelMixin from . import serializers -class RelayDomainViewSet(viewsets.ModelViewSet): +class RelayDomainViewSet(RevisionModelMixin, viewsets.ModelViewSet): """RelayDomain viewset.""" permission_classes = [IsAuthenticated, DjangoModelPermissions, ] diff --git a/mysql-requirements.txt b/mysql-requirements.txt index 4b7bd761c..dee426737 100644 --- a/mysql-requirements.txt +++ b/mysql-requirements.txt @@ -1 +1 @@ -mysqlclient<2.0.2 +mysqlclient<2.0.4 diff --git a/requirements.txt b/requirements.txt index 6bf0481e8..b44e5c104 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ django>=2.2,<2.3 django-braces==1.14.0 django-ckeditor==6.0.0 django-phonenumber-field[phonenumbers]==5.0.0 -django-reversion==3.0.8 +django-reversion==3.0.9 django-xforwardedfor-middleware==2.0 django-webpack-loader django-otp==1.0.2 @@ -35,7 +35,7 @@ chardet ovh oath redis -rrdtool>=0.1.11 +rrdtool>=0.1.10 qrcode aiosmtplib diff --git a/setup.cfg b/setup.cfg index 427c34916..947d97f8f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,3 +21,5 @@ max-line-length = 80 [flake8] exclude = migrations max-line-length = 80 +per-file-ignores = + modoboa/core/commands/templates/settings.py.tpl: E265 diff --git a/test-requirements.txt b/test-requirements.txt index 6006e2bb4..d30e8202f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,6 +1,6 @@ argon2-cffi>=16.1.0 factory-boy<=2.12.0 httmock==1.4.0 -testfixtures==6.15.0 +testfixtures==6.17.1 tox asynctest