diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d6899361f..ee04d8d38 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,19 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/PyCQA/pylint - rev: 'v3.0.3' +- repo: local hooks: - id: pylint + name: pylint + entry: pylint + language: system + types: [python] + args: + [ + "-rn", + "-sn", + "--fail-under=5" + ] - repo: https://github.com/psf/black rev: '24.2.0' hooks: diff --git a/doc/conf.py b/doc/conf.py index 10b565e31..19b41d2d2 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -19,9 +19,10 @@ import sys class Mock(MagicMock): @classmethod def __getattr__(cls, name): - return MagicMock() + return MagicMock() -MOCK_MODULES = ['rrdtool'] + +MOCK_MODULES = ["rrdtool"] sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES) # If extensions (or modules to document with autodoc) are in another directory, @@ -196,8 +197,7 @@ htmlhelp_basename = "Modoboadoc" # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ("index", "Modoboa.tex", "Modoboa Documentation", - "Antoine Nguyen", "manual"), + ("index", "Modoboa.tex", "Modoboa Documentation", "Antoine Nguyen", "manual"), ] # The name of an image file (relative to this directory) to place at the top of @@ -228,10 +228,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ("index", "modoboa", "Modoboa Documentation", - ["Antoine Nguyen"], 1) -] +man_pages = [("index", "modoboa", "Modoboa Documentation", ["Antoine Nguyen"], 1)] intersphinx_mapping = { "amavis": ("http://modoboa-amavis.readthedocs.org/en/latest/", None) diff --git a/modoboa/__init__.py b/modoboa/__init__.py index bd516d64a..0ec95caec 100644 --- a/modoboa/__init__.py +++ b/modoboa/__init__.py @@ -8,6 +8,8 @@ except DistributionNotFound: # package is not installed pass + def modoboa_admin(): from modoboa.core.commands import handle_command_line + handle_command_line() diff --git a/modoboa/admin/api/v1/serializers.py b/modoboa/admin/api/v1/serializers.py index 0ba2c6b3c..8582cd576 100644 --- a/modoboa/admin/api/v1/serializers.py +++ b/modoboa/admin/api/v1/serializers.py @@ -15,12 +15,18 @@ from rest_framework import serializers from modoboa.admin import models as admin_models from modoboa.core import ( - constants as core_constants, models as core_models, signals as core_signals + constants as core_constants, + models as core_models, + signals as core_signals, ) from modoboa.lib.exceptions import AliasExists from modoboa.lib import ( - email_utils, exceptions as lib_exceptions, fields as lib_fields, - permissions, validators, web_utils + email_utils, + exceptions as lib_exceptions, + fields as lib_fields, + permissions, + validators, + web_utils, ) from modoboa.parameters import tools as param_tools @@ -35,43 +41,61 @@ class DomainSerializer(serializers.ModelSerializer): help_text=gettext_lazy( "Quota shared between mailboxes. Can be expressed in KB, " "MB (default) or GB. A value of 0 means no quota." - ) + ), ) default_mailbox_quota = serializers.CharField( required=False, help_text=gettext_lazy( - "Default quota in MB applied to mailboxes. A value of 0 means " - "no quota." - ) + "Default quota in MB applied to mailboxes. A value of 0 means " "no quota." + ), ) class Meta: model = models.Domain fields = ( - "pk", "name", "quota", "default_mailbox_quota", "enabled", "type", - "enable_dkim", "dkim_key_selector", "dkim_key_length", - "dkim_public_key", "dkim_private_key_path", - "mailbox_count", "mbalias_count", "domainalias_count", - "dns_global_status", "message_limit", "creation", - "last_modification" + "pk", + "name", + "quota", + "default_mailbox_quota", + "enabled", + "type", + "enable_dkim", + "dkim_key_selector", + "dkim_key_length", + "dkim_public_key", + "dkim_private_key_path", + "mailbox_count", + "mbalias_count", + "domainalias_count", + "dns_global_status", + "message_limit", + "creation", + "last_modification", ) read_only_fields = ( - "pk", "dkim_public_key", "dns_global_status", "allocated_quota_in_percent" - "mailbox_count", "mbalias_count", "domainalias_count", - "enable_dns_checks", "creation", "last_modification" + "pk", + "dkim_public_key", + "dns_global_status", + "allocated_quota_in_percent" "mailbox_count", + "mbalias_count", + "domainalias_count", + "enable_dns_checks", + "creation", + "last_modification", ) def validate_name(self, value): """Check name constraints.""" - domains_must_have_authorized_mx = ( - param_tools.get_global_parameter("domains_must_have_authorized_mx") + domains_must_have_authorized_mx = param_tools.get_global_parameter( + "domains_must_have_authorized_mx" ) user = self.context["request"].user value = value.lower() if domains_must_have_authorized_mx and not user.is_superuser: if not lib.domain_has_authorized_mx(value): raise serializers.ValidationError( - _("No authorized MX record found for this domain")) + _("No authorized MX record found for this domain") + ) return value def validate_quota(self, value): @@ -87,10 +111,9 @@ class DomainSerializer(serializers.ModelSerializer): quota = data.get("quota", 0) default_mailbox_quota = data.get("default_mailbox_quota", 0) if quota != 0 and default_mailbox_quota > quota: - raise serializers.ValidationError({ - "default_mailbox_quota": - _("Cannot be greater than domain quota") - }) + raise serializers.ValidationError( + {"default_mailbox_quota": _("Cannot be greater than domain quota")} + ) return data def create(self, validated_data): @@ -98,15 +121,15 @@ class DomainSerializer(serializers.ModelSerializer): params = dict(param_tools.get_global_parameters("admin")) domain = models.Domain(**validated_data) condition = ( - params["default_domain_message_limit"] is not None and - "message_limit" not in validated_data + params["default_domain_message_limit"] is not None + and "message_limit" not in validated_data ) if condition: domain.message_limit = params["default_domain_message_limit"] creator = self.context["request"].user core_signals.can_create_object.send( - sender=self.__class__, context=creator, - klass=models.Domain, instance=domain) + sender=self.__class__, context=creator, klass=models.Domain, instance=domain + ) domain.save(creator=creator) return domain @@ -125,7 +148,12 @@ class DomainAliasSerializer(serializers.ModelSerializer): class Meta: model = admin_models.DomainAlias - fields = ("pk", "name", "target", "enabled", ) + fields = ( + "pk", + "name", + "target", + "enabled", + ) def validate_target(self, value): """Check target domain.""" @@ -143,14 +171,15 @@ class DomainAliasSerializer(serializers.ModelSerializer): creator = self.context["request"].user try: core_signals.can_create_object.send( - sender=self.__class__, context=creator, - klass=models.DomainAlias) + sender=self.__class__, context=creator, klass=models.DomainAlias + ) core_signals.can_create_object.send( - sender=self.__class__, context=domain_alias.target, - object_type="domain_aliases") + sender=self.__class__, + context=domain_alias.target, + object_type="domain_aliases", + ) except lib_exceptions.ModoboaException as inst: - raise serializers.ValidationError({ - "domain": force_str(inst)}) + raise serializers.ValidationError({"domain": force_str(inst)}) domain_alias.save(creator=creator) return domain_alias @@ -165,8 +194,13 @@ class MailboxSerializer(serializers.ModelSerializer): class Meta: model = models.Mailbox fields = ( - "pk", "full_address", "use_domain_quota", "quota", "message_limit", - "quota_usage", "is_send_only" + "pk", + "full_address", + "use_domain_quota", + "quota", + "message_limit", + "quota_usage", + "is_send_only", ) def get_quota_usage(self, mb) -> int: @@ -185,9 +219,9 @@ class MailboxSerializer(serializers.ModelSerializer): method = self.context["request"].method if not data.get("use_domain_quota", False): if "quota" not in data and method != "PATCH": - raise serializers.ValidationError({ - "quota": _("This field is required") - }) + raise serializers.ValidationError( + {"quota": _("This field is required")} + ) return data @@ -199,15 +233,28 @@ class AccountSerializer(serializers.ModelSerializer): domains = serializers.SerializerMethodField( help_text=_( "List of administered domains (resellers and domain " - "administrators only).")) + "administrators only)." + ) + ) class Meta: model = core_models.User fields = ( - "pk", "username", "first_name", "last_name", "is_active", - "master_user", "mailbox", "role", "language", "phone_number", - "secondary_email", "domains", "tfa_enabled", "date_joined", - "last_login" + "pk", + "username", + "first_name", + "last_name", + "is_active", + "master_user", + "mailbox", + "role", + "language", + "phone_number", + "secondary_email", + "domains", + "tfa_enabled", + "date_joined", + "last_login", ) def __init__(self, *args, **kwargs): @@ -230,7 +277,8 @@ class AccountSerializer(serializers.ModelSerializer): if account.role not in ["DomainAdmins", "Resellers"]: return [] return admin_models.Domain.objects.get_for_admin(account).values_list( - "name", flat=True) + "name", flat=True + ) class AccountExistsSerializer(serializers.Serializer): @@ -240,7 +288,6 @@ class AccountExistsSerializer(serializers.Serializer): class AccountPasswordSerializer(serializers.ModelSerializer): - """A serializer used to change a user password.""" new_password = serializers.CharField() @@ -248,7 +295,9 @@ class AccountPasswordSerializer(serializers.ModelSerializer): class Meta: model = core_models.User fields = ( - "password", "new_password", ) + "password", + "new_password", + ) def validate_password(self, value): """Check password.""" @@ -279,8 +328,7 @@ class WritableAccountSerializer(AccountSerializer): password = serializers.CharField(required=False) class Meta(AccountSerializer.Meta): - fields = AccountSerializer.Meta.fields + ( - "password", "random_password") + fields = AccountSerializer.Meta.fields + ("password", "random_password") def __init__(self, *args, **kwargs): """Adapt fields to current user.""" @@ -290,9 +338,11 @@ class WritableAccountSerializer(AccountSerializer): return user = self.context["request"].user self.fields["role"] = serializers.ChoiceField( - choices=permissions.get_account_roles(user)) + choices=permissions.get_account_roles(user) + ) self.fields["domains"] = serializers.ListField( - child=serializers.CharField(), allow_empty=False, required=False) + child=serializers.CharField(), allow_empty=False, required=False + ) def validate_username(self, value): """Lower case username.""" @@ -303,9 +353,9 @@ class WritableAccountSerializer(AccountSerializer): master_user = data.get("master_user", False) role = data.get("role") if master_user and role != "SuperAdmins": - raise serializers.ValidationError({ - "master_user": _("Not allowed for this role.") - }) + raise serializers.ValidationError( + {"master_user": _("Not allowed for this role.")} + ) if role == "SimpleUsers": username = data.get("username") if username: @@ -318,49 +368,50 @@ class WritableAccountSerializer(AccountSerializer): if not self.instance: data["mailbox"] = { "full_address": username, - "use_domain_quota": True + "use_domain_quota": True, } elif "full_address" in mailbox and mailbox["full_address"] != username: - raise serializers.ValidationError({ - "username": _("Must be equal to mailbox full_address") - }) + raise serializers.ValidationError( + {"username": _("Must be equal to mailbox full_address")} + ) if "mailbox" in data: - self.address, domain_name = email_utils.split_mailbox(data["mailbox"]["full_address"]) - self.domain = get_object_or_404( - admin_models.Domain, name=domain_name) + self.address, domain_name = email_utils.split_mailbox( + data["mailbox"]["full_address"] + ) + self.domain = get_object_or_404(admin_models.Domain, name=domain_name) creator = self.context["request"].user if not creator.can_access(self.domain): raise serializers.ValidationError({"mailbox": _("Permission denied.")}) if not self.instance: try: core_signals.can_create_object.send( - sender=self.__class__, context=creator, - klass=admin_models.Mailbox) + sender=self.__class__, + context=creator, + klass=admin_models.Mailbox, + ) core_signals.can_create_object.send( - sender=self.__class__, context=self.domain, - object_type="mailboxes") + sender=self.__class__, + context=self.domain, + object_type="mailboxes", + ) except lib_exceptions.ModoboaException as inst: - raise serializers.ValidationError({ - "mailbox": force_str(inst)}) - condition = ( - not data.get("random_password") and ( - data.get("password") or - not self.partial - ) + raise serializers.ValidationError({"mailbox": force_str(inst)}) + condition = not data.get("random_password") and ( + data.get("password") or not self.partial ) if condition: password = data.get("password") if password: try: password_validation.validate_password( - data["password"], self.instance) + data["password"], self.instance + ) except ValidationError as exc: - raise serializers.ValidationError({ - "password": exc.messages[0]}) + raise serializers.ValidationError({"password": exc.messages[0]}) elif not self.instance: - raise serializers.ValidationError({ - "password": _("This field is required.") - }) + raise serializers.ValidationError( + {"password": _("This field is required.")} + ) domain_names = data.get("domains") if not domain_names: return data @@ -370,9 +421,9 @@ class WritableAccountSerializer(AccountSerializer): if domain: domains.append(domain) continue - raise serializers.ValidationError({ - "domains": _("Local domain {} does not exist").format(name) - }) + raise serializers.ValidationError( + {"domains": _("Local domain {} does not exist").format(name)} + ) data["domains"] = domains return data @@ -381,10 +432,12 @@ class WritableAccountSerializer(AccountSerializer): full_address = data.pop("full_address") quota = data.pop("quota", None) mb = admin_models.Mailbox( - user=account, address=self.address, domain=self.domain, **data) + user=account, address=self.address, domain=self.domain, **data + ) mb.set_quota(quota, creator.has_perm("admin.add_domain")) default_msg_limit = param_tools.get_global_parameter( - "default_mailbox_message_limit") + "default_mailbox_message_limit" + ) if default_msg_limit is not None: mb.message_limit = default_msg_limit mb.save(creator=creator) @@ -457,20 +510,19 @@ class AliasSerializer(serializers.ModelSerializer): recipients = serializers.ListField( child=lib_fields.DRFEmailFieldUTF8AndEmptyUser(), allow_empty=False, - help_text=gettext_lazy("A list of recipient") + help_text=gettext_lazy("A list of recipient"), ) class Meta: model = admin_models.Alias - fields = ( - "pk", "address", "enabled", "internal", "recipients" - ) + fields = ("pk", "address", "enabled", "internal", "recipients") def validate_address(self, value): """Check domain.""" try: local_part, self.domain = admin_models.validate_alias_address( - value, self.context["request"].user, instance=self.instance) + value, self.context["request"].user, instance=self.instance + ) except ValidationError as err: raise serializers.ValidationError(err) except AliasExists: @@ -481,7 +533,8 @@ class AliasSerializer(serializers.ModelSerializer): """Create appropriate objects.""" creator = self.context["request"].user return admin_models.Alias.objects.create( - creator=creator, domain=self.domain, **validated_data) + creator=creator, domain=self.domain, **validated_data + ) def update(self, instance, validated_data): """Update objects.""" @@ -509,7 +562,8 @@ class SenderAddressSerializer(serializers.ModelSerializer): user = self.context["request"].user if domain and not user.can_access(domain): raise serializers.ValidationError( - _("You don't have access to this domain.")) + _("You don't have access to this domain.") + ) return value.lower() def validate_mailbox(self, value): @@ -517,7 +571,8 @@ class SenderAddressSerializer(serializers.ModelSerializer): user = self.context["request"].user if not user.can_access(value): raise serializers.ValidationError( - _("You don't have access to this mailbox.")) + _("You don't have access to this mailbox.") + ) return value diff --git a/modoboa/admin/api/v1/tests.py b/modoboa/admin/api/v1/tests.py index 7922e8c29..3cc1fd953 100644 --- a/modoboa/admin/api/v1/tests.py +++ b/modoboa/admin/api/v1/tests.py @@ -28,7 +28,8 @@ class DomainAPITestCase(ModoAPITestCase): super(DomainAPITestCase, cls).setUpTestData() factories.populate_database() cls.da_token = Token.objects.create( - user=core_models.User.objects.get(username="admin@test.com")) + user=core_models.User.objects.get(username="admin@test.com") + ) def test_get_domains(self): """Retrieve a list of domains.""" @@ -37,8 +38,7 @@ class DomainAPITestCase(ModoAPITestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 2) domain = response.data[0] - url = reverse( - "v1:domain-detail", args=[domain["pk"]]) + url = reverse("v1:domain-detail", args=[domain["pk"]]) response = self.client.get(url) data = response.json() self.assertEqual(response.status_code, 200) @@ -65,10 +65,10 @@ class DomainAPITestCase(ModoAPITestCase): self.assertEqual(response.status_code, 400) self.assertIn("default_mailbox_quota", response.data) - self.client.credentials( - HTTP_AUTHORIZATION="Token " + self.da_token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.da_token.key) response = self.client.post( - url, {"name": "test4.com", "default_mailbox_quota": 10}) + url, {"name": "test4.com", "default_mailbox_quota": 10} + ) self.assertEqual(response.status_code, 403) @mock.patch.object(dns.resolver.Resolver, "resolve") @@ -81,19 +81,19 @@ class DomainAPITestCase(ModoAPITestCase): self.set_global_parameter("valid_mxs", "192.0.2.1 2001:db8::1") self.set_global_parameter("domains_must_have_authorized_mx", True) reseller = core_factories.UserFactory( - username="reseller", groups=("Resellers", )) + username="reseller", groups=("Resellers",) + ) token = Token.objects.create(user=reseller) self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) url = reverse("v1:domain-list") response = self.client.post( - url, {"name": "no-mx.example.com", "quota": 0, - "default_mailbox_quota": 10} + url, {"name": "no-mx.example.com", "quota": 0, "default_mailbox_quota": 10} ) self.assertEqual(response.status_code, 400) self.assertEqual( - response.json()["name"][0], - "No authorized MX record found for this domain") + response.json()["name"][0], "No authorized MX record found for this domain" + ) mock_getaddrinfo.side_effect = utils.mock_ip_query_result response = self.client.post( @@ -104,25 +104,28 @@ class DomainAPITestCase(ModoAPITestCase): def test_update_domain(self): """Check domain update.""" domain = models.Domain.objects.get(name="test.com") - models.Mailbox.objects.filter( - domain__name="test.com", address="user").update( - use_domain_quota=True) + models.Mailbox.objects.filter(domain__name="test.com", address="user").update( + use_domain_quota=True + ) url = reverse("v1:domain-detail", args=[domain.pk]) response = self.client.put( - url, {"name": "test.com", "default_mailbox_quota": 1000}) + url, {"name": "test.com", "default_mailbox_quota": 1000} + ) self.assertEqual(response.status_code, 200) domain.refresh_from_db() self.assertEqual(domain.default_mailbox_quota, 1000) - mb = models.Mailbox.objects.get( - domain__name="test.com", address="user") + mb = models.Mailbox.objects.get(domain__name="test.com", address="user") self.assertEqual(mb.quota, 1000) response = self.client.put( - url, {"name": "test42.com", "default_mailbox_quota": 1000}) + url, {"name": "test42.com", "default_mailbox_quota": 1000} + ) self.assertEqual(response.status_code, 200) self.assertTrue( models.Mailbox.objects.filter( - address="user", domain__name="test42.com").exists()) + address="user", domain__name="test42.com" + ).exists() + ) def test_patch_domain(self): """Check domain partial update.""" @@ -137,8 +140,7 @@ class DomainAPITestCase(ModoAPITestCase): """Try to remove DKIM from a domain.""" domain = models.Domain.objects.get(name="test.com") url = reverse("v1:domain-detail", args=[domain.pk]) - response = self.client.put( - url, {"name": "test.com", "enable_dkim": False}) + response = self.client.put(url, {"name": "test.com", "enable_dkim": False}) self.assertEqual(response.status_code, 200) domain.refresh_from_db() self.assertEqual(domain.dkim_private_key_path, "") @@ -161,11 +163,14 @@ class DomainAliasAPITestCase(ModoAPITestCase): super(DomainAliasAPITestCase, cls).setUpTestData() factories.populate_database() cls.dom_alias1 = factories.DomainAliasFactory( - name="dalias1.com", target__name="test.com") + name="dalias1.com", target__name="test.com" + ) cls.dom_alias2 = factories.DomainAliasFactory( - name="dalias2.com", target__name="test2.com") + name="dalias2.com", target__name="test2.com" + ) cls.da_token = Token.objects.create( - user=core_models.User.objects.get(username="admin@test.com")) + user=core_models.User.objects.get(username="admin@test.com") + ) def test_get(self): """Retrieve a list of domain aliases.""" @@ -173,8 +178,7 @@ class DomainAliasAPITestCase(ModoAPITestCase): response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 2) - url = reverse( - "v1:domain_alias-detail", args=[response.data[0]["pk"]]) + url = reverse("v1:domain_alias-detail", args=[response.data[0]["pk"]]) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.data["name"], "dalias1.com") @@ -184,8 +188,7 @@ class DomainAliasAPITestCase(ModoAPITestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) - self.client.credentials( - HTTP_AUTHORIZATION="Token " + self.da_token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.da_token.key) response = self.client.get(reverse("v1:domain_alias-list")) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 0) @@ -194,31 +197,25 @@ class DomainAliasAPITestCase(ModoAPITestCase): """Try to create a new domain alias.""" url = reverse("v1:domain_alias-list") target = models.Domain.objects.get(name="test.com") - data = { - "name": "dalias3.com", - "target": target.pk - } + data = {"name": "dalias3.com", "target": target.pk} response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 201) dalias = response.json() - dalias = models.DomainAlias.objects.filter( - pk=dalias["pk"]).first() + dalias = models.DomainAlias.objects.filter(pk=dalias["pk"]).first() self.assertEqual(dalias.target, target) - self.client.credentials( - HTTP_AUTHORIZATION="Token " + self.da_token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.da_token.key) response = self.client.post( - url, {"name": "dalias4.com", "target": target.pk}, format="json") + url, {"name": "dalias4.com", "target": target.pk}, format="json" + ) self.assertEqual(response.status_code, 403) def test_put(self): """Try to update a domain alias.""" dalias = models.DomainAlias.objects.get(name="dalias1.com") url = reverse("v1:domain_alias-detail", args=[dalias.pk]) - data = { - "name": "dalias3.com", "target": dalias.target.pk - } + data = {"name": "dalias3.com", "target": dalias.target.pk} response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, 200) @@ -232,8 +229,7 @@ class DomainAliasAPITestCase(ModoAPITestCase): url = reverse("v1:domain_alias-detail", args=[dalias.pk]) response = self.client.delete(url) self.assertEqual(response.status_code, 204) - self.assertFalse( - models.DomainAlias.objects.filter(pk=dalias.pk).exists()) + self.assertFalse(models.DomainAlias.objects.filter(pk=dalias.pk).exists()) class AccountAPITestCase(ModoAPITestCase): @@ -243,10 +239,7 @@ class AccountAPITestCase(ModoAPITestCase): "username": "fromapi@test.com", "role": "SimpleUsers", "password": "Toto1234", - "mailbox": { - "full_address": "fromapi@test.com", - "quota": 10 - } + "mailbox": {"full_address": "fromapi@test.com", "quota": 10}, } @classmethod @@ -255,15 +248,15 @@ class AccountAPITestCase(ModoAPITestCase): super(AccountAPITestCase, cls).setUpTestData() factories.populate_database() cls.da_token = Token.objects.create( - user=core_models.User.objects.get(username="admin@test.com")) + user=core_models.User.objects.get(username="admin@test.com") + ) def setUp(self): """Test setup.""" super().setUp() - self.set_global_parameters({ - "enable_admin_limits": False, - "enable_domain_limits": False - }, app="limits") + self.set_global_parameters( + {"enable_admin_limits": False, "enable_domain_limits": False}, app="limits" + ) def test_get_accounts(self): """Retrieve a list of accounts.""" @@ -351,12 +344,10 @@ class AccountAPITestCase(ModoAPITestCase): response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 201) domain = models.Domain.objects.get(name="test.com") - admin = core_models.User.objects.get( - pk=response.json()["pk"]) + admin = core_models.User.objects.get(pk=response.json()["pk"]) self.assertIn(admin, domain.admins) - response = self.client.get( - reverse("v1:account-detail", args=[admin.pk])) + response = self.client.get(reverse("v1:account-detail", args=[admin.pk])) self.assertEqual(response.status_code, 200) self.assertIn("domains", response.json()) @@ -376,8 +367,7 @@ class AccountAPITestCase(ModoAPITestCase): user = core_models.User.objects.filter(pk=account["pk"]).first() self.assertIsNot(user, None) self.assertIsNot(user.mailbox, None) - self.assertEqual( - user.mailbox.quota, user.mailbox.domain.default_mailbox_quota) + self.assertEqual(user.mailbox.quota, user.mailbox.domain.default_mailbox_quota) def test_create_existing_account(self): """Check if unicity is respected.""" @@ -404,8 +394,7 @@ class AccountAPITestCase(ModoAPITestCase): def test_create_account_as_domadmin(self): """As DomainAdmin, try to create a new account.""" - self.client.credentials( - HTTP_AUTHORIZATION="Token " + self.da_token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.da_token.key) data = copy.deepcopy(self.ACCOUNT_DATA) data["mailbox"]["quota"] = 1000 url = reverse("v1:account-list") @@ -414,8 +403,7 @@ class AccountAPITestCase(ModoAPITestCase): self.assertEqual(response.status_code, 400) data["username"] = "fromapi@test2.com" - data["mailbox"].update({"full_address": "fromapi@test2.com", - "quota": 10}) + data["mailbox"].update({"full_address": "fromapi@test2.com", "quota": 10}) response = self.client.post(url, data, format="json") # Should fail because admin has no access to @test2.com domain self.assertEqual(response.status_code, 400) @@ -442,8 +430,8 @@ class AccountAPITestCase(ModoAPITestCase): "password": "Toto1234", "mailbox": { "full_address": "fromapi@test.com", - "quota": account.mailbox.quota - } + "quota": account.mailbox.quota, + }, } response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, 200) @@ -460,14 +448,9 @@ class AccountAPITestCase(ModoAPITestCase): def test_update_account_with_no_mailbox(self): """Try to disable an account.""" - account = core_factories.UserFactory( - username="reseller", groups=("Resellers", )) + account = core_factories.UserFactory(username="reseller", groups=("Resellers",)) url = reverse("v1:account-detail", args=[account.pk]) - data = { - "username": "reseller", - "role": account.role, - "is_active": False - } + data = {"username": "reseller", "role": account.role, "is_active": False} response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, 200) @@ -480,7 +463,7 @@ class AccountAPITestCase(ModoAPITestCase): "username": "fromapi@test.com", "mailbox": { "full_address": "fromapi@test.com", - } + }, } response = self.client.patch(url, data, format="json") self.assertEqual(response.status_code, 200) @@ -498,9 +481,9 @@ class AccountAPITestCase(ModoAPITestCase): "password": "Toto1234", "mailbox": { "full_address": account.mailbox.full_address, - "quota": account.mailbox.quota + "quota": account.mailbox.quota, }, - "domains": ["test.com", "test2.com"] + "domains": ["test.com", "test2.com"], } response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, 200) @@ -525,8 +508,8 @@ class AccountAPITestCase(ModoAPITestCase): "password": "Toto1234", "mailbox": { "full_address": "fromapi@test3.com", - "quota": account.mailbox.quota - } + "quota": account.mailbox.quota, + }, } response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, 404) @@ -539,20 +522,17 @@ class AccountAPITestCase(ModoAPITestCase): url = reverse("v1:account-detail", args=[account.pk]) response = self.client.delete(url) self.assertEqual(response.status_code, 204) - self.assertFalse( - core_models.User.objects.filter(pk=account.pk).exists()) + self.assertFalse(core_models.User.objects.filter(pk=account.pk).exists()) self.assertFalse(domadmin.can_access(account)) def test_account_exists(self): """Validate /exists/ service.""" url = reverse("v1:account-exists") - response = self.client.get( - "{}?email={}".format(url, "user@test.com")) + response = self.client.get("{}?email={}".format(url, "user@test.com")) self.assertEqual(response.status_code, 200) content = response.json() self.assertTrue(content["exists"]) - response = self.client.get( - "{}?email={}".format(url, "pipo@test.com")) + response = self.client.get("{}?email={}".format(url, "pipo@test.com")) self.assertEqual(response.status_code, 200) content = response.json() self.assertFalse(content["exists"]) @@ -562,17 +542,16 @@ class AccountAPITestCase(ModoAPITestCase): def test_change_password(self): """Check the change password service.""" account = core_models.User.objects.get(username="user@test.com") - url = reverse( - "v1:account-password", args=[account.pk]) + url = reverse("v1:account-password", args=[account.pk]) response = self.client.put( - url, {"password": "toto", "new_password": "pass"}, - format="json") + url, {"password": "toto", "new_password": "pass"}, format="json" + ) # must fail because password is too weak self.assertEqual(response.status_code, 400) response = self.client.put( - url, {"password": "toto", "new_password": "Toto1234"}, - format="json") + url, {"password": "toto", "new_password": "Toto1234"}, format="json" + ) self.assertEqual(response.status_code, 200) account.refresh_from_db() self.assertTrue(account.check_password("Toto1234")) @@ -585,13 +564,16 @@ class AccountAPITestCase(ModoAPITestCase): response = self.client.post(url) self.assertEqual(response.status_code, 404) - self.set_global_parameters({ - "sms_password_recovery": True, - "sms_provider": "ovh", - "sms_ovh_application_key": "key", - "sms_ovh_application_secret": "secret", - "sms_ovh_consumer_key": "consumer" - }, app="core") + self.set_global_parameters( + { + "sms_password_recovery": True, + "sms_provider": "ovh", + "sms_ovh_application_key": "key", + "sms_ovh_application_secret": "secret", + "sms_ovh_consumer_key": "consumer", + }, + app="core", + ) # No email provided response = self.client.post(url) self.assertEqual(response.status_code, 400) @@ -615,21 +597,19 @@ class AliasAPITestCase(ModoAPITestCase): ALIAS_DATA = { "address": "alias_fromapi@test.com", - "recipients": [ - "user@test.com", "postmaster@test.com", "user_éé@nonlocal.com" - ] + "recipients": ["user@test.com", "postmaster@test.com", "user_éé@nonlocal.com"], } @classmethod def setUpTestData(cls): # NOQA:N802 """Create test data.""" super(AliasAPITestCase, cls).setUpTestData() - cls.localconfig.parameters.set_value( - "enable_admin_limits", False, app="limits") + cls.localconfig.parameters.set_value("enable_admin_limits", False, app="limits") cls.localconfig.save() factories.populate_database() cls.da_token = Token.objects.create( - user=core_models.User.objects.get(username="admin@test.com")) + user=core_models.User.objects.get(username="admin@test.com") + ) def test_get_aliases(self): """Retrieve a list of aliases.""" @@ -664,13 +644,23 @@ class AliasAPITestCase(ModoAPITestCase): domadmin = core_models.User.objects.get(username="admin@test.com") self.assertTrue(domadmin.can_access(alias)) self.assertEqual(alias.aliasrecipient_set.count(), 3) - self.assertTrue(alias.aliasrecipient_set.filter( - address="user@test.com", r_mailbox__isnull=False).exists()) - self.assertTrue(alias.aliasrecipient_set.filter( - address="postmaster@test.com", r_alias__isnull=False).exists()) - self.assertTrue(alias.aliasrecipient_set.filter( - address="user_éé@nonlocal.com", - r_mailbox__isnull=True, r_alias__isnull=True).exists()) + self.assertTrue( + alias.aliasrecipient_set.filter( + address="user@test.com", r_mailbox__isnull=False + ).exists() + ) + self.assertTrue( + alias.aliasrecipient_set.filter( + address="postmaster@test.com", r_alias__isnull=False + ).exists() + ) + self.assertTrue( + alias.aliasrecipient_set.filter( + address="user_éé@nonlocal.com", + r_mailbox__isnull=True, + r_alias__isnull=True, + ).exists() + ) # Create catchall alias data = copy.deepcopy(self.ALIAS_DATA) @@ -691,8 +681,7 @@ class AliasAPITestCase(ModoAPITestCase): def test_create_alias_as_domadmin(self): """As DomainAdmin, try to create a new alias.""" - self.client.credentials( - HTTP_AUTHORIZATION="Token " + self.da_token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.da_token.key) url = reverse("v1:alias-list") response = self.client.post(url, self.ALIAS_DATA, format="json") self.assertEqual(response.status_code, 201) @@ -710,26 +699,20 @@ class AliasAPITestCase(ModoAPITestCase): url = reverse("v1:alias-detail", args=[alias.pk]) data = { "address": "alias@test.com", - "recipients": ["user@test.com", "user@nonlocal.com"] + "recipients": ["user@test.com", "user@nonlocal.com"], } response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, 200) alias.refresh_from_db() self.assertEqual(alias.aliasrecipient_set.count(), 2) - data = { - "address": "alias@test.com", - "recipients": ["user@nonlocal.com"] - } + data = {"address": "alias@test.com", "recipients": ["user@nonlocal.com"]} response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, 200) alias.refresh_from_db() self.assertEqual(alias.aliasrecipient_set.count(), 1) - data = { - "address": "alias@test.com", - "recipients": [] - } + data = {"address": "alias@test.com", "recipients": []} response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, 400) @@ -741,13 +724,12 @@ class AliasAPITestCase(ModoAPITestCase): url = reverse("v1:alias-detail", args=[alias.pk]) response = self.client.delete(url) self.assertEqual(response.status_code, 204) - self.assertFalse( - models.Alias.objects.filter(pk=alias.pk).exists()) + self.assertFalse(models.Alias.objects.filter(pk=alias.pk).exists()) self.assertFalse(domadmin.can_access(alias)) self.assertFalse( models.AliasRecipient.objects.filter( - address="user@test.com", alias__address="alias@test.com") - .exists() + address="user@test.com", alias__address="alias@test.com" + ).exists() ) @@ -758,18 +740,18 @@ class SenderAddressAPITestCase(ModoAPITestCase): def setUpTestData(cls): # NOQA:N802 """Create test data.""" super(SenderAddressAPITestCase, cls).setUpTestData() - cls.localconfig.parameters.set_value( - "enable_admin_limits", False, app="limits") + cls.localconfig.parameters.set_value("enable_admin_limits", False, app="limits") cls.localconfig.save() factories.populate_database() cls.sa1 = factories.SenderAddressFactory( address="test@domain.ext", mailbox__user__username="user@test.com", mailbox__address="user", - mailbox__domain__name="test.com" + mailbox__domain__name="test.com", ) cls.da_token = Token.objects.create( - user=core_models.User.objects.get(username="admin@test.com")) + user=core_models.User.objects.get(username="admin@test.com") + ) def test_list(self): """Retrieve a list of sender addresses.""" @@ -782,35 +764,31 @@ class SenderAddressAPITestCase(ModoAPITestCase): def test_create(self): """Create a new sender addresses.""" url = reverse("v1:sender_address-list") - mbox = models.Mailbox.objects.get( - address="admin", domain__name="test.com") + mbox = models.Mailbox.objects.get(address="admin", domain__name="test.com") data = {"address": "@extdomain.test", "mailbox": mbox.pk} response = self.client.post(url, data) self.assertEqual(response.status_code, 201) - self.client.credentials( - HTTP_AUTHORIZATION="Token " + self.da_token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.da_token.key) data = {"address": "user@test2.com", "mailbox": mbox.pk} response = self.client.post(url, data) self.assertEqual(response.status_code, 400) self.assertEqual( - response.json()["address"][0], - "You don't have access to this domain.") + response.json()["address"][0], "You don't have access to this domain." + ) - mbox = models.Mailbox.objects.get( - address="admin", domain__name="test2.com") + mbox = models.Mailbox.objects.get(address="admin", domain__name="test2.com") data = {"address": "toto@titi.com", "mailbox": mbox.pk} response = self.client.post(url, data) self.assertEqual(response.status_code, 400) self.assertEqual( - response.json()["mailbox"][0], - "You don't have access to this mailbox.") + response.json()["mailbox"][0], "You don't have access to this mailbox." + ) def test_patch(self): """Patch an existing sender address.""" url = reverse("v1:sender_address-detail", args=[self.sa1.pk]) - mbox = models.Mailbox.objects.get( - address="user", domain__name="test.com") + mbox = models.Mailbox.objects.get(address="user", domain__name="test.com") data = {"address": "@extdomain.test"} response = self.client.patch(url, data) self.assertEqual(response.status_code, 200) @@ -821,8 +799,7 @@ class SenderAddressAPITestCase(ModoAPITestCase): def test_update(self): """Update an existing sender address.""" url = reverse("v1:sender_address-detail", args=[self.sa1.pk]) - mbox = models.Mailbox.objects.get( - address="user", domain__name="test.com") + mbox = models.Mailbox.objects.get(address="user", domain__name="test.com") data = {"address": "@extdomain.test", "mailbox": mbox.pk} response = self.client.put(url, data) self.assertEqual(response.status_code, 200) diff --git a/modoboa/admin/api/v1/urls.py b/modoboa/admin/api/v1/urls.py index e2e514aaa..80838574d 100644 --- a/modoboa/admin/api/v1/urls.py +++ b/modoboa/admin/api/v1/urls.py @@ -6,11 +6,11 @@ from . import viewsets router = routers.SimpleRouter() router.register(r"domains", viewsets.DomainViewSet, basename="domain") -router.register( - r"domainaliases", viewsets.DomainAliasViewSet, basename="domain_alias") +router.register(r"domainaliases", viewsets.DomainAliasViewSet, basename="domain_alias") router.register(r"accounts", viewsets.AccountViewSet, basename="account") router.register(r"aliases", viewsets.AliasViewSet, basename="alias") router.register( - r"senderaddresses", viewsets.SenderAddressViewSet, basename="sender_address") + r"senderaddresses", viewsets.SenderAddressViewSet, basename="sender_address" +) urlpatterns = router.urls diff --git a/modoboa/admin/api/v1/viewsets.py b/modoboa/admin/api/v1/viewsets.py index 892be34d0..f6ed5804f 100644 --- a/modoboa/admin/api/v1/viewsets.py +++ b/modoboa/admin/api/v1/viewsets.py @@ -25,23 +25,24 @@ from . import serializers @extend_schema_view( retrieve=extend_schema( description="Retrieve a particular domain", - summary="Retrieve a particular domain" + summary="Retrieve a particular domain", ), list=extend_schema( - description="Retrieve a list of domains", - summary="Retrieve a list of domains" + description="Retrieve a list of domains", summary="Retrieve a list of domains" ), create=extend_schema( - description="Create a new domain", - summary="Create a new domain" - ) + description="Create a new domain", summary="Create a new domain" + ), ) -class DomainViewSet(GetThrottleViewsetMixin, - lib_viewsets.RevisionModelMixin, - viewsets.ModelViewSet): +class DomainViewSet( + GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet +): """Domain viewset.""" - permission_classes = [IsAuthenticated, DjangoModelPermissions, ] + permission_classes = [ + IsAuthenticated, + DjangoModelPermissions, + ] serializer_class = serializers.DomainSerializer def get_queryset(self): @@ -63,13 +64,17 @@ class DomainAliasFilterSet(dj_filters.FilterSet): fields = ["domain"] -class DomainAliasViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, - viewsets.ModelViewSet): +class DomainAliasViewSet( + GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet +): """ViewSet for DomainAlias.""" - filter_backends = (dj_filters.DjangoFilterBackend, ) + filter_backends = (dj_filters.DjangoFilterBackend,) filterset_class = DomainAliasFilterSet - permission_classes = [IsAuthenticated, DjangoModelPermissions, ] + permission_classes = [ + IsAuthenticated, + DjangoModelPermissions, + ] renderer_classes = (renderers.JSONRenderer, lib_renderers.CSVRenderer) serializer_class = serializers.DomainAliasSerializer @@ -83,11 +88,16 @@ class DomainAliasViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixi return context -class AccountViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet): +class AccountViewSet( + GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet +): """ViewSet for User/Mailbox.""" - filter_backends = (filters.SearchFilter, ) - permission_classes = [IsAuthenticated, DjangoModelPermissions, ] + filter_backends = (filters.SearchFilter,) + permission_classes = [ + IsAuthenticated, + DjangoModelPermissions, + ] search_fields = ("^first_name", "^last_name", "^email") def get_throttles(self): @@ -105,15 +115,14 @@ class AccountViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, v "password": serializers.AccountPasswordSerializer, "reset_password": serializers.ResetPasswordSerializer, } - return action_dict.get( - self.action, serializers.WritableAccountSerializer) + return action_dict.get(self.action, serializers.WritableAccountSerializer) def get_queryset(self): """Filter queryset based on current user.""" user = self.request.user - ids = user.objectaccess_set \ - .filter(content_type=ContentType.objects.get_for_model(user)) \ - .values_list("object_id", flat=True) + ids = user.objectaccess_set.filter( + content_type=ContentType.objects.get_for_model(user) + ).values_list("object_id", flat=True) queryset = core_models.User.objects.filter(pk__in=ids) domain = self.request.query_params.get("domain") if domain: @@ -131,8 +140,7 @@ class AccountViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, v if serializer.is_valid(): serializer.save() return Response() - return Response( - serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @action(detail=False) def exists(self, request): @@ -156,25 +164,23 @@ class AccountViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, v @action(methods=["post"], detail=False) def reset_password(self, request): """Reset account password and send a new one by SMS.""" - sms_password_recovery = ( - request.localconfig.parameters - .get_value("sms_password_recovery", app="core") + sms_password_recovery = request.localconfig.parameters.get_value( + "sms_password_recovery", app="core" ) if not sms_password_recovery: return Response(status=404) serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = core_models.User.objects.filter( - email=serializer.validated_data["email"]).first() + email=serializer.validated_data["email"] + ).first() if not user or not user.phone_number: return Response(status=404) - backend = sms_backends.get_active_backend( - request.localconfig.parameters) + backend = sms_backends.get_active_backend(request.localconfig.parameters) if not backend: return Response(status=404) password = lib.make_password() - content = _("Here is your new Modoboa password: {}").format( - password) + content = _("Here is your new Modoboa password: {}").format(password) if not backend.send(content, [str(user.phone_number)]): body = {"status": "ko"} else: @@ -185,23 +191,26 @@ class AccountViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, v return Response(body) -class AliasViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet): +class AliasViewSet( + GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet +): """ create: Create a new alias instance. """ - permission_classes = [IsAuthenticated, DjangoModelPermissions, ] + permission_classes = [ + IsAuthenticated, + DjangoModelPermissions, + ] serializer_class = serializers.AliasSerializer def get_queryset(self): """Filter queryset based on current user.""" user = self.request.user - ids = ( - user.objectaccess_set.filter( - content_type=ContentType.objects.get_for_model(models.Alias)) - .values_list("object_id", flat=True) - ) + ids = user.objectaccess_set.filter( + content_type=ContentType.objects.get_for_model(models.Alias) + ).values_list("object_id", flat=True) queryset = models.Alias.objects.filter(pk__in=ids) domain = self.request.query_params.get("domain") if domain: @@ -217,21 +226,23 @@ class SenderAddressFilterSet(dj_filters.FilterSet): fields = ["mailbox"] -class SenderAddressViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, - viewsets.ModelViewSet): +class SenderAddressViewSet( + GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet +): """View set for SenderAddress model.""" - filter_backends = (dj_filters.DjangoFilterBackend, ) + filter_backends = (dj_filters.DjangoFilterBackend,) filterset_class = SenderAddressFilterSet - permission_classes = [IsAuthenticated, DjangoModelPermissions, ] + permission_classes = [ + IsAuthenticated, + DjangoModelPermissions, + ] serializer_class = serializers.SenderAddressSerializer def get_queryset(self): """Filter queryset based on current user.""" user = self.request.user - mb_ids = ( - user.objectaccess_set.filter( - content_type=ContentType.objects.get_for_model(models.Mailbox)) - .values_list("object_id", flat=True) - ) + mb_ids = user.objectaccess_set.filter( + content_type=ContentType.objects.get_for_model(models.Mailbox) + ).values_list("object_id", flat=True) return models.SenderAddress.objects.filter(mailbox__pk__in=mb_ids) diff --git a/modoboa/admin/api/v2/serializers.py b/modoboa/admin/api/v2/serializers.py index e3e44227a..26fc74906 100644 --- a/modoboa/admin/api/v2/serializers.py +++ b/modoboa/admin/api/v2/serializers.py @@ -56,7 +56,9 @@ class DomainSerializer(v1_serializers.DomainSerializer): class Meta(v1_serializers.DomainSerializer.Meta): fields = v1_serializers.DomainSerializer.Meta.fields + ( - "domain_admin", "transport", "opened_alarms_count" + "domain_admin", + "transport", + "opened_alarms_count", ) def __init__(self, *args, **kwargs): @@ -71,7 +73,9 @@ class DomainSerializer(v1_serializers.DomainSerializer): result = super().validate(data) dtype = data.get("type", "domain") if dtype == "relaydomain" and not data.get("transport"): - raise serializers.ValidationError({"transport": _("This field is required")}) + raise serializers.ValidationError( + {"transport": _("This field is required")} + ) return result def create(self, validated_data): @@ -96,16 +100,15 @@ class DomainSerializer(v1_serializers.DomainSerializer): except core_models.User.DoesNotExist: pass else: - raise lib_exceptions.Conflict( - _("User '%s' already exists") % username) + raise lib_exceptions.Conflict(_("User '%s' already exists") % username) user = self.context["request"].user core_signals.can_create_object.send( - self.__class__, context=user, klass=models.Mailbox) + self.__class__, context=user, klass=models.Mailbox + ) da = core_models.User(username=username, email=username, is_active=True) password = domain_admin.get("password") if password is None: - password = param_tools.get_global_parameter( - "default_password", app="core") + password = param_tools.get_global_parameter("default_password", app="core") da.set_password(password) da.save() da.role = "DomainAdmins" @@ -115,25 +118,28 @@ class DomainSerializer(v1_serializers.DomainSerializer): if domain_admin["with_mailbox"]: dom_admin_username = domain_admin["username"] mb = models.Mailbox( - address=dom_admin_username, domain=domain, - user=da, use_domain_quota=True + address=dom_admin_username, + domain=domain, + user=da, + use_domain_quota=True, ) - mb.set_quota( - override_rules=user.has_perm("admin.change_domain")) + mb.set_quota(override_rules=user.has_perm("admin.change_domain")) mb.save(creator=user) # 3. Create aliases if needed condition = ( - domain.type == "domain" and - domain_admin["with_aliases"] and - dom_admin_username != "postmaster" + domain.type == "domain" + and domain_admin["with_aliases"] + and dom_admin_username != "postmaster" ) if condition: core_signals.can_create_object.send( - self.__class__, context=user, klass=models.Alias) - address = u"postmaster@{}".format(domain.name) + self.__class__, context=user, klass=models.Alias + ) + address = "postmaster@{}".format(domain.name) alias = models.Alias.objects.create( - address=address, domain=domain, enabled=True) + address=address, domain=domain, enabled=True + ) alias.set_recipients([mb.full_address]) alias.post_create(user) @@ -162,9 +168,9 @@ class DomainSerializer(v1_serializers.DomainSerializer): instance.save() if resources: for resource in resources: - instance.domainobjectlimit_set.filter( - name=resource["name"] - ).update(max_value=resource["max_value"]) + instance.domainobjectlimit_set.filter(name=resource["name"]).update( + max_value=resource["max_value"] + ) return instance @@ -190,16 +196,19 @@ class AdminGlobalParametersSerializer(serializers.Serializer): enable_dnsbl_checks = serializers.BooleanField(default=True) dkim_keys_storage_dir = serializers.CharField(allow_blank=True) dkim_default_key_length = serializers.ChoiceField( - default=2048, choices=constants.DKIM_KEY_LENGTHS) + default=2048, choices=constants.DKIM_KEY_LENGTHS + ) default_domain_quota = serializers.IntegerField(default=0) default_domain_message_limit = serializers.IntegerField( - required=False, allow_null=True) + required=False, allow_null=True + ) # Mailboxes settings handle_mailboxes = serializers.BooleanField(default=False) default_mailbox_quota = serializers.IntegerField(default=0) default_mailbox_message_limit = serializers.IntegerField( - required=False, allow_null=True) + required=False, allow_null=True + ) auto_account_removal = serializers.BooleanField(default=False) auto_create_domain_and_mailbox = serializers.BooleanField(default=True) create_alias_on_mbox_rename = serializers.BooleanField(default=False) @@ -207,30 +216,22 @@ class AdminGlobalParametersSerializer(serializers.Serializer): def validate_default_domain_quota(self, value): """Ensure quota is a positive integer.""" if value < 0: - raise serializers.ValidationError( - _("Must be a positive integer") - ) + raise serializers.ValidationError(_("Must be a positive integer")) return value def validate_default_mailbox_quota(self, value): """Ensure quota is a positive integer.""" if value < 0: - raise serializers.ValidationError( - _("Must be a positive integer") - ) + raise serializers.ValidationError(_("Must be a positive integer")) return value def validate_dkim_keys_storage_dir(self, value): """Check that directory exists.""" if value: if not os.path.isdir(value): - raise serializers.ValidationError( - _("Directory not found.") - ) + raise serializers.ValidationError(_("Directory not found.")) if not os.access(value, os.W_OK): - raise serializers.ValidationError( - _("Directory non-writable") - ) + raise serializers.ValidationError(_("Directory non-writable")) code, output = exec_cmd("which openssl") if code: raise serializers.ValidationError( @@ -244,8 +245,7 @@ class AdminGlobalParametersSerializer(serializers.Serializer): return value try: ip_addresses = [ - ipaddress.ip_network(v.strip()) - for v in value.split() if v.strip() + ipaddress.ip_network(v.strip()) for v in value.split() if v.strip() ] except ValueError: raise serializers.ValidationError( @@ -255,11 +255,12 @@ class AdminGlobalParametersSerializer(serializers.Serializer): def validate_auto_create_domain_and_mailbox(self, value): """ - Make sure the option does not get deactivated - while imap migration is enabled. + Make sure the option does not get deactivated + while imap migration is enabled. """ - if not value and param_tools.get_global_parameter("enabled_imapmigration", - "imap_migration"): + if not value and param_tools.get_global_parameter( + "enabled_imapmigration", "imap_migration" + ): raise serializers.ValidationError( _("Please disable imap migration before disabling this option.") ) @@ -268,13 +269,14 @@ class AdminGlobalParametersSerializer(serializers.Serializer): def validate(self, data): """Check MX options.""" condition = ( - data.get("enable_mx_checks") and - data.get("domains_must_have_authorized_mx") and - not data.get("valid_mxs")) + data.get("enable_mx_checks") + and data.get("domains_must_have_authorized_mx") + and not data.get("valid_mxs") + ) if condition: - raise serializers.ValidationError({ - "valid_mxs": _("Define at least one authorized network / address") - }) + raise serializers.ValidationError( + {"valid_mxs": _("Define at least one authorized network / address")} + ) if not data.get("enable_mx_checks"): data["enable_ipv6_mx_checks"] = False return data @@ -335,7 +337,8 @@ class IdentitySerializer(serializers.Serializer): # (not used for now) return [] actions = admin_signals.extra_account_identities_actions.send( - self.__class__, account=identity) + self.__class__, account=identity + ) cleaned_actions = [] for action in actions: try: @@ -358,15 +361,11 @@ class AccountSerializer(v1_serializers.AccountSerializer): """Add support for user resources.""" aliases = serializers.ListField( - child=lib_fields.DRFEmailFieldUTF8(), - source="mailbox.alias_addresses" + child=lib_fields.DRFEmailFieldUTF8(), source="mailbox.alias_addresses" ) class Meta(v1_serializers.AccountSerializer.Meta): - fields = ( - v1_serializers.AccountSerializer.Meta.fields - + ("aliases", ) - ) + fields = v1_serializers.AccountSerializer.Meta.fields + ("aliases",) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -375,7 +374,7 @@ class AccountSerializer(v1_serializers.AccountSerializer): self.fields["resources"] = serializers.SerializerMethodField() def get_resources(self, account) -> List[AccountResourceSerializer]: - if account.role == 'SimpleUsers': + if account.role == "SimpleUsers": return [] resources = [] for limit in account.userobjectlimit_set.all(): @@ -395,7 +394,10 @@ class MailboxSerializer(serializers.ModelSerializer): class Meta: model = models.Mailbox fields = ( - "pk", "use_domain_quota", "quota", "message_limit", + "pk", + "use_domain_quota", + "quota", + "message_limit", "is_send_only", ) @@ -408,9 +410,9 @@ class MailboxSerializer(serializers.ModelSerializer): method = self.context["request"].method if not data.get("use_domain_quota", False): if "quota" not in data and method != "PATCH": - raise serializers.ValidationError({ - "quota": _("This field is required") - }) + raise serializers.ValidationError( + {"quota": _("This field is required")} + ) return data @@ -426,8 +428,7 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer): """Add support for aliases and sender addresses.""" aliases = serializers.ListField( - child=lib_fields.DRFEmailFieldUTF8(), - required=False + child=lib_fields.DRFEmailFieldUTF8(), required=False ) mailbox = MailboxSerializer(required=False) @@ -449,7 +450,8 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer): aliases = [] for alias in value: localpart, domain = models.validate_alias_address( - alias, self.context["request"].user, ignore_existing=True) + alias, self.context["request"].user, ignore_existing=True + ) aliases.append({"localpart": localpart, "domain": domain}) return aliases @@ -458,9 +460,9 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer): master_user = data.get("master_user", False) role = data.get("role") if master_user and role != "SuperAdmins": - raise serializers.ValidationError({ - "master_user": _("Not allowed for this role.") - }) + raise serializers.ValidationError( + {"master_user": _("Not allowed for this role.")} + ) if role == "SimpleUsers": username = data.get("username") if username: @@ -471,46 +473,43 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer): mailbox = data.get("mailbox") if mailbox is None: if not self.instance: - data["mailbox"] = { - "use_domain_quota": True - } + data["mailbox"] = {"use_domain_quota": True} if "mailbox" in data and "username" in data: self.address, domain_name = email_utils.split_mailbox(data["username"]) - self.domain = get_object_or_404( - models.Domain, name=domain_name) + self.domain = get_object_or_404(models.Domain, name=domain_name) creator = self.context["request"].user if not creator.can_access(self.domain): raise serializers.ValidationError({"mailbox": _("Permission denied.")}) if not self.instance: try: core_signals.can_create_object.send( - sender=self.__class__, context=creator, - klass=models.Mailbox) + sender=self.__class__, context=creator, klass=models.Mailbox + ) core_signals.can_create_object.send( - sender=self.__class__, context=self.domain, - object_type="mailboxes") + sender=self.__class__, + context=self.domain, + object_type="mailboxes", + ) except lib_exceptions.ModoboaException as inst: - raise serializers.ValidationError({ - "mailbox": str(inst) - }) + raise serializers.ValidationError({"mailbox": str(inst)}) if data.get("password") or not self.partial: password = data.get("password") if password: try: password_validation.validate_password( - data["password"], self.instance) + data["password"], self.instance + ) except ValidationError as exc: - raise serializers.ValidationError({ - "password": exc.messages[0]}) + raise serializers.ValidationError({"password": exc.messages[0]}) elif not self.instance: - raise serializers.ValidationError({ - "password": _("This field is required.") - }) + raise serializers.ValidationError( + {"password": _("This field is required.")} + ) aliases = data.get("aliases") if aliases and "mailbox" not in data: - raise serializers.ValidationError({ - "aliases": _("A mailbox is required to create aliases.") - }) + raise serializers.ValidationError( + {"aliases": _("A mailbox is required to create aliases.")} + ) domain_names = data.get("domains") if not domain_names: return data @@ -520,9 +519,9 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer): if domain: domains.append(domain) continue - raise serializers.ValidationError({ - "domains": _("Local domain {} does not exist").format(name) - }) + raise serializers.ValidationError( + {"domains": _("Local domain {} does not exist").format(name)} + ) data["domains"] = domains return data @@ -550,8 +549,8 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer): address="{}@{}".format(alias["localpart"], alias["domain"]), recipients=[user.username], creator=creator, - domain=alias["domain"] - ) + domain=alias["domain"], + ) return user def update(self, instance, validated_data): @@ -584,7 +583,7 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer): address=address, recipients=[validated_data["username"]], creator=creator, - domain=alias["domain"] + domain=alias["domain"], ) try: alias_recipients.remove(address) @@ -592,18 +591,17 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer): continue for alias_address in alias_recipients: alias = models.Alias.objects.filter( - address=alias_address, internal=False) + address=alias_address, internal=False + ) if alias.exists(): - alias.first().remove_recipient_or_delete( - validated_data["username"] - ) + alias.first().remove_recipient_or_delete(validated_data["username"]) instance.save() resources = validated_data.get("resources") if resources: for resource in resources: - instance.userobjectlimit_set.filter( - name=resource["name"] - ).update(max_value=resource["max_value"]) + instance.userobjectlimit_set.filter(name=resource["name"]).update( + max_value=resource["max_value"] + ) self.set_permissions(instance, domains) return instance diff --git a/modoboa/admin/api/v2/tests.py b/modoboa/admin/api/v2/tests.py index 472c87e33..fa95f5f0d 100644 --- a/modoboa/admin/api/v2/tests.py +++ b/modoboa/admin/api/v2/tests.py @@ -19,7 +19,8 @@ class DomainViewSetTestCase(ModoAPITestCase): super().setUpTestData() factories.populate_database() cls.da_token = Token.objects.create( - user=core_models.User.objects.get(username="admin@test.com")) + user=core_models.User.objects.get(username="admin@test.com") + ) def test_create(self): url = reverse("v2:domain-list") @@ -28,8 +29,8 @@ class DomainViewSetTestCase(ModoAPITestCase): "domain_admin": { "username": "admin", "with_mailbox": True, - "with_aliases": True - } + "with_aliases": True, + }, } resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 201) @@ -39,8 +40,7 @@ class DomainViewSetTestCase(ModoAPITestCase): admin = dom.admins.first() self.assertTrue(hasattr(admin, "mailbox")) self.assertTrue( - models.Alias.objects.filter( - address="postmaster@domain.tld").exists() + models.Alias.objects.filter(address="postmaster@domain.tld").exists() ) def test_update(self): @@ -50,11 +50,8 @@ class DomainViewSetTestCase(ModoAPITestCase): "type": "relaydomain", "transport": { "service": "relay", - "settings": { - "relay_target_port": 25, - "relay_target_host": "localhost" - } - } + "settings": {"relay_target_port": 25, "relay_target_host": "localhost"}, + }, } url = reverse("v2:domain-detail", args=[domain.pk]) resp = self.client.put(url, data, format="json") @@ -63,7 +60,7 @@ class DomainViewSetTestCase(ModoAPITestCase): self.assertEqual(domain.transport.service, data["transport"]["service"]) self.assertEqual( domain.transport._settings["relay_target_host"], - data["transport"]["settings"]["relay_target_host"] + data["transport"]["settings"]["relay_target_host"], ) data["transport"]["relay_verify_recipients"] = True @@ -75,9 +72,7 @@ class DomainViewSetTestCase(ModoAPITestCase): domain = models.Domain.objects.get(name="test2.com") data = { "name": "test2.com", - "resources": [ - {"name": "domain_aliases", "max_value": 20} - ] + "resources": [{"name": "domain_aliases", "max_value": 20}], } url = reverse("v2:domain-detail", args=[domain.pk]) resp = self.client.put(url, data, format="json") @@ -87,8 +82,7 @@ class DomainViewSetTestCase(ModoAPITestCase): ) def test_delete(self): - self.client.credentials( - HTTP_AUTHORIZATION="Token " + self.da_token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.da_token.key) domain = models.Domain.objects.get(name="test2.com") url = reverse("v2:domain-delete", args=[domain.pk]) @@ -129,15 +123,14 @@ class DomainViewSetTestCase(ModoAPITestCase): self.assertEqual(resp.status_code, 200) def test_domains_import(self): - f = ContentFile(b"""domain; domain1.com; 1000; 100; True + f = ContentFile( + b"""domain; domain1.com; 1000; 100; True domain; domain2.com; 1000; 200; False domainalias; domalias1.com; domain1.com; True -""", name="domains.csv") - self.client.post( - reverse("v2:domain-import-from-csv"), { - "sourcefile": f - } +""", + name="domains.csv", ) + self.client.post(reverse("v2:domain-import-from-csv"), {"sourcefile": f}) admin = core_models.User.objects.get(username="admin") dom = models.Domain.objects.get(name="domain1.com") self.assertEqual(dom.quota, 1000) @@ -164,8 +157,7 @@ domainalias; domalias1.com; domain1.com; True "domain,test2.com,0,0,True", ] self.assertCountEqual( - expected_response, - force_str(response.content.strip()).split("\r\n") + expected_response, force_str(response.content.strip()).split("\r\n") ) @@ -182,19 +174,14 @@ class AccountViewSetTestCase(ModoAPITestCase): data = { "username": "toto@test.com", "role": "SimpleUsers", - "mailbox": { - "use_domain_quota": True - }, + "mailbox": {"use_domain_quota": True}, "password": "Toto12345", "language": "fr", - "aliases": ["alias3@test.com"] + "aliases": ["alias3@test.com"], } resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 201) - self.assertTrue( - models.Alias.objects.filter( - address="alias3@test.com").exists() - ) + self.assertTrue(models.Alias.objects.filter(address="alias3@test.com").exists()) def test_create_admin(self): url = reverse("v2:account-list") @@ -203,7 +190,7 @@ class AccountViewSetTestCase(ModoAPITestCase): "role": "SuperAdmins", "password": "Toto12345", "language": "fr", - "aliases": ["alias3@test.com"] + "aliases": ["alias3@test.com"], } resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 400) @@ -251,12 +238,8 @@ class AccountViewSetTestCase(ModoAPITestCase): "username": "user@test.com", "role": "SimpleUsers", "password": "Toto12345", - "mailbox": { - "quota": 10 - }, - "aliases": [ - "aliasupdate1@test.com" - ] + "mailbox": {"quota": 10}, + "aliases": ["aliasupdate1@test.com"], } resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) @@ -271,7 +254,7 @@ class AccountViewSetTestCase(ModoAPITestCase): "mailbox": { "message_limit": 10, "is_send_only": True, - } + }, } resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) @@ -283,9 +266,7 @@ class AccountViewSetTestCase(ModoAPITestCase): "username": "user@test.com", "role": "SimpleUsers", "password": "Toto12345", - "mailbox": { - "quota": 10 - } + "mailbox": {"quota": 10}, } resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) @@ -299,7 +280,7 @@ class AccountViewSetTestCase(ModoAPITestCase): "mailbox": { "message_limit": None, "is_send_only": False, - } + }, } resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) @@ -314,29 +295,24 @@ class AccountViewSetTestCase(ModoAPITestCase): "username": "user@test.com", "role": "SimpleUsers", "password": "Toto12345", - "mailbox": { - "quota": 10 - }, - "aliases": [ - "aliasupdate1@test.com" - ] + "mailbox": {"quota": 10}, + "aliases": ["aliasupdate1@test.com"], } resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) self.assertTrue( - models.Alias.objects.filter(address="aliasupdate1@test.com").exists()) + models.Alias.objects.filter(address="aliasupdate1@test.com").exists() + ) # Create an alias for another user url = reverse("v2:account-list") data = { "username": "toto@test.com", "role": "SimpleUsers", - "mailbox": { - "use_domain_quota": True - }, + "mailbox": {"use_domain_quota": True}, "password": "Toto12345", "language": "fr", - "aliases": ["totoalias@test.com"] + "aliases": ["totoalias@test.com"], } resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 201) @@ -347,13 +323,11 @@ class AccountViewSetTestCase(ModoAPITestCase): "username": "user@test.com", "role": "SimpleUsers", "password": "Toto12345", - "mailbox": { - "quota": 10 - }, + "mailbox": {"quota": 10}, "aliases": [ "totoalias@test.com", "aliasupdate1@test.com", - ] + ], } resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) @@ -367,17 +341,16 @@ class AccountViewSetTestCase(ModoAPITestCase): data = { "username": "user@test.com", "role": "SimpleUsers", - "mailbox": { - "quota": 10 - }, - "aliases": [] + "mailbox": {"quota": 10}, + "aliases": [], } resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) self.assertFalse( - models.Alias.objects.filter(address="aliasupdate1@test.com").exists()) - alias_recipients = list(models.Alias.objects.filter( - address="totoalias@test.com").first().recipients + models.Alias.objects.filter(address="aliasupdate1@test.com").exists() + ) + alias_recipients = list( + models.Alias.objects.filter(address="totoalias@test.com").first().recipients ) self.assertEqual(alias_recipients, ["toto@test.com"]) @@ -388,9 +361,7 @@ class AccountViewSetTestCase(ModoAPITestCase): "username": "superadmin@test.com", "role": "SuperAdmins", "password": "Toto12345", - "mailbox": { - "quota": 10 - } + "mailbox": {"quota": 10}, } resp = self.client.patch(url, data, format="json") self.assertEqual(resp.status_code, 200) @@ -405,7 +376,7 @@ class AccountViewSetTestCase(ModoAPITestCase): data = { "resources": [ {"name": "mailboxes", "max_value": 10}, - {"name": "mailbox_aliases", "max_value": 10} + {"name": "mailbox_aliases", "max_value": 10}, ] } resp = self.client.patch(url, data, format="json") @@ -436,17 +407,20 @@ class IdentityViewSetTestCase(ModoAPITestCase): self.assertEqual(len(resp.json()), 8) def test_import(self): - f = ContentFile(""" + f = ContentFile( + """ account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 0 account; Truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 5; test.com alias; alias1@test.com; True; user1@test.com forward; alias2@test.com; True; user1+ext@test.com forward; fwd1@test.com; True; user@extdomain.com dlist; dlist@test.com; True; user1@test.com; user@extdomain.com -""", name="identities.csv") # NOQA:E501 +""", + name="identities.csv", + ) # NOQA:E501 self.client.post( reverse("v2:identities-import-from-csv"), - {"sourcefile": f, "crypt_password": True} + {"sourcefile": f, "crypt_password": True}, ) admin = core_models.User.objects.get(username="admin") u1 = core_models.User.objects.get(username="user1@test.com") @@ -461,13 +435,11 @@ dlist; dlist@test.com; True; user1@test.com; user@extdomain.com self.assertEqual(mb1.quota, 0) self.assertTrue(admin.is_owner(mb1)) self.assertEqual(mb1.full_address, "user1@test.com") - self.assertTrue( - self.client.login(username="user1@test.com", password="toto") - ) + self.assertTrue(self.client.login(username="user1@test.com", password="toto")) da = core_models.User.objects.get(username="truc@test.com") damb = da.mailbox - self.assertEqual(da.first_name, u"René") + self.assertEqual(da.first_name, "René") self.assertEqual(da.role, "DomainAdmins") self.assertEqual(damb.quota, 5) self.assertFalse(damb.use_domain_quota) @@ -478,30 +450,23 @@ dlist; dlist@test.com; True; user1@test.com; user@extdomain.com self.assertTrue(da.can_access(u)) al = models.Alias.objects.get(address="alias1@test.com") - self.assertTrue( - al.aliasrecipient_set - .filter(r_mailbox=u1.mailbox).exists() - ) + self.assertTrue(al.aliasrecipient_set.filter(r_mailbox=u1.mailbox).exists()) self.assertTrue(admin.is_owner(al)) fwd = models.Alias.objects.get(address="fwd1@test.com") self.assertTrue( - fwd.aliasrecipient_set - .filter( - address="user@extdomain.com", r_mailbox__isnull=True, - r_alias__isnull=True) - .exists() + fwd.aliasrecipient_set.filter( + address="user@extdomain.com", + r_mailbox__isnull=True, + r_alias__isnull=True, + ).exists() ) self.assertTrue(admin.is_owner(fwd)) dlist = models.Alias.objects.get(address="dlist@test.com") + self.assertTrue(dlist.aliasrecipient_set.filter(r_mailbox=u1.mailbox).exists()) self.assertTrue( - dlist.aliasrecipient_set - .filter(r_mailbox=u1.mailbox).exists() - ) - self.assertTrue( - dlist.aliasrecipient_set.filter(address="user@extdomain.com") - .exists() + dlist.aliasrecipient_set.filter(address="user@extdomain.com").exists() ) self.assertTrue(admin.is_owner(dlist)) @@ -513,10 +478,7 @@ dlist; dlist@test.com; True; user1@test.com; user@extdomain.com admin_row = received_content[0].split(",") admin_row[2] = "" received_content[0] = ",".join(admin_row) - self.assertCountEqual( - expected_response.strip().split("\r\n"), - received_content - ) + self.assertCountEqual(expected_response.strip().split("\r\n"), received_content) class AliasViewSetTestCase(ModoAPITestCase): @@ -536,10 +498,7 @@ class AliasViewSetTestCase(ModoAPITestCase): data = {"address": "alias@test.com"} resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 400) - self.assertEqual( - resp.json()["address"][0], - "This alias already exists" - ) + self.assertEqual(resp.json()["address"][0], "This alias already exists") data = {"address": "alias2@test.com"} resp = self.client.post(url, data, format="json") @@ -563,8 +522,7 @@ class UserAccountViewSetTestCase(ModoAPITestCase): cls.da_token = Token.objects.create(user=cls.da) def test_forward(self): - self.client.credentials( - HTTP_AUTHORIZATION="Token " + self.da_token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.da_token.key) url = reverse("v2:account-forward") resp = self.client.get(url) self.assertEqual(resp.status_code, 200) @@ -580,8 +538,7 @@ class UserAccountViewSetTestCase(ModoAPITestCase): self.assertEqual(resp.status_code, 200) self.assertEqual( - models.Alias.objects.filter(address=self.da.username).count(), - 2 + models.Alias.objects.filter(address=self.da.username).count(), 2 ) resp = self.client.get(url) @@ -592,8 +549,7 @@ class UserAccountViewSetTestCase(ModoAPITestCase): resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 200) self.assertEqual( - models.Alias.objects.filter(address=self.da.username).count(), - 1 + models.Alias.objects.filter(address=self.da.username).count(), 1 ) @@ -605,9 +561,11 @@ class AlarmViewSetTestCase(ModoAPITestCase): super().setUpTestData() factories.populate_database() factories.AlarmFactory( - domain__name="test.com", mailbox=None, title="Test alarm") + domain__name="test.com", mailbox=None, title="Test alarm" + ) cls.da_token = Token.objects.create( - user=core_models.User.objects.get(username="admin@test.com")) + user=core_models.User.objects.get(username="admin@test.com") + ) def test_list(self): url = reverse("v2:alarm-list") @@ -621,11 +579,11 @@ class AlarmViewSetTestCase(ModoAPITestCase): domain = models.Domain.objects.get(name="test.com") # Try performing action on restricted domains - self.client.credentials( - HTTP_AUTHORIZATION="Token " + self.da_token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.da_token.key) domain = models.Domain.objects.get(name="test2.com") alarm_restricted = models.Alarm.objects.create( - domain=domain, mailbox=None, title="Test alarm 2") + domain=domain, mailbox=None, title="Test alarm 2" + ) alarm_restricted.save() url = reverse("v2:alarm-switch", args=[alarm_restricted.pk]) resp = self.client.post(url) @@ -638,7 +596,8 @@ class AlarmViewSetTestCase(ModoAPITestCase): self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) alarm = models.Alarm.objects.create( - domain=domain, mailbox=None, title="Test alarm 3") + domain=domain, mailbox=None, title="Test alarm 3" + ) alarm.save() # Switch status of the alarm to close @@ -678,9 +637,11 @@ class AlarmViewSetTestCase(ModoAPITestCase): resp = self.client.delete(f"{url}?ids[]=toto") self.assertEqual(resp.status_code, 400) alarm1 = factories.AlarmFactory( - domain__name="test.com", mailbox=None, title="Test alarm") + domain__name="test.com", mailbox=None, title="Test alarm" + ) alarm2 = factories.AlarmFactory( - domain__name="test.com", mailbox=None, title="Test alarm") + domain__name="test.com", mailbox=None, title="Test alarm" + ) resp = self.client.delete(f"{url}?ids[]={alarm1.pk}&ids[]={alarm2.pk}") self.assertEqual(resp.status_code, 204) with self.assertRaises(models.Alarm.DoesNotExist): diff --git a/modoboa/admin/api/v2/urls.py b/modoboa/admin/api/v2/urls.py index aa0e04b9d..e85b08726 100644 --- a/modoboa/admin/api/v2/urls.py +++ b/modoboa/admin/api/v2/urls.py @@ -9,17 +9,15 @@ from . import viewsets router = routers.SimpleRouter() router.register(r"domains", viewsets.DomainViewSet, basename="domain") router.register( - r"domainaliases", v1_viewsets.DomainAliasViewSet, basename="domain_alias") + r"domainaliases", v1_viewsets.DomainAliasViewSet, basename="domain_alias" +) router.register(r"accounts", viewsets.AccountViewSet, basename="account") router.register(r"aliases", viewsets.AliasViewSet, basename="alias") router.register( - r"senderaddresses", v1_viewsets.SenderAddressViewSet, - basename="sender_address") -router.register( - r"identities", viewsets.IdentityViewSet, basename="identities") -router.register( - r"account", viewsets.UserAccountViewSet, basename="account") -router.register( - r"alarms", viewsets.AlarmViewSet, basename="alarm") + r"senderaddresses", v1_viewsets.SenderAddressViewSet, basename="sender_address" +) +router.register(r"identities", viewsets.IdentityViewSet, basename="identities") +router.register(r"account", viewsets.UserAccountViewSet, basename="account") +router.register(r"alarms", viewsets.AlarmViewSet, basename="alarm") urlpatterns = router.urls diff --git a/modoboa/admin/api/v2/viewsets.py b/modoboa/admin/api/v2/viewsets.py index 44cb89bb9..3555f9f02 100644 --- a/modoboa/admin/api/v2/viewsets.py +++ b/modoboa/admin/api/v2/viewsets.py @@ -7,7 +7,13 @@ from django.contrib.contenttypes.models import ContentType from django_filters import rest_framework as dj_filters from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import ( - filters, mixins, parsers, permissions, response, status, viewsets + filters, + mixins, + parsers, + permissions, + response, + status, + viewsets, ) from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied @@ -29,31 +35,32 @@ from . import serializers @extend_schema_view( retrieve=extend_schema( description="Retrieve a particular domain", - summary="Retrieve a particular domain" + summary="Retrieve a particular domain", ), list=extend_schema( - description="Retrieve a list of domains", - summary="Retrieve a list of domains" + description="Retrieve a list of domains", summary="Retrieve a list of domains" ), create=extend_schema( - description="Create a new domain", - summary="Create a new domain" + description="Create a new domain", summary="Create a new domain" ), delete=extend_schema( - description="Delete a particular domain", - summary="Delete a particular domain" + description="Delete a particular domain", summary="Delete a particular domain" ), ) -class DomainViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - mixins.CreateModelMixin, - mixins.UpdateModelMixin, - viewsets.GenericViewSet): +class DomainViewSet( + GetThrottleViewsetMixin, + lib_viewsets.RevisionModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.CreateModelMixin, + mixins.UpdateModelMixin, + viewsets.GenericViewSet, +): """V2 viewset.""" permission_classes = ( - permissions.IsAuthenticated, permissions.DjangoModelPermissions, + permissions.IsAuthenticated, + permissions.DjangoModelPermissions, ) def get_queryset(self): @@ -112,8 +119,9 @@ class DomainViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, domain.remove_admin(serializer.validated_data["account"]) return response.Response() - @action(methods=["get"], detail=False, - renderer_classes=(lib_renderers.CSVRenderer,)) + @action( + methods=["get"], detail=False, renderer_classes=(lib_renderers.CSVRenderer,) + ) def export(self, request, **kwargs): """Export domains and aliases to CSV.""" result = [] @@ -121,21 +129,19 @@ class DomainViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, result += domain.to_csv_rows() return response.Response(result) - @extend_schema( - request=serializers.CSVImportSerializer + @extend_schema(request=serializers.CSVImportSerializer) + @action( + methods=["post"], + detail=False, + parser_classes=(parsers.MultiPartParser, parsers.FormParser), + url_path="import", ) - @action(methods=["post"], - detail=False, - parser_classes=(parsers.MultiPartParser, parsers.FormParser), - url_path="import") def import_from_csv(self, request, **kwargs): """Import domains and aliases from CSV file.""" serializer = serializers.CSVImportSerializer(data=request.data) serializer.is_valid(raise_exception=True) status, msg = lib.import_data( - request.user, - request.FILES["sourcefile"], - serializer.validated_data + request.user, request.FILES["sourcefile"], serializer.validated_data ) return response.Response({"status": status, "message": msg}) @@ -144,9 +150,8 @@ class AccountFilterSet(dj_filters.FilterSet): """Custom FilterSet for Account.""" domain = dj_filters.ModelChoiceFilter( - queryset=lambda request: models.Domain.objects.get_for_admin( - request.user), - field_name="mailbox__domain" + queryset=lambda request: models.Domain.objects.get_for_admin(request.user), + field_name="mailbox__domain", ) role = dj_filters.CharFilter(method="filter_role") @@ -176,23 +181,19 @@ class AccountViewSet(v1_viewsets.AccountViewSet): def get_queryset(self): """Filter queryset based on current user.""" user = self.request.user - ids = ( - user.objectaccess_set - .filter(content_type=ContentType.objects.get_for_model(user)) - .values_list("object_id", flat=True) - ) - return ( - core_models.User.objects.filter(pk__in=ids) - .prefetch_related("userobjectlimit_set") + ids = user.objectaccess_set.filter( + content_type=ContentType.objects.get_for_model(user) + ).values_list("object_id", flat=True) + return core_models.User.objects.filter(pk__in=ids).prefetch_related( + "userobjectlimit_set" ) @action(methods=["post"], detail=False) def validate(self, request, **kwargs): """Validate given account without creating it.""" serializer = self.get_serializer( - data=request.data, - context=self.get_serializer_context(), - partial=True) + data=request.data, context=self.get_serializer_context(), partial=True + ) serializer.is_valid(raise_exception=True) return response.Response(status=204) @@ -215,17 +216,19 @@ class AccountViewSet(v1_viewsets.AccountViewSet): class IdentityViewSet(GetThrottleViewsetMixin, viewsets.ViewSet): """Viewset for identities.""" - permission_classes = (permissions.IsAuthenticated, ) + permission_classes = (permissions.IsAuthenticated,) serializer_class = None def list(self, request, **kwargs): """Return all identities.""" serializer = serializers.IdentitySerializer( - lib.get_identities(request.user), many=True) + lib.get_identities(request.user), many=True + ) return response.Response(serializer.data) - @action(methods=["get"], detail=False, - renderer_classes=(lib_renderers.CSVRenderer,)) + @action( + methods=["get"], detail=False, renderer_classes=(lib_renderers.CSVRenderer,) + ) def export(self, request, **kwargs): """Export accounts and aliases to CSV.""" result = [] @@ -233,21 +236,19 @@ class IdentityViewSet(GetThrottleViewsetMixin, viewsets.ViewSet): result.append(idt.to_csv_row()) return response.Response(result) - @extend_schema( - request=serializers.CSVIdentityImportSerializer + @extend_schema(request=serializers.CSVIdentityImportSerializer) + @action( + methods=["post"], + detail=False, + parser_classes=(parsers.MultiPartParser, parsers.FormParser), + url_path="import", ) - @action(methods=["post"], - detail=False, - parser_classes=(parsers.MultiPartParser, parsers.FormParser), - url_path="import") def import_from_csv(self, request, **kwargs): """Import accounts and aliases from CSV file.""" serializer = serializers.CSVIdentityImportSerializer(data=request.data) serializer.is_valid(raise_exception=True) status, msg = lib.import_data( - request.user, - request.FILES["sourcefile"], - serializer.validated_data + request.user, request.FILES["sourcefile"], serializer.validated_data ) return response.Response({"status": status, "message": msg}) @@ -261,23 +262,20 @@ class AliasViewSet(v1_viewsets.AliasViewSet): def validate(self, request, **kwargs): """Validate given alias without creating it.""" serializer = self.get_serializer( - data=request.data, - context=self.get_serializer_context(), - partial=True) + data=request.data, context=self.get_serializer_context(), partial=True + ) try: serializer.is_valid(raise_exception=True) except AliasExists as e: return response.Response( data={"id": e.alias_id, "status": _("This alias already exists")}, - status=409 - ) + status=409, + ) return response.Response(status=204) @action(methods=["get"], detail=False) def random_address(self, request, **kwargs): - return response.Response({ - "address": models.Alias.generate_random_address() - }) + return response.Response({"address": models.Alias.generate_random_address()}) class UserAccountViewSet(GetThrottleViewsetMixin, viewsets.ViewSet): @@ -288,7 +286,8 @@ class UserAccountViewSet(GetThrottleViewsetMixin, viewsets.ViewSet): """Get or define user forward.""" mb = request.user.mailbox alias = models.Alias.objects.filter( - address=mb.full_address, internal=False).first() + address=mb.full_address, internal=False + ).first() data = {} if request.method == "GET": if alias is not None and alias.recipients: @@ -304,17 +303,16 @@ class UserAccountViewSet(GetThrottleViewsetMixin, viewsets.ViewSet): recipients = serializer.validated_data.get("recipients") if not recipients: models.Alias.objects.filter( - address=mb.full_address, internal=False).delete() + address=mb.full_address, internal=False + ).delete() # Make sure internal self-alias is enabled - models.Alias.objects.filter( - address=mb.full_address, internal=True - ).update(enabled=True) + models.Alias.objects.filter(address=mb.full_address, internal=True).update( + enabled=True + ) else: if alias is None: alias = models.Alias.objects.create( - address=mb.full_address, - domain=mb.domain, - enabled=mb.user.is_active + address=mb.full_address, domain=mb.domain, enabled=mb.user.is_active ) alias.post_create(request.user) if serializer.validated_data["keepcopies"]: @@ -346,32 +344,35 @@ class AlarmFilterSet(dj_filters.FilterSet): return queryset.filter(created__gt=value) -class AlarmViewSet(GetThrottleViewsetMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - viewsets.GenericViewSet): +class AlarmViewSet( + GetThrottleViewsetMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet, +): """Viewset for Alarm.""" - filter_backends = (filters.OrderingFilter, - filters.SearchFilter, - dj_filters.DjangoFilterBackend) + filter_backends = ( + filters.OrderingFilter, + filters.SearchFilter, + dj_filters.DjangoFilterBackend, + ) filterset_class = AlarmFilterSet ordering_fields = ["created", "status", "title"] pagination_class = pagination.CustomPageNumberPagination - permission_classes = ( - permissions.IsAuthenticated, - ) + permission_classes = (permissions.IsAuthenticated,) search_fields = ["domain__name", "title"] serializer_class = serializers.AlarmSerializer def get_queryset(self): - return models.Alarm.objects.select_related("domain").filter( - domain__in=models.Domain.objects.get_for_admin( - self.request.user) - ).order_by("-created") + return ( + models.Alarm.objects.select_related("domain") + .filter(domain__in=models.Domain.objects.get_for_admin(self.request.user)) + .order_by("-created") + ) - @action(methods=['patch'], detail=True) + @action(methods=["patch"], detail=True) def switch(self, request, **kwargs): """Custom update method that switch status of an alarm.""" alarm = self.get_object() @@ -383,7 +384,7 @@ class AlarmViewSet(GetThrottleViewsetMixin, alarm.reopen() return response.Response(status=status.HTTP_204_NO_CONTENT) - @action(methods=['delete'], detail=False) + @action(methods=["delete"], detail=False) def bulk_delete(self, request, **kwargs): """Delete multiple alarms at the same time.""" ids = request.query_params.getlist("ids[]") @@ -392,7 +393,6 @@ class AlarmViewSet(GetThrottleViewsetMixin, try: ids = [int(alarm_id) for alarm_id in ids] except ValueError: - return response.Response( - _("Received invalid alarm id(s)"), status=400) + return response.Response(_("Received invalid alarm id(s)"), status=400) models.Alarm.objects.filter(pk__in=ids).delete() return response.Response(status=204) diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index eb97ec8e4..c75292c96 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -25,9 +25,7 @@ class AdminParametersForm(param_forms.AdminParametersForm): enable_mx_checks = YesNoField( label=gettext_lazy("Enable MX checks"), initial=True, - help_text=gettext_lazy( - "Check that every domain has a valid MX record" - ) + help_text=gettext_lazy("Check that every domain has a valid MX record"), ) valid_mxs = forms.CharField( @@ -38,7 +36,7 @@ class AdminParametersForm(param_forms.AdminParametersForm): " A warning will be sent if a record does not respect it." ), widget=forms.Textarea, - required=False + required=False, ) domains_must_have_authorized_mx = YesNoField( @@ -47,40 +45,33 @@ class AdminParametersForm(param_forms.AdminParametersForm): help_text=gettext_lazy( "Prevent the creation of a new domain if its MX record does " "not use one of the defined addresses." - ) + ), ) enable_ipv6_mx_checks = YesNoField( label=gettext_lazy("Enable IPV6 checks"), initial=True, - help_text=gettext_lazy( - "Check if MX setup for ipv6 is valid" - ) + help_text=gettext_lazy("Check if MX setup for ipv6 is valid"), ) enable_spf_checks = YesNoField( label=gettext_lazy("Enable SPF checks"), initial=True, - help_text=gettext_lazy( - "Check if every domain has a valid SPF record" - ) + help_text=gettext_lazy("Check if every domain has a valid SPF record"), ) enable_dkim_checks = YesNoField( label=gettext_lazy("Enable DKIM checks"), initial=True, help_text=gettext_lazy( - "Check if every domain with DKIM signin enabled has a valid DNS " - "record" - ) + "Check if every domain with DKIM signin enabled has a valid DNS " "record" + ), ) enable_dmarc_checks = YesNoField( label=gettext_lazy("Enable DMARC checks"), initial=True, - help_text=gettext_lazy( - "Check if every domain has a valid DMARC record" - ) + help_text=gettext_lazy("Check if every domain has a valid DMARC record"), ) enable_autoconfig_checks = YesNoField( @@ -88,15 +79,13 @@ class AdminParametersForm(param_forms.AdminParametersForm): initial=True, help_text=gettext_lazy( "Check if every domain has a valid records for autoconfiguration" - ) + ), ) enable_dnsbl_checks = YesNoField( label=gettext_lazy("Enable DNSBL checks"), initial=True, - help_text=gettext_lazy( - "Check every domain against major DNSBL providers" - ) + help_text=gettext_lazy("Check every domain against major DNSBL providers"), ) custom_dns_server = GenericIPAddressField( @@ -105,7 +94,7 @@ class AdminParametersForm(param_forms.AdminParametersForm): initial="", help_text=gettext_lazy( "Use a custom DNS server instead of local server configuration" - ) + ), ) dkim_keys_storage_dir = forms.CharField( @@ -116,16 +105,14 @@ class AdminParametersForm(param_forms.AdminParametersForm): "be stored. Make sure this directory belongs to root user " "and is not readable by the outside world." ), - required=False + required=False, ) dkim_default_key_length = forms.ChoiceField( label=gettext_lazy("Default DKIM key length"), initial=2048, choices=constants.DKIM_KEY_LENGTHS, - help_text=gettext_lazy( - "Default length in bits for newly generated DKIM keys." - ) + help_text=gettext_lazy("Default length in bits for newly generated DKIM keys."), ) default_domain_quota = forms.IntegerField( @@ -134,7 +121,7 @@ class AdminParametersForm(param_forms.AdminParametersForm): help_text=gettext_lazy( "Default quota (in MB) applied to freshly created domains with no " "value specified. A value of 0 means no quota." - ) + ), ) default_domain_message_limit = forms.IntegerField( @@ -143,7 +130,7 @@ class AdminParametersForm(param_forms.AdminParametersForm): help_text=gettext_lazy( "Number of messages freshly created domains will be " "allowed to send per day. Leave empty for no limit." - ) + ), ) mbsep = SeparatorField(label=gettext_lazy("Mailboxes")) @@ -154,7 +141,7 @@ class AdminParametersForm(param_forms.AdminParametersForm): help_text=gettext_lazy( "Rename or remove mailboxes on the filesystem when they get" " renamed or removed within Modoboa" - ) + ), ) default_mailbox_quota = forms.IntegerField( @@ -163,7 +150,7 @@ class AdminParametersForm(param_forms.AdminParametersForm): help_text=gettext_lazy( "Default mailbox quota (in MB) applied to freshly created " "mailboxes with no value specified. A value of 0 means no quota." - ) + ), ) default_mailbox_message_limit = forms.IntegerField( @@ -172,23 +159,23 @@ class AdminParametersForm(param_forms.AdminParametersForm): help_text=gettext_lazy( "Number of messages freshly created mailboxes will be " "allowed to send per day. Leave empty for no limit." - ) + ), ) auto_account_removal = YesNoField( label=gettext_lazy("Automatic account removal"), initial=False, help_text=gettext_lazy( - "When a mailbox is removed, also remove the associated account") + "When a mailbox is removed, also remove the associated account" + ), ) auto_create_domain_and_mailbox = YesNoField( label=gettext_lazy("Automatic domain/mailbox creation"), initial=True, help_text=gettext_lazy( - "Create a domain and a mailbox when an account is automatically " - "created." - ) + "Create a domain and a mailbox when an account is automatically " "created." + ), ) create_alias_on_mbox_rename = YesNoField( @@ -196,22 +183,19 @@ class AdminParametersForm(param_forms.AdminParametersForm): initial=False, help_text=gettext_lazy( "Create an alias using the old address when a mailbox is renamed." - ) + ), ) # Visibility rules visibility_rules = { "valid_mxs": "enable_mx_checks=True", "domains_must_have_authorized_mx": "enable_mx_checks=True", - "enable_ipv6_mx_checks": "enable_mx_checks=True" + "enable_ipv6_mx_checks": "enable_mx_checks=True", } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.field_widths = { - "default_domain_quota": 2, - "default_mailbox_quota": 2 - } + self.field_widths = {"default_domain_quota": 2, "default_mailbox_quota": 2} hide_fields = False dpath = None code, output = exec_cmd("which dovecot") @@ -219,8 +203,9 @@ class AdminParametersForm(param_forms.AdminParametersForm): dpath = force_str(output).strip() else: known_paths = getattr( - settings, "DOVECOT_LOOKUP_PATH", - ("/usr/sbin/dovecot", "/usr/local/sbin/dovecot") + settings, + "DOVECOT_LOOKUP_PATH", + ("/usr/sbin/dovecot", "/usr/local/sbin/dovecot"), ) for fpath in known_paths: if os.path.isfile(fpath) and os.access(fpath, os.X_OK): @@ -242,17 +227,13 @@ class AdminParametersForm(param_forms.AdminParametersForm): def clean_default_domain_quota(self): """Ensure quota is a positive integer.""" if self.cleaned_data["default_domain_quota"] < 0: - raise forms.ValidationError( - gettext_lazy("Must be a positive integer") - ) + raise forms.ValidationError(gettext_lazy("Must be a positive integer")) return self.cleaned_data["default_domain_quota"] def clean_default_mailbox_quota(self): """Ensure quota is a positive integer.""" if self.cleaned_data["default_mailbox_quota"] < 0: - raise forms.ValidationError( - gettext_lazy("Must be a positive integer") - ) + raise forms.ValidationError(gettext_lazy("Must be a positive integer")) return self.cleaned_data["default_mailbox_quota"] def clean_dkim_keys_storage_dir(self): @@ -260,14 +241,11 @@ class AdminParametersForm(param_forms.AdminParametersForm): storage_dir = self.cleaned_data.get("dkim_keys_storage_dir", "") if storage_dir: if not os.path.isdir(storage_dir): - raise forms.ValidationError( - gettext_lazy("Directory not found.") - ) + raise forms.ValidationError(gettext_lazy("Directory not found.")) code, output = exec_cmd("which openssl") if code: raise forms.ValidationError( - gettext_lazy( - "openssl not found, please make sure it is installed.") + gettext_lazy("openssl not found, please make sure it is installed.") ) return storage_dir @@ -278,14 +256,11 @@ class AdminParametersForm(param_forms.AdminParametersForm): return value try: ip_addresses = [ - ipaddress.ip_network(v.strip()) - for v in value.split() if v.strip() + ipaddress.ip_network(v.strip()) for v in value.split() if v.strip() ] except ValueError: raise forms.ValidationError( - gettext_lazy( - "This field only allows valid IP addresses (or networks)" - ) + gettext_lazy("This field only allows valid IP addresses (or networks)") ) return value @@ -293,171 +268,249 @@ class AdminParametersForm(param_forms.AdminParametersForm): """Check MX options.""" cleaned_data = super(AdminParametersForm, self).clean() condition = ( - cleaned_data.get("enable_mx_checks") and - cleaned_data.get("domains_must_have_authorized_mx") and - not cleaned_data.get("valid_mxs")) + cleaned_data.get("enable_mx_checks") + and cleaned_data.get("domains_must_have_authorized_mx") + and not cleaned_data.get("valid_mxs") + ) if condition: self.add_error( - "valid_mxs", - _("Define at least one authorized network / address") + "valid_mxs", _("Define at least one authorized network / address") ) if not cleaned_data.get("enable_mx_checks"): cleaned_data["enable_ipv6_mx_checks"] = False return cleaned_data -GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([ - ("domains", { - "label": gettext_lazy("Domains"), - "params": collections.OrderedDict([ - ("enable_mx_checks", { - "label": gettext_lazy("Enable MX checks"), - "help_text": gettext_lazy( - "Check that every domain has a valid MX record" - ) - }), - ("valid_mxs", { - "label": gettext_lazy("Valid MXs"), - "display": "enable_mx_checks=true", - "help_text": gettext_lazy( - "A list of IP or network address every MX record should " - "match. A warning will be sent if a record does not " - "respect it." +GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict( + [ + ( + "domains", + { + "label": gettext_lazy("Domains"), + "params": collections.OrderedDict( + [ + ( + "enable_mx_checks", + { + "label": gettext_lazy("Enable MX checks"), + "help_text": gettext_lazy( + "Check that every domain has a valid MX record" + ), + }, + ), + ( + "valid_mxs", + { + "label": gettext_lazy("Valid MXs"), + "display": "enable_mx_checks=true", + "help_text": gettext_lazy( + "A list of IP or network address every MX record should " + "match. A warning will be sent if a record does not " + "respect it." + ), + }, + ), + ( + "domains_must_have_authorized_mx", + { + "label": gettext_lazy( + "New domains must use authorized MX(s)" + ), + "display": "enable_mx_checks=true", + "help_text": gettext_lazy( + "Prevent the creation of a new domain if its MX record " + "does not use one of the defined addresses." + ), + }, + ), + ( + "enable_ipv6_mx_checks", + { + "label": gettext_lazy("Enable IPV6 checks"), + "display": "enable_mx_checks=true", + "help_text": gettext_lazy( + "Check if MX setup for ipv6 is valid" + ), + }, + ), + ( + "enable_spf_checks", + { + "label": gettext_lazy("Enable SPF checks"), + "help_text": gettext_lazy( + "Check if every domain has a valid SPF record" + ), + }, + ), + ( + "enable_dkim_checks", + { + "label": gettext_lazy("Enable DKIM checks"), + "help_text": gettext_lazy( + "Check if every domain with DKIM signin enabled " + "has a valid DNS record" + ), + }, + ), + ( + "enable_dmarc_checks", + { + "label": gettext_lazy("Enable DMARC checks"), + "help_text": gettext_lazy( + "Check if every domain has a valid DMARC record" + ), + }, + ), + ( + "enable_autoconfig_checks", + { + "label": gettext_lazy("Enable autoconfig checks"), + "help_text": gettext_lazy( + "Check if every domain has a valid records for " + "autoconfiguration" + ), + }, + ), + ( + "enable_dnsbl_checks", + { + "label": gettext_lazy("Enable DNSBL checks"), + "help_text": gettext_lazy( + "Check every domain against major DNSBL providers" + ), + }, + ), + ( + "custom_dns_server", + { + "label": gettext_lazy("Custom DNS server"), + "help_text": gettext_lazy( + "Use a custom DNS server instead of local server " + "configuration" + ), + }, + ), + ( + "dkim_keys_storage_dir", + { + "label": gettext_lazy("DKIM keys storage directory"), + "help_text": gettext_lazy( + "Absolute path of the directory where DKIM private keys " + "will be stored. Make sure this directory belongs to root " + "user and is not readable by the outside world." + ), + }, + ), + ( + "dkim_default_key_length", + { + "label": gettext_lazy("Default DKIM key length"), + "help_text": gettext_lazy( + "Default length in bits for newly generated DKIM keys." + ), + }, + ), + ( + "default_domain_quota", + { + "label": gettext_lazy("Default domain quota"), + "help_text": gettext_lazy( + "Default quota (in MB) applied to freshly created domains " + "with no value specified. A value of 0 means no quota." + ), + }, + ), + ( + "default_domain_message_limit", + { + "label": gettext_lazy("Default domain sending limit"), + "help_text": gettext_lazy( + "Number of messages freshly created domains will be " + "allowed to send per day. Leave empty for no limit." + ), + }, + ), + ] ), - }), - ("domains_must_have_authorized_mx", { - "label": gettext_lazy("New domains must use authorized MX(s)"), - "display": "enable_mx_checks=true", - "help_text": gettext_lazy( - "Prevent the creation of a new domain if its MX record " - "does not use one of the defined addresses." - ) - }), - ("enable_ipv6_mx_checks", { - "label": gettext_lazy("Enable IPV6 checks"), - "display": "enable_mx_checks=true", - "help_text": gettext_lazy( - "Check if MX setup for ipv6 is valid" - ) - }), - ("enable_spf_checks", { - "label": gettext_lazy("Enable SPF checks"), - "help_text": gettext_lazy( - "Check if every domain has a valid SPF record" - ) - }), - ("enable_dkim_checks", { - "label": gettext_lazy("Enable DKIM checks"), - "help_text": gettext_lazy( - "Check if every domain with DKIM signin enabled " - "has a valid DNS record" - ) - }), - ("enable_dmarc_checks", { - "label": gettext_lazy("Enable DMARC checks"), - "help_text": gettext_lazy( - "Check if every domain has a valid DMARC record" - ) - }), - ("enable_autoconfig_checks", { - "label": gettext_lazy("Enable autoconfig checks"), - "help_text": gettext_lazy( - "Check if every domain has a valid records for " - "autoconfiguration" - ) - }), - ("enable_dnsbl_checks", { - "label": gettext_lazy("Enable DNSBL checks"), - "help_text": gettext_lazy( - "Check every domain against major DNSBL providers" - ) - }), - ("custom_dns_server", { - "label": gettext_lazy("Custom DNS server"), - "help_text": gettext_lazy( - "Use a custom DNS server instead of local server " - "configuration" - ) - }), - ("dkim_keys_storage_dir", { - "label": gettext_lazy("DKIM keys storage directory"), - "help_text": gettext_lazy( - "Absolute path of the directory where DKIM private keys " - "will be stored. Make sure this directory belongs to root " - "user and is not readable by the outside world." + }, + ), + ( + "mailboxes", + { + "label": gettext_lazy("Mailboxes"), + "params": collections.OrderedDict( + [ + ( + "handle_mailboxes", + { + "label": gettext_lazy("Handle mailboxes on filesystem"), + "help_text": gettext_lazy( + "Rename or remove mailboxes on the filesystem when they get" + " renamed or removed within Modoboa" + ), + }, + ), + ( + "default_mailbox_quota", + { + "label": gettext_lazy("Default mailbox quota"), + "help_text": gettext_lazy( + "Default mailbox quota (in MB) applied to freshly created " + "domains with no value specified. A value of 0 means no " + "quota." + ), + }, + ), + ( + "default_mailbox_message_limit", + { + "label": gettext_lazy("Default mailbox sending limit"), + "help_text": gettext_lazy( + "Number of messages freshly created mailboxes will be " + "allowed to send per day. Leave empty for no limit." + ), + }, + ), + ( + "auto_account_removal", + { + "label": gettext_lazy("Automatic account removal"), + "help_text": gettext_lazy( + "When a mailbox is removed, also remove the associated " + "account" + ), + }, + ), + ( + "auto_create_domain_and_mailbox", + { + "label": gettext_lazy( + "Automatic domain/mailbox creation" + ), + "help_text": gettext_lazy( + "Create a domain and a mailbox when an account is " + "automatically created." + ), + }, + ), + ( + "create_alias_on_mbox_rename", + { + "label": gettext_lazy( + "Create an alias when a mailbox is renamed" + ), + "help_text": gettext_lazy( + "Create an alias using the old address when a mailbox is " + "renamed." + ), + }, + ), + ] ), - }), - ("dkim_default_key_length", { - "label": gettext_lazy("Default DKIM key length"), - "help_text": gettext_lazy( - "Default length in bits for newly generated DKIM keys." - ) - }), - ("default_domain_quota", { - "label": gettext_lazy("Default domain quota"), - "help_text": gettext_lazy( - "Default quota (in MB) applied to freshly created domains " - "with no value specified. A value of 0 means no quota." - ) - }), - ("default_domain_message_limit", { - "label": gettext_lazy("Default domain sending limit"), - "help_text": gettext_lazy( - "Number of messages freshly created domains will be " - "allowed to send per day. Leave empty for no limit." - ) - }), - ]) - }), - ("mailboxes", { - "label": gettext_lazy("Mailboxes"), - "params": collections.OrderedDict([ - ("handle_mailboxes", { - "label": gettext_lazy("Handle mailboxes on filesystem"), - "help_text": gettext_lazy( - "Rename or remove mailboxes on the filesystem when they get" - " renamed or removed within Modoboa" - ) - }), - ("default_mailbox_quota", { - "label": gettext_lazy("Default mailbox quota"), - "help_text": gettext_lazy( - "Default mailbox quota (in MB) applied to freshly created " - "domains with no value specified. A value of 0 means no " - "quota." - ) - }), - ("default_mailbox_message_limit", { - "label": gettext_lazy("Default mailbox sending limit"), - "help_text": gettext_lazy( - "Number of messages freshly created mailboxes will be " - "allowed to send per day. Leave empty for no limit." - ) - }), - ("auto_account_removal", { - "label": gettext_lazy("Automatic account removal"), - "help_text": gettext_lazy( - "When a mailbox is removed, also remove the associated " - "account") - }), - ("auto_create_domain_and_mailbox", { - "label": gettext_lazy("Automatic domain/mailbox creation"), - "help_text": gettext_lazy( - "Create a domain and a mailbox when an account is " - "automatically created." - ) - }), - ("create_alias_on_mbox_rename", { - "label": gettext_lazy( - "Create an alias when a mailbox is renamed"), - "help_text": gettext_lazy( - "Create an alias using the old address when a mailbox is " - "renamed." - ) - }) - ]) - }), -]) + }, + ), + ] +) def load_admin_settings(): @@ -466,8 +519,12 @@ def load_admin_settings(): from .api.v2 import serializers param_tools.registry.add( - "global", AdminParametersForm, gettext_lazy("Administration")) + "global", AdminParametersForm, gettext_lazy("Administration") + ) param_tools.registry.add2( - "global", "admin", gettext_lazy("Administration"), + "global", + "admin", + gettext_lazy("Administration"), GLOBAL_PARAMETERS_STRUCT, - serializers.AdminGlobalParametersSerializer) + serializers.AdminGlobalParametersSerializer, + ) diff --git a/modoboa/admin/constants.py b/modoboa/admin/constants.py index 7a3595de9..da54ecb7e 100644 --- a/modoboa/admin/constants.py +++ b/modoboa/admin/constants.py @@ -17,12 +17,7 @@ DNSBL_PROVIDERS = [ # Do not run tests for these domains. # https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains -RESERVED_TLD = [ - "example", - "invalid", - "localhost", - "test" -] +RESERVED_TLD = ["example", "invalid", "localhost", "test"] DOMAIN_TYPES = [ ("domain", _("Domain")), diff --git a/modoboa/admin/factories.py b/modoboa/admin/factories.py index cd5d33221..2d59ff863 100644 --- a/modoboa/admin/factories.py +++ b/modoboa/admin/factories.py @@ -9,12 +9,11 @@ from . import models class DomainFactory(PermissionFactory): - """Factory to create Domains.""" class Meta: model = models.Domain - django_get_or_create = ("name", ) + django_get_or_create = ("name",) name = factory.Sequence(lambda n: "domain{}.test".format(n)) type = "domain" # NOQA:A003 @@ -24,19 +23,17 @@ class DomainFactory(PermissionFactory): class DomainAliasFactory(PermissionFactory): - """Factory to create DomainAlias objects.""" class Meta: model = models.DomainAlias - django_get_or_create = ("name", ) + django_get_or_create = ("name",) target = factory.SubFactory(DomainFactory) enabled = True class MailboxFactory(PermissionFactory): - """A factory to create Mailbox instances.""" class Meta: @@ -98,31 +95,23 @@ def populate_database(): """ dom = DomainFactory(name="test.com", quota=50) admin = UserFactory( - username="admin@test.com", groups=("DomainAdmins", ), - password="{PLAIN}toto" + username="admin@test.com", groups=("DomainAdmins",), password="{PLAIN}toto" ) MailboxFactory(address="admin", domain=dom, user=admin) account = UserFactory.create( - username="user@test.com", groups=("SimpleUsers",), + username="user@test.com", + groups=("SimpleUsers",), ) MailboxFactory.create(address="user", domain=dom, user=account) - al = AliasFactory.create( - address="forward@test.com", domain=dom - ) - AliasRecipientFactory.create( - address="user@external.com", alias=al) + al = AliasFactory.create(address="forward@test.com", domain=dom) + AliasRecipientFactory.create(address="user@external.com", alias=al) - al = AliasFactory.create( - address="alias@test.com", domain=dom - ) + al = AliasFactory.create(address="alias@test.com", domain=dom) mb = account.mailbox - AliasRecipientFactory.create( - address=mb.full_address, alias=al, r_mailbox=mb) + AliasRecipientFactory.create(address=mb.full_address, alias=al, r_mailbox=mb) - al = AliasFactory.create( - address="postmaster@test.com", domain=dom - ) + al = AliasFactory.create(address="postmaster@test.com", domain=dom) for address in ["toto@titi.com", "test@truc.fr"]: AliasRecipientFactory.create(address=address, alias=al) @@ -130,12 +119,9 @@ def populate_database(): dom2 = DomainFactory.create(name="test2.com", default_mailbox_quota=0) admin = UserFactory.create( - username="admin@test2.com", groups=("DomainAdmins",), - password="{PLAIN}toto" + username="admin@test2.com", groups=("DomainAdmins",), password="{PLAIN}toto" ) MailboxFactory.create(address="admin", domain=dom2, user=admin) - u = UserFactory.create( - username="user@test2.com", groups=("SimpleUsers",) - ) + u = UserFactory.create(username="user@test2.com", groups=("SimpleUsers",)) MailboxFactory.create(address="user", domain=dom2, user=u) dom2.add_admin(admin) diff --git a/modoboa/admin/forms/__init__.py b/modoboa/admin/forms/__init__.py index 3637c3637..4239ef97e 100644 --- a/modoboa/admin/forms/__init__.py +++ b/modoboa/admin/forms/__init__.py @@ -1,19 +1,29 @@ from .account import ( - AccountForm, AccountFormGeneral, AccountFormMail, AccountPermissionsForm, - AccountWizard + AccountForm, + AccountFormGeneral, + AccountFormMail, + AccountPermissionsForm, + AccountWizard, ) from .alias import AliasForm -from .domain import ( - DomainForm, DomainFormGeneral, DomainFormOptions, DomainWizard -) +from .domain import DomainForm, DomainFormGeneral, DomainFormOptions, DomainWizard from .export import ExportDataForm from .forward import ForwardForm from .import_ import ImportDataForm, ImportIdentitiesForm __all__ = [ - "DomainFormGeneral", "DomainFormOptions", "DomainForm", - "AccountFormGeneral", "AccountFormMail", "AccountPermissionsForm", - "AccountForm", "AliasForm", "ImportDataForm", "ImportIdentitiesForm", + "DomainFormGeneral", + "DomainFormOptions", + "DomainForm", + "AccountFormGeneral", + "AccountFormMail", + "AccountPermissionsForm", + "AccountForm", + "AliasForm", + "ImportDataForm", + "ImportIdentitiesForm", "ExportDataForm", - "ForwardForm", "DomainWizard", "AccountWizard" + "ForwardForm", + "DomainWizard", + "AccountWizard", ] diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 8fd2572b6..cbfcffc52 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -14,9 +14,7 @@ from modoboa.core import signals as core_signals from modoboa.core.models import User from modoboa.lib import exceptions as lib_exceptions, fields as lib_fields from modoboa.lib.email_utils import split_mailbox -from modoboa.lib.form_utils import ( - DynamicForm, TabForms, WizardForm, WizardStep -) +from modoboa.lib.form_utils import DynamicForm, TabForms, WizardForm, WizardStep from modoboa.lib.permissions import get_account_roles from modoboa.lib.validators import validate_utf8_email from modoboa.lib.web_utils import render_to_json_response, size2integer @@ -25,7 +23,6 @@ from .. import lib, models, signals class AccountFormGeneral(forms.ModelForm): - """General account form.""" username = forms.CharField( @@ -33,12 +30,12 @@ class AccountFormGeneral(forms.ModelForm): help_text=gettext_lazy( "The user's name. Must be a valid e-mail address for simple users " "or administrators with a mailbox." - ) + ), ) role = forms.ChoiceField( label=gettext_lazy("Role"), choices=[("", gettext_lazy("Choose"))], - help_text=gettext_lazy("What level of permission this user will have") + help_text=gettext_lazy("What level of permission this user will have"), ) random_password = forms.BooleanField( label=gettext_lazy("Random password"), @@ -46,55 +43,66 @@ class AccountFormGeneral(forms.ModelForm): "Generate a random password. If you're updating this account and " "check this box, a new password will be generated." ), - required=False + required=False, ) password1 = forms.CharField( - label=gettext_lazy("Password"), widget=forms.widgets.PasswordInput, - required=False + label=gettext_lazy("Password"), + widget=forms.widgets.PasswordInput, + required=False, ) password2 = forms.CharField( label=gettext_lazy("Confirmation"), widget=forms.widgets.PasswordInput, - help_text=gettext_lazy( - "Enter the same password as above, for verification." - ), - required=False + help_text=gettext_lazy("Enter the same password as above, for verification."), + required=False, ) class Meta: model = User fields = ( - "username", "first_name", "last_name", "role", "is_active", + "username", + "first_name", + "last_name", + "role", + "is_active", "master_user", ) - labels = { - "is_active": gettext_lazy("Enabled") - } + labels = {"is_active": gettext_lazy("Enabled")} def __init__(self, user, *args, **kwargs): super(AccountFormGeneral, self).__init__(*args, **kwargs) self.fields = OrderedDict( - (key, self.fields[key]) for key in - ["role", "username", "first_name", "last_name", - "random_password", "password1", "password2", - "master_user", "is_active"] + (key, self.fields[key]) + for key in [ + "role", + "username", + "first_name", + "last_name", + "random_password", + "password1", + "password2", + "master_user", + "is_active", + ] ) self.user = user condition = ( - user.role == "DomainAdmins" or - user.role == "Resellers" and self.instance == user + user.role == "DomainAdmins" + or user.role == "Resellers" + and self.instance == user ) if condition: self.fields["role"] = forms.CharField( label="", widget=forms.HiddenInput(attrs={"class": "form-control"}), - required=False + required=False, ) else: self.fields["role"].choices += ( get_account_roles(user, self.instance) - if self.instance.pk else get_account_roles(user) + if self.instance.pk + else get_account_roles(user) ) if not user.is_superuser: @@ -104,8 +112,8 @@ class AccountFormGeneral(forms.ModelForm): return domain_disabled = ( - hasattr(self.instance, "mailbox") and - not self.instance.mailbox.domain.enabled + hasattr(self.instance, "mailbox") + and not self.instance.mailbox.domain.enabled ) if domain_disabled: self.fields["is_active"].widget.attrs["disabled"] = "disabled" @@ -113,9 +121,10 @@ class AccountFormGeneral(forms.ModelForm): del self.fields["is_active"] self.fields["role"].initial = self.instance.role condition = ( - not self.instance.is_local and - param_tools.get_global_parameter( - "ldap_auth_method", app="core") == "directbind") + not self.instance.is_local + and param_tools.get_global_parameter("ldap_auth_method", app="core") + == "directbind" + ) if condition: del self.fields["random_password"] del self.fields["password1"] @@ -158,13 +167,12 @@ class AccountFormGeneral(forms.ModelForm): if self.errors: return self.cleaned_data condition = ( - self.cleaned_data.get("master_user") and - self.cleaned_data["role"] != "SuperAdmins" + self.cleaned_data.get("master_user") + and self.cleaned_data["role"] != "SuperAdmins" ) if condition: self.add_error( - "master_user", - _("Only super administrators are allowed for this mode") + "master_user", _("Only super administrators are allowed for this mode") ) random_password = self.cleaned_data.get("random_password") if random_password: @@ -182,12 +190,11 @@ class AccountFormGeneral(forms.ModelForm): return self.cleaned_data if password1 != password2: self.add_error( - "password2", - _("The two password fields didn't match.")) + "password2", _("The two password fields didn't match.") + ) return self.cleaned_data try: - password_validation.validate_password( - password2, self.instance) + password_validation.validate_password(password2, self.instance) except forms.ValidationError as ve: self.add_error("password2", ve.messages) return self.cleaned_data @@ -196,7 +203,8 @@ class AccountFormGeneral(forms.ModelForm): account = super(AccountFormGeneral, self).save(commit=False) if self.user == account and not self.cleaned_data["is_active"]: raise lib_exceptions.PermDeniedException( - _("You can't disable your own account")) + _("You can't disable your own account") + ) if not account.pk: account.language = settings.LANGUAGE_CODE if commit: @@ -207,7 +215,8 @@ class AccountFormGeneral(forms.ModelForm): if hasattr(account, "mailbox"): # Update forward status according to account status models.Alias.objects.filter(address=account.email).update( - enabled=account.is_active) + enabled=account.is_active + ) return account @@ -222,12 +231,11 @@ class AccountProfileForm(forms.ModelForm): class AccountFormMail(forms.Form, DynamicForm): """Form to handle mail part.""" - email = lib_fields.UTF8EmailField( - label=gettext_lazy("E-mail"), required=False) + email = lib_fields.UTF8EmailField(label=gettext_lazy("E-mail"), required=False) create_alias_with_old_address = forms.BooleanField( label=gettext_lazy("Create an alias using the old address"), required=False, - initial=False + initial=False, ) quota = forms.CharField( label=gettext_lazy("Quota"), @@ -239,20 +247,20 @@ class AccountFormMail(forms.Form, DynamicForm): "unlimited value (not allowed for domain " "administrators)." ), - widget=forms.widgets.TextInput(attrs={"class": "form-control"}) + widget=forms.widgets.TextInput(attrs={"class": "form-control"}), ) quota_act = forms.BooleanField(required=False) message_limit = forms.IntegerField( label=gettext_lazy("Message sending limit"), required=False, min_value=0, - help_text=gettext_lazy( - "Number of messages this mailbox can send per day") + help_text=gettext_lazy("Number of messages this mailbox can send per day"), ) is_send_only = forms.BooleanField( required=False, help_text=gettext_lazy( - "Block IMAP connection and incoming mails for this mailbox") + "Block IMAP connection and incoming mails for this mailbox" + ), ) aliases = lib_fields.UTF8AndEmptyUserEmailField( label=gettext_lazy("Alias(es)"), @@ -261,7 +269,7 @@ class AccountFormMail(forms.Form, DynamicForm): "Alias(es) of this mailbox. Indicate only one address per input, " "press ENTER to add a new input. To create a catchall alias, just " "enter the domain name (@domain.tld)." - ) + ), ) senderaddress = lib_fields.UTF8AndEmptyUserEmailField( label=gettext_lazy("Sender addresses"), @@ -271,16 +279,14 @@ class AccountFormMail(forms.Form, DynamicForm): "allowed to send emails using this address, even if it " "does not exist locally. Indicate one address per input. Press " "ENTER to add a new input." - ) + ), ) def __init__(self, user, *args, **kwargs): self.mb = kwargs.pop("instance", None) self.user = user super().__init__(*args, **kwargs) - self.field_widths = { - "quota": 3 - } + self.field_widths = {"quota": 3} params = dict(param_tools.get_global_parameters("admin")) if self.mb is not None: self.fields["email"].required = True @@ -288,13 +294,13 @@ class AccountFormMail(forms.Form, DynamicForm): for cpt, ralias in enumerate(qset): name = "aliases_{}".format(cpt + 1) self._create_field( - lib_fields.UTF8AndEmptyUserEmailField, name, - ralias.alias.address) + lib_fields.UTF8AndEmptyUserEmailField, name, ralias.alias.address + ) for cpt, saddress in enumerate(self.mb.senderaddress_set.all()): name = "senderaddress_{}".format(cpt + 1) self._create_field( - lib_fields.UTF8AndEmptyUserEmailField, name, - saddress.address) + lib_fields.UTF8AndEmptyUserEmailField, name, saddress.address + ) self.fields["email"].initial = self.mb.full_address self.fields["quota_act"].initial = self.mb.use_domain_quota if not self.mb.use_domain_quota and self.mb.quota: @@ -302,22 +308,24 @@ class AccountFormMail(forms.Form, DynamicForm): if self.mb.message_limit: self.fields["message_limit"].initial = self.mb.message_limit self.fields["is_send_only"].initial = self.mb.is_send_only - self.fields["create_alias_with_old_address"].initial = ( - params["create_alias_on_mbox_rename"] - ) + self.fields["create_alias_with_old_address"].initial = params[ + "create_alias_on_mbox_rename" + ] else: del self.fields["create_alias_with_old_address"] self.fields["quota_act"].initial = True if params["default_mailbox_message_limit"] is not None: - self.fields["message_limit"].initial = ( - params["default_mailbox_message_limit"]) + self.fields["message_limit"].initial = params[ + "default_mailbox_message_limit" + ] if len(args) and isinstance(args[0], QueryDict): self._load_from_qdict( - args[0], "aliases", lib_fields.UTF8AndEmptyUserEmailField) + args[0], "aliases", lib_fields.UTF8AndEmptyUserEmailField + ) self._load_from_qdict( - args[0], "senderaddress", - lib_fields.UTF8AndEmptyUserEmailField) + args[0], "senderaddress", lib_fields.UTF8AndEmptyUserEmailField + ) def clean_email(self): """Ensure lower case emails""" @@ -332,8 +340,8 @@ class AccountFormMail(forms.Form, DynamicForm): if not self.mb: try: core_signals.can_create_object.send( - sender=self.__class__, context=self.domain, - object_type="mailboxes") + sender=self.__class__, context=self.domain, object_type="mailboxes" + ) except lib_exceptions.ModoboaException as inst: raise forms.ValidationError(inst) return email @@ -350,9 +358,10 @@ class AccountFormMail(forms.Form, DynamicForm): cleaned_data = super(AccountFormMail, self).clean() use_default_domain_quota = cleaned_data["quota_act"] condition = ( - not use_default_domain_quota and - cleaned_data["quota"] is not None and - cleaned_data["quota"] < 0) + not use_default_domain_quota + and cleaned_data["quota"] is not None + and cleaned_data["quota"] < 0 + ) if condition: self.add_error("quota", _("Must be a positive integer")) self.aliases = [] @@ -367,16 +376,14 @@ class AccountFormMail(forms.Form, DynamicForm): self.add_error(name, _("Local domain does not exist")) continue if not self.user.can_access(domain): - self.add_error( - name, _("You don't have access to this domain")) + self.add_error(name, _("You don't have access to this domain")) continue self.aliases.append(value.lower()) elif name.startswith("senderaddress"): local_part, domname = split_mailbox(value) domain = models.Domain.objects.filter(name=domname).first() if domain and not self.user.can_access(domain): - self.add_error( - name, _("You don't have access to this domain")) + self.add_error(name, _("You don't have access to this domain")) continue self.sender_addresses.append(value.lower()) return cleaned_data @@ -386,17 +393,20 @@ class AccountFormMail(forms.Form, DynamicForm): if not user.can_access(self.domain): raise lib_exceptions.PermDeniedException core_signals.can_create_object.send( - self.__class__, context=user, klass=models.Mailbox) + self.__class__, context=user, klass=models.Mailbox + ) self.mb = models.Mailbox( - address=self.locpart, domain=self.domain, user=account, + address=self.locpart, + domain=self.domain, + user=account, use_domain_quota=self.cleaned_data["quota_act"], message_limit=self.cleaned_data.get("message_limit"), - is_send_only=self.cleaned_data.get("is_send_only", False) + is_send_only=self.cleaned_data.get("is_send_only", False), ) override_rules = ( - user.is_superuser or - user.has_perm("admin.add_domain") and - not user.userobjectlimit_set.get(name="quota").max_value + user.is_superuser + or user.has_perm("admin.add_domain") + and not user.userobjectlimit_set.get(name="quota").max_value ) self.mb.set_quota(self.cleaned_data["quota"], override_rules) self.mb.save(creator=user) @@ -405,7 +415,8 @@ class AccountFormMail(forms.Form, DynamicForm): """Update mailbox aliases.""" # 1. We check if existing aliases were removed... qset = self.mb.aliasrecipient_set.select_related("alias").filter( - alias__internal=False) + alias__internal=False + ) for ralias in qset: if ralias.alias.address not in self.aliases: alias = ralias.alias @@ -420,22 +431,28 @@ class AccountFormMail(forms.Form, DynamicForm): return # 3. Check limits at all levels core_signals.can_create_object.send( - self.__class__, context=user, klass=models.Alias, - count=len(self.aliases)) + self.__class__, context=user, klass=models.Alias, count=len(self.aliases) + ) core_signals.can_create_object.send( - self.__class__, context=self.mb.domain, - object_type="mailbox_aliases", count=len(self.aliases)) + self.__class__, + context=self.mb.domain, + object_type="mailbox_aliases", + count=len(self.aliases), + ) # 4. Create / update aliases for alias in self.aliases: - if self.mb.aliasrecipient_set.select_related("alias").filter( - alias__address=alias, alias__internal=False).exists(): + if ( + self.mb.aliasrecipient_set.select_related("alias") + .filter(alias__address=alias, alias__internal=False) + .exists() + ): continue local_part, domname = split_mailbox(alias) al, _ = models.Alias.objects.get_or_create( address=alias, domain=models.Domain.objects.get(name=domname), internal=False, - defaults={'enabled': account.is_active}, + defaults={"enabled": account.is_active}, ) al.add_recipients([self.mb.full_address]) al.post_create(user) @@ -451,8 +468,7 @@ class AccountFormMail(forms.Form, DynamicForm): return to_create = [] for saddress in self.sender_addresses: - to_create.append( - models.SenderAddress(address=saddress, mailbox=self.mb)) + to_create.append(models.SenderAddress(address=saddress, mailbox=self.mb)) models.SenderAddress.objects.bulk_create(to_create) def save(self, user, account): @@ -466,8 +482,7 @@ class AccountFormMail(forms.Form, DynamicForm): if not hasattr(self, "mb") or self.mb is None: self.create_mailbox(user, account) else: - self.cleaned_data["use_domain_quota"] = ( - self.cleaned_data["quota_act"]) + self.cleaned_data["use_domain_quota"] = self.cleaned_data["quota_act"] if self.cleaned_data.get("create_alias_with_old_address", False): self.aliases.append(self.mb.full_address) self.mb.update_from_dict(user, self.cleaned_data) @@ -487,7 +502,7 @@ class AccountPermissionsForm(forms.Form, DynamicForm): domains = lib_fields.DomainNameField( label=gettext_lazy("Domain(s)"), required=False, - help_text=gettext_lazy("Domain(s) that user administrates") + help_text=gettext_lazy("Domain(s) that user administrates"), ) def __init__(self, *args, **kwargs): @@ -504,15 +519,13 @@ class AccountPermissionsForm(forms.Form, DynamicForm): name = "domains_%d" % (pos + 1) self._create_field(lib_fields.DomainNameField, name, dom.name) if len(args) and isinstance(args[0], QueryDict): - self._load_from_qdict( - args[0], "domains", lib_fields.DomainNameField) + self._load_from_qdict(args[0], "domains", lib_fields.DomainNameField) def save(self): if self.account.role == "SimpleUsers": return current_domains = [ - dom.name for dom in - models.Domain.objects.get_for_admin(self.account) + dom.name for dom in models.Domain.objects.get_for_admin(self.account) ] for name, value in self.cleaned_data.items(): if not name.startswith("domains"): @@ -534,45 +547,57 @@ class AccountForm(TabForms): def __init__(self, request, *args, **kwargs): self.user = request.user self.forms = [ - {"id": "general", "title": _("General"), - "formtpl": "admin/account_general_form.html", - "cls": AccountFormGeneral, - "new_args": [self.user], "mandatory": True}, - {"id": "profile", "title": _("Profile"), - "formtpl": "admin/account_profile_form.html", - "cls": AccountProfileForm}, - {"id": "mail", - "title": _("Mail"), "formtpl": "admin/mailform.html", - "cls": AccountFormMail, - "new_args": [self.user]}, - {"id": "perms", "title": _("Permissions"), - "formtpl": "admin/permsform.html", - "cls": AccountPermissionsForm} + { + "id": "general", + "title": _("General"), + "formtpl": "admin/account_general_form.html", + "cls": AccountFormGeneral, + "new_args": [self.user], + "mandatory": True, + }, + { + "id": "profile", + "title": _("Profile"), + "formtpl": "admin/account_profile_form.html", + "cls": AccountProfileForm, + }, + { + "id": "mail", + "title": _("Mail"), + "formtpl": "admin/mailform.html", + "cls": AccountFormMail, + "new_args": [self.user], + }, + { + "id": "perms", + "title": _("Permissions"), + "formtpl": "admin/permsform.html", + "cls": AccountPermissionsForm, + }, ] cbargs = {"user": self.user} if "instances" in kwargs: cbargs["account"] = kwargs["instances"]["general"] - results = signals.extra_account_forms.send( - sender=self.__class__, **cbargs) - self.forms += reduce( - lambda a, b: a + b, [result[1] for result in results]) + results = signals.extra_account_forms.send(sender=self.__class__, **cbargs) + self.forms += reduce(lambda a, b: a + b, [result[1] for result in results]) super(AccountForm, self).__init__(request, *args, **kwargs) def extra_context(self, context): account = self.instances["general"] - context.update({ - "title": account.username, - "formid": "accountform", - "action": reverse("admin:account_change", - args=[account.id]), - }) + context.update( + { + "title": account.username, + "formid": "accountform", + "action": reverse("admin:account_change", args=[account.id]), + } + ) def check_perms(self, account): """Check if perms form must displayed or not.""" return ( - self.user.is_superuser and - not account.is_superuser and - account.has_perm("core.add_user") + self.user.is_superuser + and not account.is_superuser + and account.has_perm("core.add_user") ) def _before_is_valid(self, form): @@ -585,7 +610,8 @@ class AccountForm(TabForms): return True results = signals.check_extra_account_form.send( - sender=self.__class__, account=self.account, form=form) + sender=self.__class__, account=self.account, form=form + ) results = [result[1] for result in results] if False in results: return False @@ -617,31 +643,33 @@ class AccountForm(TabForms): class AccountWizard(WizardForm): - """Account creation wizard.""" def __init__(self, request): super(AccountWizard, self).__init__(request) self.add_step( WizardStep( - "general", AccountFormGeneral, _("General"), - new_args=[request.user] + "general", AccountFormGeneral, _("General"), new_args=[request.user] ) ) self.add_step( WizardStep( - "mail", AccountFormMail, _("Mail"), + "mail", + AccountFormMail, + _("Mail"), "admin/mailform.html", - new_args=[request.user] + new_args=[request.user], ) ) def extra_context(self, context): - context.update({ - "title": _("New account"), - "action": reverse("admin:account_add"), - "formid": "newaccount_form" - }) + context.update( + { + "title": _("New account"), + "action": reverse("admin:account_add"), + "formid": "newaccount_form", + } + ) def done(self): account = self.first_step.form.save() diff --git a/modoboa/admin/forms/alias.py b/modoboa/admin/forms/alias.py index 86e95b5da..33794bfdb 100644 --- a/modoboa/admin/forms/alias.py +++ b/modoboa/admin/forms/alias.py @@ -17,43 +17,50 @@ class AliasForm(forms.ModelForm, DynamicForm): """A form to create/modify an alias.""" random_address = forms.BooleanField( - label=gettext_lazy("Random address"), - required=False) + label=gettext_lazy("Random address"), required=False + ) address = lib_fields.UTF8AndEmptyUserEmailField( label=gettext_lazy("Email address"), help_text=gettext_lazy( "The alias address. To create a catchall alias, just enter the " "domain name (@domain.tld)." ), - widget=forms.TextInput(attrs={"class": "form-control"}) + widget=forms.TextInput(attrs={"class": "form-control"}), ) recipients = lib_fields.UTF8AndEmptyUserEmailField( - label=gettext_lazy("Recipients"), required=False, + label=gettext_lazy("Recipients"), + required=False, help_text=gettext_lazy( "Addresses this alias will point to. Indicate only one address " "per input, press ENTER to add a new input." ), - widget=forms.TextInput(attrs={"class": "form-control"}) + widget=forms.TextInput(attrs={"class": "form-control"}), ) class Meta: model = Alias fields = ("address", "domain", "enabled", "expire_at", "description") - labels = { - "domain": gettext_lazy("Domain") - } + labels = {"domain": gettext_lazy("Domain")} widgets = { "domain": forms.widgets.Select( - attrs={"class": "selectpicker", "data-live-search": "true"}) + attrs={"class": "selectpicker", "data-live-search": "true"} + ) } def __init__(self, user, *args, **kwargs): self.user = user super(AliasForm, self).__init__(*args, **kwargs) self.fields = OrderedDict( - (key, self.fields[key]) for key in - ["random_address", "address", "domain", "recipients", "enabled", - "expire_at", "description"] + (key, self.fields[key]) + for key in [ + "random_address", + "address", + "domain", + "recipients", + "enabled", + "expire_at", + "description", + ] ) if self.instance.pk: del self.fields["random_address"] @@ -87,14 +94,12 @@ class AliasForm(forms.ModelForm, DynamicForm): except Domain.DoesNotExist: raise forms.ValidationError(_("Domain does not exist")) if not self.user.can_access(domain): - raise forms.ValidationError( - _("You don't have access to this domain") - ) + raise forms.ValidationError(_("You don't have access to this domain")) if not self.instance.pk: try: core_signals.can_create_object.send( - sender=self.__class__, context=domain, - object_type="mailbox_aliases") + sender=self.__class__, context=domain, object_type="mailbox_aliases" + ) except lib_exceptions.ModoboaException as inst: raise forms.ValidationError(inst) return self.cleaned_data["address"].lower() @@ -119,14 +124,16 @@ class AliasForm(forms.ModelForm, DynamicForm): alias = super(AliasForm, self).save(commit=False) if self.cleaned_data.get("random_address"): alias.address = "{}@{}".format( - Alias.generate_random_address(), alias.domain) + Alias.generate_random_address(), alias.domain + ) else: local_part, domname = split_mailbox(self.cleaned_data["address"]) alias.domain = Domain.objects.get(name=domname) if commit: alias.save() address_list = [ - value for field, value in self.cleaned_data.items() + value + for field, value in self.cleaned_data.items() if field.startswith("recipients") and value ] alias.set_recipients(address_list) diff --git a/modoboa/admin/forms/domain.py b/modoboa/admin/forms/domain.py index c14bf067f..e5e028527 100644 --- a/modoboa/admin/forms/domain.py +++ b/modoboa/admin/forms/domain.py @@ -13,7 +13,11 @@ from modoboa.core.models import User from modoboa.lib.exceptions import Conflict from modoboa.lib.fields import DomainNameField from modoboa.lib.form_utils import ( - DynamicForm, TabForms, WizardForm, WizardStep, YesNoField + DynamicForm, + TabForms, + WizardForm, + WizardStep, + YesNoField, ) from modoboa.lib.web_utils import render_to_json_response, size2integer from modoboa.parameters import tools as param_tools @@ -33,17 +37,18 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): help_text=gettext_lazy( "Alias(es) of this domain. Indicate only one name per input, " "press ENTER to add a new input." - ) + ), ) dkim_key_selector = forms.CharField( - label=gettext_lazy("Key selector"), initial="modoboa", required=False) + label=gettext_lazy("Key selector"), initial="modoboa", required=False + ) quota = forms.CharField( label=gettext_lazy("Quota"), initial=0, help_text=gettext_lazy( "Quota shared between mailboxes. Can be expressed in KB, " "MB (default) or GB. A value of 0 means no quota." - ) + ), ) default_mailbox_quota = forms.CharField( label=gettext_lazy("Default mailbox quota"), @@ -51,15 +56,23 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): help_text=gettext_lazy( "Default quota applied to mailboxes. Can be expressed in KB, MB " "(default) or GB. A value of 0 means no quota." - ) + ), ) class Meta: model = Domain fields = ( - "name", "type", "quota", "default_mailbox_quota", "message_limit", - "aliases", "enabled", "enable_dns_checks", "enable_dkim", - "dkim_key_selector", "dkim_key_length" + "name", + "type", + "quota", + "default_mailbox_quota", + "message_limit", + "aliases", + "enabled", + "enable_dns_checks", + "enable_dkim", + "dkim_key_selector", + "dkim_key_length", ) def __init__(self, user, *args, **kwargs): @@ -71,15 +84,12 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): params = dict(param_tools.get_global_parameters("admin")) self.fields["quota"].initial = params["default_domain_quota"] if params["default_domain_message_limit"] is not None: - self.fields["message_limit"].initial = ( - params["default_domain_message_limit"]) - self.fields["default_mailbox_quota"].initial = ( - params["default_mailbox_quota"]) + self.fields["message_limit"].initial = params[ + "default_domain_message_limit" + ] + self.fields["default_mailbox_quota"].initial = params["default_mailbox_quota"] self.fields["type"].choices = constants.DOMAIN_TYPES - self.field_widths = { - "quota": 3, - "default_mailbox_quota": 3 - } + self.field_widths = {"quota": 3, "default_mailbox_quota": 3} self.user = user if len(args) and isinstance(args[0], QueryDict): @@ -93,19 +103,20 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): def clean_name(self): """Check unicity and more.""" name = self.cleaned_data["name"].lower() - label = lib.check_if_domain_exists( - name, [(DomainAlias, _("domain alias"))]) + label = lib.check_if_domain_exists(name, [(DomainAlias, _("domain alias"))]) if label is not None: raise forms.ValidationError( - _("A %s with this name already exists") % force_str(label)) + _("A %s with this name already exists") % force_str(label) + ) - domains_must_have_authorized_mx = ( - param_tools.get_global_parameter("domains_must_have_authorized_mx") + domains_must_have_authorized_mx = param_tools.get_global_parameter( + "domains_must_have_authorized_mx" ) if domains_must_have_authorized_mx and not self.user.is_superuser: if not lib.domain_has_authorized_mx(name): raise forms.ValidationError( - _("No authorized MX record found for this domain")) + _("No authorized MX record found for this domain") + ) return name @@ -116,8 +127,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): return enabled storage_dir = param_tools.get_global_parameter("dkim_keys_storage_dir") if not storage_dir: - raise forms.ValidationError( - _("DKIM keys storage directory not configured")) + raise forms.ValidationError(_("DKIM keys storage directory not configured")) return enabled def clean_quota(self): @@ -127,7 +137,8 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): def clean_default_mailbox_quota(self): """Return proper quota value.""" return size2integer( - self.cleaned_data["default_mailbox_quota"], output_unit="MB") + self.cleaned_data["default_mailbox_quota"], output_unit="MB" + ) def clean(self): """Custom fields validation. @@ -141,13 +152,13 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): if self._errors: return cleaned_data condition = ( - self.cleaned_data["quota"] != 0 and - self.cleaned_data["default_mailbox_quota"] > - self.cleaned_data["quota"]) + self.cleaned_data["quota"] != 0 + and self.cleaned_data["default_mailbox_quota"] > self.cleaned_data["quota"] + ) if condition: self.add_error( - "default_mailbox_quota", - _("Cannot be greater than domain quota")) + "default_mailbox_quota", _("Cannot be greater than domain quota") + ) elif self.user.role == "Resellers": limit = self.user.userobjectlimit_set.get(name="quota") if limit.max_value != 0: @@ -155,14 +166,12 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): msg = _("You can't define an unlimited quota.") if quota == 0: self.add_error("quota", msg) - default_mailbox_quota = self.cleaned_data[ - "default_mailbox_quota"] + default_mailbox_quota = self.cleaned_data["default_mailbox_quota"] if default_mailbox_quota == 0: self.add_error("default_mailbox_quota", msg) if self.cleaned_data["enable_dkim"]: if not self.cleaned_data.get("dkim_key_selector"): - self.add_error( - "dkim_key_selector", _("This field is required.")) + self.add_error("dkim_key_selector", _("This field is required.")) self.aliases = [] copied_data = cleaned_data.copy() for k in copied_data.keys(): @@ -177,12 +186,10 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): ) del cleaned_data[k] continue - label = lib.check_if_domain_exists( - cleaned_data[k], [(Domain, _("domain"))]) + label = lib.check_if_domain_exists(cleaned_data[k], [(Domain, _("domain"))]) if label is not None: self.add_error( - k, _("A %s with this name already exists") - % force_str(label) + k, _("A %s with this name already exists") % force_str(label) ) else: self.aliases.append(cleaned_data[k]) @@ -198,7 +205,8 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): """ d = super(DomainFormGeneral, self).save(commit=False) core_signals.can_create_object.send( - sender=self.__class__, context=user, klass=Domain, instance=d) + sender=self.__class__, context=user, klass=Domain, instance=d + ) if not commit: return d d.save() @@ -209,18 +217,23 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): self.aliases.remove(dalias.name) if self.aliases: core_signals.can_create_object.send( - self.__class__, context=user, klass=DomainAlias, - count=len(self.aliases)) + self.__class__, context=user, klass=DomainAlias, count=len(self.aliases) + ) core_signals.can_create_object.send( - self.__class__, context=d, object_type="domain_aliases", - count=len(self.aliases)) + self.__class__, + context=d, + object_type="domain_aliases", + count=len(self.aliases), + ) for alias in self.aliases: if d.domainalias_set.filter(name=alias).exists(): continue options = {"creator": user} if domalias_post_create else {} - DomainAlias(name=alias, target=d, enabled=d.enabled).save( - **options) - if self.cleaned_data.get("enable_dkim") is False and d.dkim_private_key_path != "": + DomainAlias(name=alias, target=d, enabled=d.enabled).save(**options) + if ( + self.cleaned_data.get("enable_dkim") is False + and d.dkim_private_key_path != "" + ): d.dkim_private_key_path = "" d.save() return d @@ -232,9 +245,7 @@ class DomainFormOptions(forms.Form): create_dom_admin = YesNoField( label=gettext_lazy("Create a domain administrator"), initial=False, - help_text=gettext_lazy( - "Automatically create an administrator for this domain" - ) + help_text=gettext_lazy("Automatically create an administrator for this domain"), ) dom_admin_username = forms.CharField( @@ -244,40 +255,35 @@ class DomainFormOptions(forms.Form): "The administrator's name. Don't include the domain's name here, " "it will be automatically appended." ), - required=False + required=False, ) random_password = YesNoField( label=gettext_lazy("Random password"), initial=False, - help_text=gettext_lazy( - "Generate a random password for the administrator." - ), - required=False + help_text=gettext_lazy("Generate a random password for the administrator."), + required=False, ) with_mailbox = YesNoField( label=gettext_lazy("With a mailbox"), initial=True, - help_text=gettext_lazy( - "Create a mailbox for the administrator." - ), - required=False + help_text=gettext_lazy("Create a mailbox for the administrator."), + required=False, ) create_aliases = YesNoField( label=gettext_lazy("Create aliases"), initial=True, - help_text=gettext_lazy( - "Automatically create standard aliases for this domain" - ), - required=False + help_text=gettext_lazy("Automatically create standard aliases for this domain"), + required=False, ) def __init__(self, user, *args, **kwargs): super(DomainFormOptions, self).__init__(*args, **kwargs) results = core_signals.user_can_set_role.send( - sender=self.__class__, user=user, role="DomainAdmins") + sender=self.__class__, user=user, role="DomainAdmins" + ) if False in [result[1] for result in results]: self.fields = {} return @@ -293,11 +299,9 @@ class DomainFormOptions(forms.Form): cleaned_data = super(DomainFormOptions, self).clean() if cleaned_data.get("create_dom_admin"): if not cleaned_data.get("dom_admin_username"): - self.add_error( - "dom_admin_username", _("This field is required.")) + self.add_error("dom_admin_username", _("This field is required.")) if "create_aliases" not in cleaned_data: - self.add_error( - "create_aliases", _("This field is required.")) + self.add_error("create_aliases", _("This field is required.")) return cleaned_data def save(self, *args, **kwargs): @@ -307,22 +311,19 @@ class DomainFormOptions(forms.Form): return user = kwargs.pop("user") domain = kwargs.pop("domain") - username = "%s@%s" % ( - self.cleaned_data["dom_admin_username"], domain.name) + username = "%s@%s" % (self.cleaned_data["dom_admin_username"], domain.name) try: da = User.objects.get(username=username) except User.DoesNotExist: pass else: raise Conflict(_("User '%s' already exists") % username) - core_signals.can_create_object.send( - self.__class__, context=user, klass=Mailbox) + core_signals.can_create_object.send(self.__class__, context=user, klass=Mailbox) da = User(username=username, email=username, is_active=True) if self.cleaned_data["random_password"]: password = lib.make_password() else: - password = param_tools.get_global_parameter( - "default_password", app="core") + password = param_tools.get_global_parameter("default_password", app="core") da.set_password(password) da.save() da.role = "DomainAdmins" @@ -331,24 +332,27 @@ class DomainFormOptions(forms.Form): if self.cleaned_data["with_mailbox"]: dom_admin_username = self.cleaned_data["dom_admin_username"] mb = Mailbox( - address=dom_admin_username, domain=domain, - user=da, use_domain_quota=True + address=dom_admin_username, + domain=domain, + user=da, + use_domain_quota=True, ) - mb.set_quota( - override_rules=user.has_perm("admin.change_domain")) + mb.set_quota(override_rules=user.has_perm("admin.change_domain")) mb.save(creator=user) condition = ( - domain.type == "domain" and - self.cleaned_data["create_aliases"] and - dom_admin_username != "postmaster" + domain.type == "domain" + and self.cleaned_data["create_aliases"] + and dom_admin_username != "postmaster" ) if condition: core_signals.can_create_object.send( - self.__class__, context=user, klass=Alias) - address = u"postmaster@{}".format(domain.name) + self.__class__, context=user, klass=Alias + ) + address = "postmaster@{}".format(domain.name) alias = Alias.objects.create( - address=address, domain=domain, enabled=True) + address=address, domain=domain, enabled=True + ) alias.set_recipients([mb.full_address]) alias.post_create(user) @@ -363,36 +367,37 @@ class DomainForm(TabForms): self.forms = [] self.domain = None if self.user.has_perm("admin.change_domain"): - self.forms.append({ - "id": "general", - "title": _("General"), - "formtpl": "admin/domain_general_form.html", - "cls": DomainFormGeneral, - "mandatory": True, - "new_args": [request.user] - }) + self.forms.append( + { + "id": "general", + "title": _("General"), + "formtpl": "admin/domain_general_form.html", + "cls": DomainFormGeneral, + "mandatory": True, + "new_args": [request.user], + } + ) cbargs = {"user": self.user} if "instances" in kwargs: self.domain = kwargs["instances"]["general"] cbargs["domain"] = self.domain - results = signals.extra_domain_forms.send( - sender=self.__class__, **cbargs) - self.forms += reduce( - lambda a, b: a + b, [result[1] for result in results]) + results = signals.extra_domain_forms.send(sender=self.__class__, **cbargs) + self.forms += reduce(lambda a, b: a + b, [result[1] for result in results]) if not self.forms: self.active_id = "admins" super(DomainForm, self).__init__(request, *args, **kwargs) def extra_context(self, context): """Add information to template context.""" - context.update({ - "title": self.domain.name, - "action": reverse( - "admin:domain_change", args=[self.domain.pk]), - "formid": "domform", - "domain": self.domain - }) + context.update( + { + "title": self.domain.name, + "action": reverse("admin:domain_change", args=[self.domain.pk]), + "formid": "domform", + "domain": self.domain, + } + ) def is_valid(self): """Custom validation. @@ -417,8 +422,7 @@ class DomainForm(TabForms): first_form = self.forms[0]["instance"] options = {} if isinstance(first_form, DomainFormGeneral): - domain = first_form.save( - self.request.user, domalias_post_create=True) + domain = first_form.save(self.request.user, domalias_post_create=True) options.update({"domain": domain}) else: first_form.save(self.request.user) @@ -436,9 +440,11 @@ class DomainWizard(WizardForm): super(DomainWizard, self).__init__(request) self.add_step( WizardStep( - "general", DomainFormGeneral, _("General"), + "general", + DomainFormGeneral, + _("General"), "admin/domain_general_form.html", - [request.user] + [request.user], ) ) results = signals.extra_domain_wizard_steps.send(sender=self.__class__) @@ -447,18 +453,22 @@ class DomainWizard(WizardForm): self.add_step(step) self.add_step( WizardStep( - "options", DomainFormOptions, _("Options"), + "options", + DomainFormOptions, + _("Options"), "admin/domain_options_form.html", - [self.request.user] + [self.request.user], ) ) def extra_context(self, context): - context.update({ - "title": _("New domain"), - "action": reverse("admin:domain_add"), - "formid": "domform" - }) + context.update( + { + "title": _("New domain"), + "action": reverse("admin:domain_add"), + "formid": "domform", + } + ) def done(self): genform = self.first_step.form diff --git a/modoboa/admin/forms/export.py b/modoboa/admin/forms/export.py index 786a726e4..ba0863664 100644 --- a/modoboa/admin/forms/export.py +++ b/modoboa/admin/forms/export.py @@ -9,7 +9,7 @@ class ExportDataForm(forms.Form): label=gettext_lazy("Separator"), max_length=1, required=False, - widget=forms.TextInput(attrs={"class": "form-control"}) + widget=forms.TextInput(attrs={"class": "form-control"}), ) def __init__(self, *args, **kwargs): diff --git a/modoboa/admin/forms/forward.py b/modoboa/admin/forms/forward.py index 8308d2f2a..3d5af289e 100644 --- a/modoboa/admin/forms/forward.py +++ b/modoboa/admin/forms/forward.py @@ -12,14 +12,14 @@ class ForwardForm(forms.Form): label=gettext_lazy("Recipient(s)"), widget=forms.Textarea(attrs={"class": "form-control"}), required=False, - help_text=gettext_lazy( - "Indicate one or more recipients separated by a ','") + help_text=gettext_lazy("Indicate one or more recipients separated by a ','"), ) keepcopies = forms.BooleanField( label=gettext_lazy("Keep local copies"), required=False, help_text=gettext_lazy( - "Forward messages and store copies into your local mailbox") + "Forward messages and store copies into your local mailbox" + ), ) def clean_dest(self): diff --git a/modoboa/admin/forms/import_.py b/modoboa/admin/forms/import_.py index acdb1f3fb..a038d1e35 100644 --- a/modoboa/admin/forms/import_.py +++ b/modoboa/admin/forms/import_.py @@ -5,7 +5,6 @@ from django.utils.translation import gettext_lazy class ImportDataForm(forms.Form): - """Base form to import objects.""" sourcefile = forms.FileField(label=gettext_lazy("Select a file")) @@ -13,11 +12,12 @@ class ImportDataForm(forms.Form): label=gettext_lazy("Separator"), max_length=1, required=False, - widget=forms.TextInput(attrs={"class": "form-control"}) + widget=forms.TextInput(attrs={"class": "form-control"}), ) continue_if_exists = forms.BooleanField( - label=gettext_lazy("Continue on error"), required=False, - help_text=gettext_lazy("Don't treat duplicated objects as error") + label=gettext_lazy("Continue on error"), + required=False, + help_text=gettext_lazy("Don't treat duplicated objects as error"), ) def __init__(self, *args, **kwargs): @@ -31,13 +31,12 @@ class ImportDataForm(forms.Form): class ImportIdentitiesForm(ImportDataForm): - """A form to import identities.""" crypt_password = forms.BooleanField( - label=gettext_lazy("Crypt passwords"), required=False, + label=gettext_lazy("Crypt passwords"), + required=False, help_text=gettext_lazy( - "Check this option if passwords contained in your file " - "are not crypted" - ) + "Check this option if passwords contained in your file " "are not crypted" + ), ) diff --git a/modoboa/admin/handlers.py b/modoboa/admin/handlers.py index 4a6fcf15b..a3f402e4b 100644 --- a/modoboa/admin/handlers.py +++ b/modoboa/admin/handlers.py @@ -23,18 +23,20 @@ def update_domain_mxs_and_mailboxes(sender, instance, **kwargs): if kwargs.get("created"): return instance.mailbox_set.filter(use_domain_quota=True).update( - quota=instance.default_mailbox_quota) + quota=instance.default_mailbox_quota + ) if instance.old_mail_homes is None: return - qset = ( - models.Quota.objects.filter( - username__contains="@{}".format(instance.oldname)) + qset = models.Quota.objects.filter( + username__contains="@{}".format(instance.oldname) ) for q in qset: username = q.username.replace( - "@{}".format(instance.oldname), "@{}".format(instance.name)) + "@{}".format(instance.oldname), "@{}".format(instance.name) + ) models.Quota.objects.create( - username=username, bytes=q.bytes, messages=q.messages) + username=username, bytes=q.bytes, messages=q.messages + ) q.delete() for mb in instance.mailbox_set.all(): mb.rename_dir(instance.old_mail_homes[mb.pk]) @@ -42,8 +44,7 @@ def update_domain_mxs_and_mailboxes(sender, instance, **kwargs): # .update(address=Concat("local_part", Value(instance.name))) # if the local_part was stored aside the address... for alias in instance.alias_set.all(): - alias.address = "{}@{}".format( - alias.address.split("@", 1)[0], instance.name) + alias.address = "{}@{}".format(alias.address.split("@", 1)[0], instance.name) alias.save(update_fields=["address"]) @@ -51,11 +52,8 @@ def update_domain_mxs_and_mailboxes(sender, instance, **kwargs): def create_dkim_key(sender, instance, **kwargs): if not instance.enable_dkim: return - queue = django_rq.get_queue('dkim') - queue.enqueue(call_command, - "modo", - "manage_dkim_keys", - f"--domain={instance.name}") + queue = django_rq.get_queue("dkim") + queue.enqueue(call_command, "modo", "manage_dkim_keys", f"--domain={instance.name}") @receiver(signals.post_save, sender=models.DomainAlias) @@ -64,16 +62,17 @@ def create_alias_for_domainalias(sender, instance, **kwargs): if not kwargs.get("created"): return alias = models.Alias.objects.create( - address=u"@{}".format(instance.name), enabled=True, internal=True) + address="@{}".format(instance.name), enabled=True, internal=True + ) models.AliasRecipient.objects.create( - address=u"@{}".format(instance.target.name), alias=alias) + address="@{}".format(instance.target.name), alias=alias + ) @receiver(signals.post_delete, sender=models.DomainAlias) def remove_alias_for_domainalias(sender, instance, **kwargs): """Remove the alias associated to domain alias.""" - models.Alias.objects.filter( - address=u"@{}".format(instance.name)).delete() + models.Alias.objects.filter(address="@{}".format(instance.name)).delete() @receiver(signals.post_save, sender=models.Mailbox) @@ -84,20 +83,22 @@ def manage_alias_for_mailbox(sender, instance, **kwargs): address=instance.full_address, domain=instance.domain, internal=True, - defaults={ - "enabled": instance.user.enabled - } + defaults={"enabled": instance.user.enabled}, ) models.AliasRecipient.objects.create( - address=instance.full_address, alias=alias, r_mailbox=instance) + address=instance.full_address, alias=alias, r_mailbox=instance + ) return old_address = getattr(instance, "old_full_address", None) if old_address is None or old_address == instance.full_address: return # Update old self alias alr = models.AliasRecipient.objects.get( - alias__address=old_address, address=old_address, - r_mailbox=instance, alias__internal=True) + alias__address=old_address, + address=old_address, + r_mailbox=instance, + alias__internal=True, + ) alr.address = instance.full_address alr.save() alr.alias.address = instance.full_address @@ -106,7 +107,8 @@ def manage_alias_for_mailbox(sender, instance, **kwargs): # Update AliasRecipient instances instance.aliasrecipient_set.filter(alias__internal=False).update( - address=instance.full_address) + address=instance.full_address + ) @receiver(signals.pre_delete, sender=models.Mailbox) @@ -132,7 +134,8 @@ def mailbox_deleted_handler(sender, **kwargs): request = lib_signals.get_request() if request: if not request.localconfig.parameters.get_value( - "handle_mailboxes", raise_exception=False): + "handle_mailboxes", raise_exception=False + ): return keepdir = request.POST.get("keepdir", "false") == "true" if keepdir: @@ -142,7 +145,8 @@ def mailbox_deleted_handler(sender, **kwargs): # Management command context localconfig = core_models.LocalConfig.objects.first() if not localconfig.parameters.get_value( - "handle_mailboxes", raise_exception=False): + "handle_mailboxes", raise_exception=False + ): return mb.delete_dir() @@ -150,17 +154,18 @@ def mailbox_deleted_handler(sender, **kwargs): @receiver(signals.post_delete, sender=models.Mailbox) def remove_alias_for_mailbox(sender, instance, **kwargs): """Remove "self alias" for this mailbox.""" - models.Alias.objects.filter( - address=instance.full_address).delete() + models.Alias.objects.filter(address=instance.full_address).delete() @receiver(core_signals.register_postfix_maps) def register_postfix_maps(sender, **kwargs): """Register admin map files.""" return [ - postfix_maps.DomainsMap, postfix_maps.DomainsAliasesMap, - postfix_maps.AliasesMap, postfix_maps.MaintainMap, - postfix_maps.SenderLoginMap + postfix_maps.DomainsMap, + postfix_maps.DomainsAliasesMap, + postfix_maps.AliasesMap, + postfix_maps.MaintainMap, + postfix_maps.SenderLoginMap, ] @@ -177,11 +182,11 @@ def account_auto_created(sender, user, **kwargs): domain = models.Domain.objects.get(name=domname) except models.Domain.DoesNotExist: label = lib.check_if_domain_exists( - domname, [(models.DomainAlias, _("domain alias"))]) + domname, [(models.DomainAlias, _("domain alias"))] + ) if label is not None: return - domain = models.Domain( - name=domname, enabled=True, default_mailbox_quota=0) + domain = models.Domain(name=domname, enabled=True, default_mailbox_quota=0) domain.save(creator=sadmins[0]) for su in sadmins[1:]: permissions.grant_access_to_object(su, domain) @@ -201,11 +206,10 @@ def account_auto_created(sender, user, **kwargs): @receiver(core_signals.account_exported) def export_admin_domains(sender, user, **kwargs): """Export administered domains too.""" - result = [user.mailbox.quota] if hasattr(user, "mailbox") else [u""] + result = [user.mailbox.quota] if hasattr(user, "mailbox") else [""] if user.role != "DomainAdmins": return result - return result + [ - dom.name for dom in models.Domain.objects.get_for_admin(user)] + return result + [dom.name for dom in models.Domain.objects.get_for_admin(user)] @receiver(core_signals.account_imported) @@ -232,14 +236,17 @@ def import_account_mailbox(sender, user, account, row, **kwargs): if not user.can_access(domain): raise exceptions.PermDeniedException core_signals.can_create_object.send( - sender="import", context=user, klass=models.Mailbox) + sender="import", context=user, klass=models.Mailbox + ) core_signals.can_create_object.send( - sender="import", context=domain, object_type="mailboxes") + sender="import", context=domain, object_type="mailboxes" + ) account.save() qset = models.Mailbox.objects.filter(address=mailbox, domain=domain) if qset.exists(): raise exceptions.Conflict( - _("Mailbox {} already exists").format(account.email)) + _("Mailbox {} already exists").format(account.email) + ) if len(row) == 1: quota = None else: @@ -252,11 +259,12 @@ def import_account_mailbox(sender, user, account, row, **kwargs): ) use_domain_quota = True if not quota else False mb = models.Mailbox( - address=mailbox, domain=domain, - user=account, use_domain_quota=use_domain_quota) - mb.set_quota( - quota, override_rules=user.has_perm("admin.change_domain") + address=mailbox, + domain=domain, + user=account, + use_domain_quota=use_domain_quota, ) + mb.set_quota(quota, override_rules=user.has_perm("admin.change_domain")) mb.save(creator=user) if account.role == "DomainAdmins": for domname in row[2:]: @@ -275,19 +283,20 @@ def admin_menu(sender, location, user, **kwargs): entries = [] if user.has_perm("admin.view_domain"): entries += [ - {"name": "domains", - "url": reverse("admin:domain_list"), - "label": _("Domains")} + { + "name": "domains", + "url": reverse("admin:domain_list"), + "label": _("Domains"), + } ] - conditions = ( - user.has_perm("core.add_user"), - user.has_perm("admin.add_alias") - ) + conditions = (user.has_perm("core.add_user"), user.has_perm("admin.add_alias")) if any(conditions): entries += [ - {"name": "identities", - "url": reverse("admin:identity_list"), - "label": _("Identities")}, + { + "name": "identities", + "url": reverse("admin:identity_list"), + "label": _("Identities"), + }, ] return entries @@ -300,10 +309,12 @@ def user_menu(sender, location, user, **kwargs): if not hasattr(user, "mailbox"): return [] return [ - {"name": "forward", - "class": "ajaxnav", - "url": "forward/", - "label": _("Forward")} + { + "name": "forward", + "class": "ajaxnav", + "url": "forward/", + "label": _("Forward"), + } ] @@ -321,12 +332,15 @@ def grant_access_to_all_objects(sender, account, role, **kwargs): if role != "SuperAdmins": return perm_models = [ - core_models.User, models.Domain, models.DomainAlias, models.Mailbox, - models.Alias] + core_models.User, + models.Domain, + models.DomainAlias, + models.Mailbox, + models.Alias, + ] for model in perm_models: permissions.grant_access_to_objects( - account, model.objects.all(), - ContentType.objects.get_for_model(model) + account, model.objects.all(), ContentType.objects.get_for_model(model) ) @@ -338,19 +352,15 @@ def add_widgets_to_admin_dashboard(sender, user, **kwargs): "domains_counter": models.Domain.objects.count(), "domain_aliases_counter": models.DomainAlias.objects.count(), "identities_counter": ( - core_models.User.objects.count() + - models.Alias.objects.filter(internal=False).count()), + core_models.User.objects.count() + + models.Alias.objects.filter(internal=False).count() + ), } template = "admin/_global_statistics_widget.html" else: - context = { - "domains": models.Domain.objects.get_for_admin(user)} + context = {"domains": models.Domain.objects.get_for_admin(user)} template = "admin/_per_domain_statistics_widget.html" - return [{ - "column": "left", - "template": template, - "context": context - }] + return [{"column": "left", "template": template, "context": context}] @receiver(admin_signals.import_object) diff --git a/modoboa/admin/lib.py b/modoboa/admin/lib.py index a1100cfc9..b81f9dd6f 100644 --- a/modoboa/admin/lib.py +++ b/modoboa/admin/lib.py @@ -25,9 +25,7 @@ from reversion import revisions as reversion from modoboa.core import signals as core_signals from modoboa.core.models import User -from modoboa.lib.exceptions import ( - Conflict, ModoboaException, PermDeniedException -) +from modoboa.lib.exceptions import Conflict, ModoboaException, PermDeniedException from modoboa.parameters import tools as param_tools from . import signals @@ -40,13 +38,16 @@ def needs_mailbox(): Some applications (the webmail for example) need a mailbox to work. """ + def decorator(f): @wraps(f) def wrapped_f(request, *args, **kwargs): if hasattr(request.user, "mailbox"): return f(request, *args, **kwargs) raise PermDeniedException(_("A mailbox is required")) + return wrapped_f + return decorator @@ -61,13 +62,12 @@ def get_identities(user, searchquery=None, idtfilter=None, grpfilter=None): """ accounts = [] if idtfilter is None or not idtfilter or idtfilter == "account": - ids = user.objectaccess_set \ - .filter(content_type=ContentType.objects.get_for_model(user)) \ - .values_list("object_id", flat=True) + ids = user.objectaccess_set.filter( + content_type=ContentType.objects.get_for_model(user) + ).values_list("object_id", flat=True) q = Q(pk__in=ids) if searchquery is not None: - q &= Q(username__icontains=searchquery) \ - | Q(email__icontains=searchquery) + q &= Q(username__icontains=searchquery) | Q(email__icontains=searchquery) if grpfilter is not None and grpfilter: if grpfilter == "SuperAdmins": q &= Q(is_superuser=True) @@ -76,16 +76,19 @@ def get_identities(user, searchquery=None, idtfilter=None, grpfilter=None): accounts = User.objects.filter(q).prefetch_related("groups") aliases = [] - if idtfilter is None or not idtfilter \ - or (idtfilter in ["alias", "forward", "dlist"]): + if ( + idtfilter is None + or not idtfilter + or (idtfilter in ["alias", "forward", "dlist"]) + ): alct = ContentType.objects.get_for_model(Alias) - ids = user.objectaccess_set.filter(content_type=alct) \ - .values_list("object_id", flat=True) + ids = user.objectaccess_set.filter(content_type=alct).values_list( + "object_id", flat=True + ) q = Q(pk__in=ids, internal=False) if searchquery is not None: - q &= ( - Q(address__icontains=searchquery) | - Q(domain__name__icontains=searchquery) + q &= Q(address__icontains=searchquery) | Q( + domain__name__icontains=searchquery ) aliases = Alias.objects.select_related("domain").filter(q) if idtfilter is not None and idtfilter: @@ -101,8 +104,7 @@ def get_domains(user, domfilter=None, searchquery=None, **extrafilters): :rtype: list :return: a list of domains and/or relay domains """ - domains = ( - Domain.objects.get_for_admin(user).prefetch_related("domainalias_set")) + domains = Domain.objects.get_for_admin(user).prefetch_related("domainalias_set") if domfilter: domains = domains.filter(type=domfilter) if searchquery is not None: @@ -110,7 +112,8 @@ def get_domains(user, domfilter=None, searchquery=None, **extrafilters): q |= Q(domainalias__name__contains=searchquery) domains = domains.filter(q).distinct() results = signals.extra_domain_qset_filters.send( - sender="get_domains", domfilter=domfilter, extrafilters=extrafilters) + sender="get_domains", domfilter=domfilter, extrafilters=extrafilters + ) if results: qset_filters = {} for result in results: @@ -136,8 +139,7 @@ def import_domain(user, row, formopts): """Specific code for domains import""" if not user.has_perm("admin.add_domain"): raise PermDeniedException(_("You are not allowed to import domains")) - core_signals.can_create_object.send( - sender="import", context=user, klass=Domain) + core_signals.can_create_object.send(sender="import", context=user, klass=Domain) dom = Domain() dom.from_csv(user, row) @@ -145,10 +147,10 @@ def import_domain(user, row, formopts): def import_domainalias(user, row, formopts): """Specific code for domain aliases import""" if not user.has_perm("admin.add_domainalias"): - raise PermDeniedException( - _("You are not allowed to import domain aliases.")) + raise PermDeniedException(_("You are not allowed to import domain aliases.")) core_signals.can_create_object.send( - sender="import", context=user, klass=DomainAlias) + sender="import", context=user, klass=DomainAlias + ) domalias = DomainAlias() domalias.from_csv(user, row) @@ -200,14 +202,15 @@ def get_dns_records(name, typ, resolver=None): except dns.resolver.NoAnswer as e: logger.error( _("No %(type)s record for %(name)s") % {"type": typ, "name": name}, - exc_info=e + exc_info=e, ) except dns.resolver.NoNameservers as e: logger.error(_("No working name servers found"), exc_info=e) except dns.resolver.Timeout as e: logger.warning( - _("DNS resolution timeout, unable to query %s at the moment") % - name, exc_info=e) + _("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: @@ -225,7 +228,8 @@ def get_domain_mx_list(domain): return result for dns_answer in dns_answers: mx_domain = dns_answer.exchange.to_unicode( - omit_final_dot=True, idna_codec=IDNA_2008_UTS_46) + omit_final_dot=True, idna_codec=IDNA_2008_UTS_46 + ) rtypes = ["A"] if param_tools.get_global_parameter("enable_ipv6_mx_checks", app="admin"): rtypes.append("AAAA") @@ -239,11 +243,11 @@ def get_domain_mx_list(domain): mx_ip = ipaddress.ip_address(address_smart) except ValueError as e: logger.warning( - _("Invalid IP address format for " - "{domain}; {addr}").format( - domain=mx_domain, - addr=smart_str(ip_answer.address) - ), exc_info=e) + _("Invalid IP address format for " "{domain}; {addr}").format( + domain=mx_domain, addr=smart_str(ip_answer.address) + ), + exc_info=e, + ) else: result.append((mx_domain, mx_ip)) return result @@ -252,8 +256,11 @@ def get_domain_mx_list(domain): def domain_has_authorized_mx(name): """Check if domain has authorized mx record at least.""" valid_mxs = param_tools.get_global_parameter("valid_mxs") - valid_mxs = [ipaddress.ip_network(smart_str(v.strip())) - for v in valid_mxs.split() if v.strip()] + valid_mxs = [ + ipaddress.ip_network(smart_str(v.strip())) + for v in valid_mxs.split() + if v.strip() + ] domain_mxs = get_domain_mx_list(name) for _mx_addr, mx_ip_addr in domain_mxs: for subnet in valid_mxs: @@ -264,13 +271,12 @@ def domain_has_authorized_mx(name): def make_password(): """Create a random password.""" - length = int( - param_tools.get_global_parameter("random_password_length", app="core") - ) + length = int(param_tools.get_global_parameter("random_password_length", app="core")) while True: password = "".join( - random.SystemRandom().choice( - string.ascii_letters + string.digits) for _ in range(length)) + random.SystemRandom().choice(string.ascii_letters + string.digits) + for _ in range(length) + ) try: password_validation.validate_password(password) except ValidationError: @@ -297,7 +303,8 @@ def import_data(user, file_object, options: dict): if not row: continue fct = signals.import_object.send( - sender="importdata", objtype=row[0].strip()) + sender="importdata", objtype=row[0].strip() + ) fct = [func for x_, func in fct if func is not None] if not fct: continue @@ -315,6 +322,6 @@ def import_data(user, file_object, options: dict): cpt += 1 msg = _("%d objects imported successfully") % cpt return True, msg - except (ModoboaException) as e: + except ModoboaException as e: error = str(e) return False, error diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py index b7d9c3b99..bfa527c76 100644 --- a/modoboa/admin/management/commands/handle_mailbox_operations.py +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -31,8 +31,9 @@ class Command(BaseCommand): def add_arguments(self, parser): """Add extra arguments to command line.""" parser.add_argument( - "--pidfile", default="/tmp/handle_mailbox_operations.pid", - help="Path to the file that will contain the PID of this process" + "--pidfile", + default="/tmp/handle_mailbox_operations.pid", + help="Path to the file that will contain the PID of this process", ) def rename_mailbox(self, operation): @@ -45,9 +46,7 @@ class Command(BaseCommand): os.makedirs(dirname) except os.error as e: raise OperationError(str(e)) - code, output = exec_cmd( - "mv %s %s" % (operation.argument, new_mail_home) - ) + code, output = exec_cmd("mv %s %s" % (operation.argument, new_mail_home)) if code: raise OperationError(output) @@ -58,8 +57,7 @@ class Command(BaseCommand): def onerror(function, path, excinfo): """Handle errors.""" - self.logger.critical( - "delete failed (reason: {})".format(excinfo)) + self.logger.critical("delete failed (reason: {})".format(excinfo)) shutil.rmtree(operation.argument, False, onerror) @@ -97,7 +95,8 @@ class Command(BaseCommand): f(ope) except (OperationError, InternalError) as e: self.logger.critical( - "%s failed (reason: %s)", ope, str(e).decode("utf-8")) + "%s failed (reason: %s)", ope, str(e).decode("utf-8") + ) else: self.logger.info("%s succeed", ope) ope.delete() diff --git a/modoboa/admin/management/commands/modo.py b/modoboa/admin/management/commands/modo.py index 295b09062..a5ce79284 100644 --- a/modoboa/admin/management/commands/modo.py +++ b/modoboa/admin/management/commands/modo.py @@ -26,9 +26,7 @@ class Command(BaseCommand): } def add_arguments(self, parser): - subparsers = parser.add_subparsers( - dest='subcommand', title='subcommands' - ) + subparsers = parser.add_subparsers(dest="subcommand", title="subcommands") # required argument is added in Python 3.7 subparsers.required = True @@ -36,9 +34,7 @@ class Command(BaseCommand): command = command_class(self.stdout, self.stderr) command.style = self.style - subparser = subparsers.add_parser( - command_name, help=command_class.help - ) + subparser = subparsers.add_parser(command_name, help=command_class.help) # This is needed to output console friendly errors subparser.called_from_command_line = self._called_from_command_line @@ -47,4 +43,4 @@ class Command(BaseCommand): subparser.set_defaults(command=command) def execute(self, *args, **options): - return options.pop('command').execute(*args, **options) + return options.pop("command").execute(*args, **options) diff --git a/modoboa/admin/management/commands/subcommands/_export.py b/modoboa/admin/management/commands/subcommands/_export.py index 694035adb..a01df74b2 100644 --- a/modoboa/admin/management/commands/subcommands/_export.py +++ b/modoboa/admin/management/commands/subcommands/_export.py @@ -18,12 +18,18 @@ class ExportCommand(BaseCommand): def add_arguments(self, parser): """Add arguments to command.""" parser.add_argument( - "--sepchar", type=str, dest="sepchar", default=";", - help="Separator used in generated file." + "--sepchar", + type=str, + dest="sepchar", + default=";", + help="Separator used in generated file.", ) parser.add_argument( - "objtype", type=str, choices=["domains", "identities"], - help="The type of object to export (domains or identities)") + "objtype", + type=str, + choices=["domains", "identities"], + help="The type of object to export (domains or identities)", + ) def export_domains(self): """Export all domains.""" @@ -39,7 +45,8 @@ class ExportCommand(BaseCommand): qset = ( models.Alias.objects.filter(internal=False) .exclude(alias_recipient_aliases=None) - .distinct().prefetch_related("aliasrecipient_set") + .distinct() + .prefetch_related("aliasrecipient_set") ) for alias in qset: alias.to_csv(self.csvwriter) @@ -56,5 +63,6 @@ class ExportCommand(BaseCommand): def handle(self, *args, **options): exts_pool.load_all() self.csvwriter = csv.writer( - self.stdout, delimiter=smart_str(options["sepchar"])) + self.stdout, delimiter=smart_str(options["sepchar"]) + ) getattr(self, "export_{}".format(options["objtype"]))() diff --git a/modoboa/admin/management/commands/subcommands/_import.py b/modoboa/admin/management/commands/subcommands/_import.py index 1f1e57e8b..0be4e2575 100644 --- a/modoboa/admin/management/commands/subcommands/_import.py +++ b/modoboa/admin/management/commands/subcommands/_import.py @@ -27,33 +27,33 @@ class ImportCommand(BaseCommand): def add_arguments(self, parser): """Add extra arguments to command.""" parser.add_argument( - "--sepchar", type=str, default=";", - help="Separator used in file.") + "--sepchar", type=str, default=";", help="Separator used in file." + ) parser.add_argument( - "--continue-if-exists", action="store_true", - dest="continue_if_exists", default=False, - help="Continue even if an entry already exists.") + "--continue-if-exists", + action="store_true", + dest="continue_if_exists", + default=False, + help="Continue even if an entry already exists.", + ) parser.add_argument( - "--crypt-password", action="store_true", - default=False, help="Encrypt provided passwords.") - parser.add_argument( - "files", type=str, nargs="+", help="CSV files to import.") + "--crypt-password", + action="store_true", + default=False, + help="Encrypt provided passwords.", + ) + parser.add_argument("files", type=str, nargs="+", help="CSV files to import.") def _import(self, filename, options, encoding="utf-8"): """Import domains or identities.""" - superadmin = ( - core_models.User.objects.filter(is_superuser=True).first() - ) + superadmin = core_models.User.objects.filter(is_superuser=True).first() if not os.path.isfile(filename): raise CommandError("File not found") - num_lines = sum( - 1 for line in io.open(filename, encoding=encoding) if line - ) + num_lines = sum(1 for line in io.open(filename, encoding=encoding) if line) pbar = progressbar.ProgressBar( - widgets=[ - progressbar.Percentage(), progressbar.Bar(), progressbar.ETA() - ], maxval=num_lines + widgets=[progressbar.Percentage(), progressbar.Bar(), progressbar.ETA()], + maxval=num_lines, ).start() with io.open(filename, encoding=encoding, newline="") as f: reader = csv.reader(f, delimiter=options["sepchar"]) @@ -62,7 +62,8 @@ class ImportCommand(BaseCommand): if not row: continue fct = signals.import_object.send( - sender=self.__class__, objtype=row[0].strip()) + sender=self.__class__, objtype=row[0].strip() + ) fct = [func for x_, func in fct if func is not None] if not fct: continue @@ -74,7 +75,9 @@ class ImportCommand(BaseCommand): continue raise CommandError( "Object already exists at line {}: {}".format( - i+1, options["sepchar"].join(row[:2]))) + i + 1, options["sepchar"].join(row[:2]) + ) + ) i += 1 pbar.update(i) @@ -90,10 +93,14 @@ class ImportCommand(BaseCommand): except CommandError as exc: raise exc except UnicodeDecodeError: - self.stdout.write(self.style.NOTICE( - _("CSV file is not encoded in UTF-8, attempting to guess " - "encoding") - )) + self.stdout.write( + self.style.NOTICE( + _( + "CSV file is not encoded in UTF-8, attempting to guess " + "encoding" + ) + ) + ) detector = UniversalDetector() with io.open(filename, "rb") as fp: for line in fp: @@ -102,18 +109,19 @@ class ImportCommand(BaseCommand): break detector.close() - self.stdout.write(self.style.NOTICE( - _("Reading CSV file using %(encoding)s encoding") % - detector.result - )) + self.stdout.write( + self.style.NOTICE( + _("Reading CSV file using %(encoding)s encoding") + % detector.result + ) + ) try: with transaction.atomic(): self._import( - filename, options, - encoding=detector.result["encoding"] + filename, options, encoding=detector.result["encoding"] ) except UnicodeDecodeError as exc: raise CommandError( - _("Unable to decode CSV file using %(encoding)s " - "encoding") % detector.result + _("Unable to decode CSV file using %(encoding)s " "encoding") + % detector.result ) from exc diff --git a/modoboa/admin/management/commands/subcommands/_manage_dkim_keys.py b/modoboa/admin/management/commands/subcommands/_manage_dkim_keys.py index e528809ed..7f5a61275 100644 --- a/modoboa/admin/management/commands/subcommands/_manage_dkim_keys.py +++ b/modoboa/admin/management/commands/subcommands/_manage_dkim_keys.py @@ -25,8 +25,8 @@ class ManageDKIMKeys(BaseCommand): if not os.access(storage_dir, os.W_OK): if not alarm_qset.exists(): domain.alarms.create( - title=_("DKIM path non-writable"), - internal_name=DKIM_WRITE_ERROR) + title=_("DKIM path non-writable"), internal_name=DKIM_WRITE_ERROR + ) else: alarm = alarm_qset.first() if alarm.status != ALARM_OPENED: @@ -35,19 +35,27 @@ class ManageDKIMKeys(BaseCommand): elif alarm_qset.exists(): alarm_qset.first().close() key_size = ( - domain.dkim_key_length if domain.dkim_key_length - else self.default_key_length) + domain.dkim_key_length + if domain.dkim_key_length + else self.default_key_length + ) code, output = sysutils.exec_cmd( - "openssl genrsa -out {} {}".format(pkey_path, key_size)) + "openssl genrsa -out {} {}".format(pkey_path, key_size) + ) if code: - print("Failed to generate DKIM private key for domain {}: {}" - .format(domain.name, smart_str(output))) + print( + "Failed to generate DKIM private key for domain {}: {}".format( + domain.name, smart_str(output) + ) + ) domain.dkim_private_key_path = pkey_path - code, output = sysutils.exec_cmd( - "openssl rsa -in {} -pubout".format(pkey_path)) + code, output = sysutils.exec_cmd("openssl rsa -in {} -pubout".format(pkey_path)) if code: - print("Failed to generate DKIM public key for domain {}: {}" - .format(domain.name, smart_str(output))) + print( + "Failed to generate DKIM public key for domain {}: {}".format( + domain.name, smart_str(output) + ) + ) public_key = "" for cpt, line in enumerate(smart_str(output).splitlines()): if cpt == 0 or line.startswith("-----"): @@ -59,24 +67,27 @@ class ManageDKIMKeys(BaseCommand): def add_arguments(self, parser): """Add arguments to command.""" parser.add_argument( - "--domain", type=str, dest="domain", default="", - help="Domain target for keys generation." + "--domain", + type=str, + dest="domain", + default="", + help="Domain target for keys generation.", ) def handle(self, *args, **options): """Entry point.""" self.default_key_length = param_tools.get_global_parameter( - "dkim_default_key_length") + "dkim_default_key_length" + ) if options["domain"] != "": - domain = models.Domain.objects.filter(name=options["domain"], - enable_dkim=True, - dkim_private_key_path="") + domain = models.Domain.objects.filter( + name=options["domain"], enable_dkim=True, dkim_private_key_path="" + ) if domain.exists(): self.create_new_dkim_key(domain[0]) return - qset = models.Domain.objects.filter( - enable_dkim=True, dkim_private_key_path="") + qset = models.Domain.objects.filter(enable_dkim=True, dkim_private_key_path="") for domain in qset: self.create_new_dkim_key(domain) diff --git a/modoboa/admin/management/commands/subcommands/_mx.py b/modoboa/admin/management/commands/subcommands/_mx.py index 41fd9a349..a2d365da1 100644 --- a/modoboa/admin/management/commands/subcommands/_mx.py +++ b/modoboa/admin/management/commands/subcommands/_mx.py @@ -40,30 +40,41 @@ class CheckMXRecords(BaseCommand): def valid_mxs(self): """Return valid MXs set in admin.""" valid_mxs = param_tools.get_global_parameter("valid_mxs") - return [ipaddress.ip_network(smart_str(v.strip())) - for v in valid_mxs.split() if v.strip()] + return [ + ipaddress.ip_network(smart_str(v.strip())) + for v in valid_mxs.split() + if v.strip() + ] def add_arguments(self, parser): """Add extra arguments to command.""" parser.add_argument( - "--no-dnsbl", action="store_true", default=False, - help="Skip DNSBL queries.") + "--no-dnsbl", action="store_true", default=False, help="Skip DNSBL queries." + ) parser.add_argument( - "--email", type=str, action="append", default=[], - help="One or more email to notify") + "--email", + type=str, + action="append", + default=[], + help="One or more email to notify", + ) parser.add_argument( - "--skip-admin-emails", action="store_true", + "--skip-admin-emails", + action="store_true", default=False, - help="Skip domain's admins email notification.") + help="Skip domain's admins email notification.", + ) parser.add_argument( - "--domain", type=str, action="append", default=[], - help="Domain name or id to update.") + "--domain", + type=str, + action="append", + default=[], + help="Domain name or id to update.", + ) parser.add_argument( - "--timeout", type=int, default=3, - help="Timeout used for queries.") - parser.add_argument( - "--ttl", type=int, default=7200, - help="TTL for dns query.") + "--timeout", type=int, default=3, help="Timeout used for queries." + ) + parser.add_argument("--ttl", type=int, default=7200, help="TTL for dns query.") def query_dnsbl(self, mx_list, provider): """Check given IP against given DNSBL provider.""" @@ -79,10 +90,10 @@ class CheckMXRecords(BaseCommand): pattern = "{}.{}.".format(reverse, provider) try: result = socket.gethostbyname(pattern) - #result from dnsbl is in ipv4 format + # result from dnsbl is in ipv4 format splited_result = result.split(".") if int(splited_result[-1]) > 15: - #Typical dnsbl result : 127.0.0.[1-15] (depends on services) + # Typical dnsbl result : 127.0.0.[1-15] (depends on services) result = False except socket.gaierror: result = False @@ -101,13 +112,14 @@ class CheckMXRecords(BaseCommand): if not result: result = "" dnsbl_result = models.DNSBLResult.objects.filter( - domain=domain, provider=provider, mx=mx).first() + domain=domain, provider=provider, mx=mx + ).first() trigger = False if dnsbl_result is None: to_create.append( models.DNSBLResult( - domain=domain, provider=provider, mx=mx, - status=result) + domain=domain, provider=provider, mx=mx, status=result + ) ) if result: trigger = True @@ -118,22 +130,15 @@ class CheckMXRecords(BaseCommand): trigger = True alarm_name = "domain_mx_in_dnsbl_{}".format(provider) if trigger: - title = _("MX {} listed by DNSBL provider {}").format( - mx.name, provider) + title = _("MX {} listed by DNSBL provider {}").format(mx.name, provider) domain.alarms.create( - internal_name=alarm_name, - status=constants.ALARM_OPENED, - title=title + internal_name=alarm_name, status=constants.ALARM_OPENED, title=title ) alerts.append((provider, mx.name)) else: domain.alarms.filter( - internal_name=alarm_name, - status=constants.ALARM_OPENED - ).update( - status=constants.ALARM_CLOSED, - closed=timezone.now() - ) + internal_name=alarm_name, status=constants.ALARM_OPENED + ).update(status=constants.ALARM_CLOSED, closed=timezone.now()) models.DNSBLResult.objects.bulk_create(to_create) return alerts @@ -142,8 +147,9 @@ class CheckMXRecords(BaseCommand): emails = list(options["email"]) if not options["skip_admin_emails"]: emails.extend( - domain.admins.exclude(mailbox__isnull=True) - .values_list("email", flat=True) + domain.admins.exclude(mailbox__isnull=True).values_list( + "email", flat=True + ) ) if not len(emails): return @@ -159,24 +165,21 @@ class CheckMXRecords(BaseCommand): """ alerts = [] check = False - mxs = [(mx, ipaddress.ip_address("%s" % mx.address)) - for mx in mx_list] + mxs = [(mx, ipaddress.ip_address("%s" % mx.address)) for mx in mx_list] valid_mxs = self.valid_mxs if not mxs: alarm, created = domain.alarms.get_or_create( internal_name="domain_has_no_mx", status=constants.ALARM_OPENED, - defaults={"title": _("Domain has no MX record")} + defaults={"title": _("Domain has no MX record")}, ) if created: - alerts.append( - _("Domain {} has no MX record").format(domain.name)) + alerts.append(_("Domain {} has no MX record").format(domain.name)) else: # Close opened alarm domain.alarms.filter( - internal_name="domain_has_no_mx", - status=constants.ALARM_OPENED).update( - status=constants.ALARM_CLOSED, closed=timezone.now()) + internal_name="domain_has_no_mx", status=constants.ALARM_OPENED + ).update(status=constants.ALARM_CLOSED, closed=timezone.now()) if valid_mxs: for subnet in valid_mxs: for mx, addr in mxs: @@ -184,67 +187,62 @@ class CheckMXRecords(BaseCommand): mx.managed = check = True mx.save() if check is False: - mx_names = [ - "{0.name} ({0.address})".format(mx) for mx in mx_list] + mx_names = ["{0.name} ({0.address})".format(mx) for mx in mx_list] alarm, created = domain.alarms.get_or_create( internal_name="domain_invalid_mx", status=constants.ALARM_OPENED, defaults={ "title": _("Invalid MX record: {}").format( - ", ".join(mx_names)) - } + ", ".join(mx_names) + ) + }, ) if created: alerts.append( - _("MX record for domain {0} is invalid: {1}") - .format(domain, ", ".join(mx_names)) + _("MX record for domain {0} is invalid: {1}").format( + domain, ", ".join(mx_names) + ) ) else: # Close opened alarm domain.alarms.filter( - internal_name="domain_invalid_mx", - status=constants.ALARM_OPENED - ).update( - status=constants.ALARM_CLOSED, - closed=timezone.now() - ) + internal_name="domain_invalid_mx", status=constants.ALARM_OPENED + ).update(status=constants.ALARM_CLOSED, closed=timezone.now()) if not alerts: return - subject = _("[modoboa] MX issue(s) for domain {}").format( - domain.name) + subject = _("[modoboa] MX issue(s) for domain {}").format(domain.name) tpl = "admin/notifications/domain_invalid_mx.html" self.send_alert_notifications(domain, alerts, subject, tpl, **options) def check_domain(self, domain, timeout=3, ttl=7200, **options): """Check specified domain.""" - mx_list = list( - models.MXRecord.objects.get_or_create_for_domain(domain, ttl)) + mx_list = list(models.MXRecord.objects.get_or_create_for_domain(domain, ttl)) if param_tools.get_global_parameter("enable_mx_checks"): self.check_valid_mx(domain, mx_list, **options) if param_tools.get_global_parameter("enable_spf_checks"): - dns_models.DNSRecord.objects.get_or_create_for_domain( - domain, "spf", ttl) + dns_models.DNSRecord.objects.get_or_create_for_domain(domain, "spf", ttl) condition = ( - param_tools.get_global_parameter("enable_dkim_checks") and - domain.dkim_public_key + param_tools.get_global_parameter("enable_dkim_checks") + and domain.dkim_public_key ) if condition: - dns_models.DNSRecord.objects.get_or_create_for_domain( - domain, "dkim", ttl) + dns_models.DNSRecord.objects.get_or_create_for_domain(domain, "dkim", ttl) if param_tools.get_global_parameter("enable_dmarc_checks"): - dns_models.DNSRecord.objects.get_or_create_for_domain( - domain, "dmarc", ttl) + dns_models.DNSRecord.objects.get_or_create_for_domain(domain, "dmarc", ttl) if param_tools.get_global_parameter("enable_autoconfig_checks"): dns_models.DNSRecord.objects.get_or_create_for_domain( - domain, "autoconfig", ttl) + domain, "autoconfig", ttl + ) dns_models.DNSRecord.objects.get_or_create_for_domain( - domain, "autodiscover", ttl) + domain, "autodiscover", ttl + ) condition = ( - not param_tools.get_global_parameter("enable_dnsbl_checks") or - options["no_dnsbl"] is True) + not param_tools.get_global_parameter("enable_dnsbl_checks") + or options["no_dnsbl"] is True + ) if condition or not mx_list: return @@ -257,27 +255,25 @@ class CheckMXRecords(BaseCommand): jobs = [ gevent.spawn(self.query_dnsbl, mx_by_ip, provider) - for provider in self.providers] + for provider in self.providers + ] gevent.joinall(jobs, timeout) alerts = [] for job in jobs: if not job.successful(): continue provider, results = job.value - alerts += self.store_dnsbl_result( - domain, provider, results, **options) + alerts += self.store_dnsbl_result(domain, provider, results, **options) if not alerts: return - subject = _("[modoboa] DNSBL issue(s) for domain {}").format( - domain.name) + subject = _("[modoboa] DNSBL issue(s) for domain {}").format(domain.name) tpl = "admin/notifications/domain_in_dnsbl.html" self.send_alert_notifications(domain, alerts, subject, tpl, **options) def handle(self, *args, **options): """Command entry point.""" # Remove deprecated records first - models.DNSBLResult.objects.exclude( - provider__in=self.providers).delete() + models.DNSBLResult.objects.exclude(provider__in=self.providers).delete() if options["domain"]: domains = [] @@ -290,8 +286,7 @@ class CheckMXRecords(BaseCommand): except models.Domain.DoesNotExist: pass else: - domains = models.Domain.objects.filter( - enabled=True, enable_dns_checks=True) + domains = models.Domain.objects.filter(enabled=True, enable_dns_checks=True) options.pop("domain") diff --git a/modoboa/admin/management/commands/subcommands/_repair.py b/modoboa/admin/management/commands/subcommands/_repair.py index 7cc8178a6..5a921d6ce 100644 --- a/modoboa/admin/management/commands/subcommands/_repair.py +++ b/modoboa/admin/management/commands/subcommands/_repair.py @@ -33,8 +33,7 @@ def fix_owner(qs, dry_run=False, **options): log(" {cls} {obj} has no owner".format(**kw), **options) continue if isinstance(obj, User): - admin = User.objects.filter( - is_superuser=True, is_active=True).first() + admin = User.objects.filter(is_superuser=True, is_active=True).first() elif isinstance(obj, models.Domain): admin = obj.admins.first() elif isinstance(obj, models.DomainAlias): @@ -43,12 +42,10 @@ def fix_owner(qs, dry_run=False, **options): admin = obj.domain.admins.first() if not admin: # Fallback: use the first superuser found - admin = User.objects.filter( - is_superuser=True, is_active=True).first() + admin = User.objects.filter(is_superuser=True, is_active=True).first() grant_access_to_object(admin, obj, is_owner=True) kw["admin"] = admin - log(" {cls} {obj} is now owned by {admin}".format(**kw), - **options) + log(" {cls} {obj} is now owned by {admin}".format(**kw), **options) @known_problem @@ -72,22 +69,24 @@ def sometimes_mailbox_have_no_alias(**options): recipient_created = 0 for instance in models.Mailbox.objects.select_related("domain").all(): alias, created = models.Alias.objects.get_or_create( - address=instance.full_address, - domain=instance.domain, - internal=True) + address=instance.full_address, domain=instance.domain, internal=True + ) if created: alias_created += 1 log("Alias {0} created".format(alias), **options) recipient, created = models.AliasRecipient.objects.get_or_create( - alias=alias, - address=instance.full_address, - r_mailbox=instance) + alias=alias, address=instance.full_address, r_mailbox=instance + ) if created: recipient_created += 1 log("AliasRecipient {0} created".format(recipient), **options) if alias_created or recipient_created: - log("{0} alias created. {1} alias recipient created".format( - alias_created, recipient_created), **options) + log( + "{0} alias created. {1} alias recipient created".format( + alias_created, recipient_created + ), + **options, + ) class Repair(BaseCommand): @@ -98,21 +97,21 @@ class Repair(BaseCommand): def add_arguments(self, parser): """Add extra arguments to command.""" parser.add_argument( - "--dry-run", action="store_true", default=False, - help="List known problems without fixing them.") + "--dry-run", + action="store_true", + default=False, + help="List known problems without fixing them.", + ) parser.add_argument( - "--quiet", action="store_true", default=False, - help="Quiet mode.") + "--quiet", action="store_true", default=False, help="Quiet mode." + ) def handle(self, *args, **options): """Command entry point.""" # Load known problems from extensions. for ext in settings.MODOBOA_APPS: try: - __import__( - ext, locals(), globals(), - [smart_str("known_problems")] - ) + __import__(ext, locals(), globals(), [smart_str("known_problems")]) except ImportError: pass for func in known_problems: diff --git a/modoboa/admin/migrations/0001_initial.py b/modoboa/admin/migrations/0001_initial.py index 09a6edb57..b6ccc300e 100644 --- a/modoboa/admin/migrations/0001_initial.py +++ b/modoboa/admin/migrations/0001_initial.py @@ -10,158 +10,304 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Alias', + name="Alias", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('address', models.CharField(help_text='The alias address.', max_length=254, verbose_name='address')), - ('enabled', models.BooleanField(default=True, help_text='Check to activate this alias', verbose_name='enabled')), - ('internal', models.BooleanField(default=False)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "address", + models.CharField( + help_text="The alias address.", + max_length=254, + verbose_name="address", + ), + ), + ( + "enabled", + models.BooleanField( + default=True, + help_text="Check to activate this alias", + verbose_name="enabled", + ), + ), + ("internal", models.BooleanField(default=False)), ], options={ - 'ordering': ['address'], + "ordering": ["address"], }, bases=(models.Model,), ), migrations.CreateModel( - name='AliasRecipient', + name="AliasRecipient", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('address', models.EmailField(max_length=75)), - ('alias', models.ForeignKey(to='admin.Alias', on_delete=models.CASCADE)), - ('r_alias', models.ForeignKey(related_name='alias_recipient_aliases', blank=True, to='admin.Alias', null=True, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("address", models.EmailField(max_length=75)), + ( + "alias", + models.ForeignKey(to="admin.Alias", on_delete=models.CASCADE), + ), + ( + "r_alias", + models.ForeignKey( + related_name="alias_recipient_aliases", + blank=True, + to="admin.Alias", + null=True, + on_delete=models.CASCADE, + ), + ), ], options={ - 'db_table': 'modoboa_admin_aliasrecipient', + "db_table": "modoboa_admin_aliasrecipient", }, bases=(models.Model,), ), migrations.CreateModel( - name='Domain', + name="Domain", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(help_text='The domain name', unique=True, max_length=100, verbose_name='name')), - ('quota', models.IntegerField()), - ('enabled', models.BooleanField(default=True, help_text='Check to activate this domain', verbose_name='enabled')), - ('type', models.CharField(default=b'domain', max_length=20)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "name", + models.CharField( + help_text="The domain name", + unique=True, + max_length=100, + verbose_name="name", + ), + ), + ("quota", models.IntegerField()), + ( + "enabled", + models.BooleanField( + default=True, + help_text="Check to activate this domain", + verbose_name="enabled", + ), + ), + ("type", models.CharField(default=b"domain", max_length=20)), ], options={ - 'ordering': ['name'], + "ordering": ["name"], }, bases=(models.Model,), ), migrations.CreateModel( - name='DomainAlias', + name="DomainAlias", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(help_text='The alias name', unique=True, max_length=100, verbose_name='name')), - ('enabled', models.BooleanField(default=True, help_text='Check to activate this alias', verbose_name='enabled')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "name", + models.CharField( + help_text="The alias name", + unique=True, + max_length=100, + verbose_name="name", + ), + ), + ( + "enabled", + models.BooleanField( + default=True, + help_text="Check to activate this alias", + verbose_name="enabled", + ), + ), ], bases=(models.Model,), ), migrations.CreateModel( - name='Mailbox', + name="Mailbox", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('address', models.CharField(help_text='Mailbox address (without the @domain.tld part)', max_length=252, verbose_name='address')), - ('quota', models.PositiveIntegerField()), - ('use_domain_quota', models.BooleanField(default=False)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "address", + models.CharField( + help_text="Mailbox address (without the @domain.tld part)", + max_length=252, + verbose_name="address", + ), + ), + ("quota", models.PositiveIntegerField()), + ("use_domain_quota", models.BooleanField(default=False)), ], bases=(models.Model,), ), migrations.CreateModel( - name='MailboxOperation', + name="MailboxOperation", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('type', models.CharField(max_length=20, choices=[(b'rename', b'rename'), (b'delete', b'delete')])), - ('argument', models.TextField()), - ('mailbox', models.ForeignKey(blank=True, to='admin.Mailbox', null=True, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "type", + models.CharField( + max_length=20, + choices=[(b"rename", b"rename"), (b"delete", b"delete")], + ), + ), + ("argument", models.TextField()), + ( + "mailbox", + models.ForeignKey( + blank=True, + to="admin.Mailbox", + null=True, + on_delete=models.CASCADE, + ), + ), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.CreateModel( - name='ObjectDates', + name="ObjectDates", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('creation', models.DateTimeField(auto_now_add=True)), - ('last_modification', models.DateTimeField(auto_now=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("creation", models.DateTimeField(auto_now_add=True)), + ("last_modification", models.DateTimeField(auto_now=True)), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.CreateModel( - name='Quota', + name="Quota", fields=[ - ('username', models.EmailField(max_length=254, serialize=False, primary_key=True)), - ('bytes', models.BigIntegerField(default=0)), - ('messages', models.IntegerField(default=0)), + ( + "username", + models.EmailField( + max_length=254, serialize=False, primary_key=True + ), + ), + ("bytes", models.BigIntegerField(default=0)), + ("messages", models.IntegerField(default=0)), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.AddField( - model_name='mailbox', - name='dates', - field=models.ForeignKey(to='admin.ObjectDates', on_delete=models.CASCADE), + model_name="mailbox", + name="dates", + field=models.ForeignKey(to="admin.ObjectDates", on_delete=models.CASCADE), preserve_default=True, ), migrations.AddField( - model_name='mailbox', - name='domain', - field=models.ForeignKey(to='admin.Domain', on_delete=models.CASCADE), + model_name="mailbox", + name="domain", + field=models.ForeignKey(to="admin.Domain", on_delete=models.CASCADE), preserve_default=True, ), migrations.AddField( - model_name='mailbox', - name='user', - field=models.OneToOneField(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), + model_name="mailbox", + name="user", + field=models.OneToOneField( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), preserve_default=True, ), migrations.AddField( - model_name='domainalias', - name='dates', - field=models.ForeignKey(to='admin.ObjectDates', on_delete=models.CASCADE), + model_name="domainalias", + name="dates", + field=models.ForeignKey(to="admin.ObjectDates", on_delete=models.CASCADE), preserve_default=True, ), migrations.AddField( - model_name='domainalias', - name='target', - field=models.ForeignKey(verbose_name='target', to='admin.Domain', help_text='The domain this alias points to', on_delete=models.CASCADE), + model_name="domainalias", + name="target", + field=models.ForeignKey( + verbose_name="target", + to="admin.Domain", + help_text="The domain this alias points to", + on_delete=models.CASCADE, + ), preserve_default=True, ), migrations.AddField( - model_name='domain', - name='dates', - field=models.ForeignKey(to='admin.ObjectDates', on_delete=models.CASCADE), + model_name="domain", + name="dates", + field=models.ForeignKey(to="admin.ObjectDates", on_delete=models.CASCADE), preserve_default=True, ), migrations.AddField( - model_name='aliasrecipient', - name='r_mailbox', - field=models.ForeignKey(blank=True, to='admin.Mailbox', null=True, on_delete=models.CASCADE), + model_name="aliasrecipient", + name="r_mailbox", + field=models.ForeignKey( + blank=True, to="admin.Mailbox", null=True, on_delete=models.CASCADE + ), preserve_default=True, ), migrations.AlterUniqueTogether( - name='aliasrecipient', - unique_together=set([('alias', 'r_alias'), ('alias', 'r_mailbox')]), + name="aliasrecipient", + unique_together=set([("alias", "r_alias"), ("alias", "r_mailbox")]), ), migrations.AddField( - model_name='alias', - name='dates', - field=models.ForeignKey(to='admin.ObjectDates', on_delete=models.CASCADE), + model_name="alias", + name="dates", + field=models.ForeignKey(to="admin.ObjectDates", on_delete=models.CASCADE), preserve_default=True, ), migrations.AddField( - model_name='alias', - name='domain', - field=models.ForeignKey(to='admin.Domain', null=True, on_delete=models.CASCADE), + model_name="alias", + name="domain", + field=models.ForeignKey( + to="admin.Domain", null=True, on_delete=models.CASCADE + ), preserve_default=True, ), migrations.AlterUniqueTogether( - name='alias', - unique_together=set([('address', 'internal')]), + name="alias", + unique_together=set([("address", "internal")]), ), ] diff --git a/modoboa/admin/migrations/0002_migrate_from_modoboa_admin.py b/modoboa/admin/migrations/0002_migrate_from_modoboa_admin.py index 301a6406b..13d831adb 100644 --- a/modoboa/admin/migrations/0002_migrate_from_modoboa_admin.py +++ b/modoboa/admin/migrations/0002_migrate_from_modoboa_admin.py @@ -6,8 +6,7 @@ def rename_and_clean(apps, schema_editor): ContentType = apps.get_model("contenttypes", "ContentType") for ct in ContentType.objects.filter(app_label="admin"): try: - old_ct = ContentType.objects.get( - app_label="modoboa_admin", model=ct.model) + old_ct = ContentType.objects.get(app_label="modoboa_admin", model=ct.model) except ContentType.DoesNotExist: continue old_ct.app_label = "admin" @@ -29,8 +28,8 @@ def rename_and_clean(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('admin', '0001_initial'), - ('contenttypes', '0002_remove_content_type_name'), + ("admin", "0001_initial"), + ("contenttypes", "0002_remove_content_type_name"), ] operations = [ diff --git a/modoboa/admin/migrations/0003_auto_20151118_1215.py b/modoboa/admin/migrations/0003_auto_20151118_1215.py index df92da9f4..59f34eb40 100644 --- a/modoboa/admin/migrations/0003_auto_20151118_1215.py +++ b/modoboa/admin/migrations/0003_auto_20151118_1215.py @@ -4,13 +4,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('admin', '0002_migrate_from_modoboa_admin'), + ("admin", "0002_migrate_from_modoboa_admin"), ] operations = [ migrations.AlterField( - model_name='aliasrecipient', - name='address', + model_name="aliasrecipient", + name="address", field=models.EmailField(max_length=254), ), ] diff --git a/modoboa/admin/migrations/0004_auto_20160914_0907.py b/modoboa/admin/migrations/0004_auto_20160914_0907.py index 6427b4439..fc2870b5d 100644 --- a/modoboa/admin/migrations/0004_auto_20160914_0907.py +++ b/modoboa/admin/migrations/0004_auto_20160914_0907.py @@ -6,59 +6,100 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('admin', '0003_auto_20151118_1215'), + ("admin", "0003_auto_20151118_1215"), ] operations = [ migrations.CreateModel( - name='DNSBLResult', + name="DNSBLResult", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('provider', models.CharField(db_index=True, max_length=254)), - ('status', models.CharField(blank=True, db_index=True, max_length=45)), - ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin.Domain')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("provider", models.CharField(db_index=True, max_length=254)), + ("status", models.CharField(blank=True, db_index=True, max_length=45)), + ( + "domain", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="admin.Domain" + ), + ), ], ), migrations.CreateModel( - name='MXRecord', + name="MXRecord", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=254)), - ('address', models.GenericIPAddressField()), - ('managed', models.BooleanField(default=False)), - ('updated', models.DateTimeField()), - ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin.Domain')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=254)), + ("address", models.GenericIPAddressField()), + ("managed", models.BooleanField(default=False)), + ("updated", models.DateTimeField()), + ( + "domain", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="admin.Domain" + ), + ), ], ), migrations.CreateModel( - name='SenderAddress', + name="SenderAddress", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('address', models.EmailField(max_length=254)), - ('mailbox', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin.Mailbox')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("address", models.EmailField(max_length=254)), + ( + "mailbox", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="admin.Mailbox" + ), + ), ], ), migrations.AddField( - model_name='alias', - name='description', + model_name="alias", + name="description", field=models.TextField(blank=True), ), migrations.AddField( - model_name='alias', - name='expire_at', + model_name="alias", + name="expire_at", field=models.DateTimeField(blank=True, null=True), ), migrations.AddField( - model_name='dnsblresult', - name='mx', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin.MXRecord'), + model_name="dnsblresult", + name="mx", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="admin.MXRecord" + ), ), migrations.AlterUniqueTogether( - name='senderaddress', - unique_together=set([('address', 'mailbox')]), + name="senderaddress", + unique_together=set([("address", "mailbox")]), ), migrations.AlterUniqueTogether( - name='dnsblresult', - unique_together=set([('domain', 'provider', 'mx')]), + name="dnsblresult", + unique_together=set([("domain", "provider", "mx")]), ), ] diff --git a/modoboa/admin/migrations/0005_auto_20161026_1003.py b/modoboa/admin/migrations/0005_auto_20161026_1003.py index a6c94f83c..feb2a70f7 100644 --- a/modoboa/admin/migrations/0005_auto_20161026_1003.py +++ b/modoboa/admin/migrations/0005_auto_20161026_1003.py @@ -5,18 +5,18 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('admin', '0004_auto_20160914_0907'), + ("admin", "0004_auto_20160914_0907"), ] operations = [ migrations.AlterField( - model_name='alias', - name='description', - field=models.TextField(blank=True, verbose_name='Description'), + model_name="alias", + name="description", + field=models.TextField(blank=True, verbose_name="Description"), ), migrations.AlterField( - model_name='alias', - name='expire_at', - field=models.DateTimeField(blank=True, null=True, verbose_name='Expire at'), + model_name="alias", + name="expire_at", + field=models.DateTimeField(blank=True, null=True, verbose_name="Expire at"), ), ] diff --git a/modoboa/admin/migrations/0006_auto_20161104_1900.py b/modoboa/admin/migrations/0006_auto_20161104_1900.py index 9b48d6b1a..a8e42a624 100644 --- a/modoboa/admin/migrations/0006_auto_20161104_1900.py +++ b/modoboa/admin/migrations/0006_auto_20161104_1900.py @@ -19,49 +19,49 @@ def move_dates(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('admin', '0005_auto_20161026_1003'), + ("admin", "0005_auto_20161026_1003"), ] operations = [ migrations.AddField( - model_name='alias', - name='creation', + model_name="alias", + name="creation", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.AddField( - model_name='alias', - name='last_modification', + model_name="alias", + name="last_modification", field=models.DateTimeField(null=True), ), migrations.AddField( - model_name='domain', - name='creation', + model_name="domain", + name="creation", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.AddField( - model_name='domain', - name='last_modification', + model_name="domain", + name="last_modification", field=models.DateTimeField(null=True), ), migrations.AddField( - model_name='domainalias', - name='creation', + model_name="domainalias", + name="creation", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.AddField( - model_name='domainalias', - name='last_modification', + model_name="domainalias", + name="last_modification", field=models.DateTimeField(null=True), ), migrations.AddField( - model_name='mailbox', - name='creation', + model_name="mailbox", + name="creation", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.AddField( - model_name='mailbox', - name='last_modification', + model_name="mailbox", + name="last_modification", field=models.DateTimeField(null=True), ), - migrations.RunPython(move_dates) + migrations.RunPython(move_dates), ] diff --git a/modoboa/admin/migrations/0007_auto_20161104_1915.py b/modoboa/admin/migrations/0007_auto_20161104_1915.py index 318e265a4..fd8dae670 100644 --- a/modoboa/admin/migrations/0007_auto_20161104_1915.py +++ b/modoboa/admin/migrations/0007_auto_20161104_1915.py @@ -5,48 +5,48 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('admin', '0006_auto_20161104_1900'), - ('relaydomains', '0005_auto_20161105_1426'), + ("admin", "0006_auto_20161104_1900"), + ("relaydomains", "0005_auto_20161105_1426"), ] operations = [ migrations.RemoveField( - model_name='alias', - name='dates', + model_name="alias", + name="dates", ), migrations.RemoveField( - model_name='domain', - name='dates', + model_name="domain", + name="dates", ), migrations.RemoveField( - model_name='domainalias', - name='dates', + model_name="domainalias", + name="dates", ), migrations.RemoveField( - model_name='mailbox', - name='dates', + model_name="mailbox", + name="dates", ), migrations.AlterField( - model_name='alias', - name='last_modification', + model_name="alias", + name="last_modification", field=models.DateTimeField(auto_now=True), ), migrations.AlterField( - model_name='domain', - name='last_modification', + model_name="domain", + name="last_modification", field=models.DateTimeField(auto_now=True), ), migrations.AlterField( - model_name='domainalias', - name='last_modification', + model_name="domainalias", + name="last_modification", field=models.DateTimeField(auto_now=True), ), migrations.AlterField( - model_name='mailbox', - name='last_modification', + model_name="mailbox", + name="last_modification", field=models.DateTimeField(auto_now=True), ), migrations.DeleteModel( - name='ObjectDates', + name="ObjectDates", ), ] diff --git a/modoboa/admin/migrations/0008_domain_enable_dns_checks.py b/modoboa/admin/migrations/0008_domain_enable_dns_checks.py index 974d05025..2c2342a33 100644 --- a/modoboa/admin/migrations/0008_domain_enable_dns_checks.py +++ b/modoboa/admin/migrations/0008_domain_enable_dns_checks.py @@ -5,13 +5,17 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('admin', '0007_auto_20161104_1915'), + ("admin", "0007_auto_20161104_1915"), ] operations = [ migrations.AddField( - model_name='domain', - name='enable_dns_checks', - field=models.BooleanField(default=True, help_text='Check to enable DNS checks for this domain', verbose_name='Enable DNS checks'), + model_name="domain", + name="enable_dns_checks", + field=models.BooleanField( + default=True, + help_text="Check to enable DNS checks for this domain", + verbose_name="Enable DNS checks", + ), ), ] diff --git a/modoboa/admin/migrations/0009_auto_20170215_0948.py b/modoboa/admin/migrations/0009_auto_20170215_0948.py index 262549b50..3cd3702ed 100644 --- a/modoboa/admin/migrations/0009_auto_20170215_0948.py +++ b/modoboa/admin/migrations/0009_auto_20170215_0948.py @@ -5,13 +5,13 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('admin', '0008_domain_enable_dns_checks'), + ("admin", "0008_domain_enable_dns_checks"), ] operations = [ migrations.RenameField( - model_name='domain', - old_name='quota', - new_name='default_mailbox_quota', + model_name="domain", + old_name="quota", + new_name="default_mailbox_quota", ), ] diff --git a/modoboa/admin/migrations/0010_auto_20170215_1328.py b/modoboa/admin/migrations/0010_auto_20170215_1328.py index bc4535930..ad6ca3d8b 100644 --- a/modoboa/admin/migrations/0010_auto_20170215_1328.py +++ b/modoboa/admin/migrations/0010_auto_20170215_1328.py @@ -5,23 +5,30 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('admin', '0009_auto_20170215_0948'), + ("admin", "0009_auto_20170215_0948"), ] operations = [ migrations.AddField( - model_name='domain', - name='quota', - field=models.PositiveIntegerField(default=0, help_text='Quota in MB shared between mailboxes. A value of 0 means no quota.'), + model_name="domain", + name="quota", + field=models.PositiveIntegerField( + default=0, + help_text="Quota in MB shared between mailboxes. A value of 0 means no quota.", + ), ), migrations.AlterField( - model_name='domain', - name='default_mailbox_quota', - field=models.PositiveIntegerField(default=0, help_text='Default quota in MB applied to mailboxes. A value of 0 means no quota.', verbose_name='Default mailbox quota'), + model_name="domain", + name="default_mailbox_quota", + field=models.PositiveIntegerField( + default=0, + help_text="Default quota in MB applied to mailboxes. A value of 0 means no quota.", + verbose_name="Default mailbox quota", + ), ), migrations.AlterField( - model_name='mailbox', - name='quota', + model_name="mailbox", + name="quota", field=models.PositiveIntegerField(default=0), ), ] diff --git a/modoboa/admin/migrations/0011_domain_transport.py b/modoboa/admin/migrations/0011_domain_transport.py index 001b23761..9516d06c7 100644 --- a/modoboa/admin/migrations/0011_domain_transport.py +++ b/modoboa/admin/migrations/0011_domain_transport.py @@ -6,14 +6,18 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('transport', '0001_initial'), - ('admin', '0010_auto_20170215_1328'), + ("transport", "0001_initial"), + ("admin", "0010_auto_20170215_1328"), ] operations = [ migrations.AddField( - model_name='domain', - name='transport', - field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='transport.Transport'), + model_name="domain", + name="transport", + field=models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="transport.Transport", + ), ), ] diff --git a/modoboa/admin/migrations/0012_auto_20180112_1658.py b/modoboa/admin/migrations/0012_auto_20180112_1658.py index 24bc6f938..71b93b2c9 100644 --- a/modoboa/admin/migrations/0012_auto_20180112_1658.py +++ b/modoboa/admin/migrations/0012_auto_20180112_1658.py @@ -5,33 +5,44 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('admin', '0011_domain_transport'), + ("admin", "0011_domain_transport"), ] operations = [ migrations.AddField( - model_name='domain', - name='dkim_key_length', - field=models.PositiveIntegerField(blank=True, choices=[(1024, '1024'), (2048, '2048'), (4096, '4096')], null=True, verbose_name='Key length'), + model_name="domain", + name="dkim_key_length", + field=models.PositiveIntegerField( + blank=True, + choices=[(1024, "1024"), (2048, "2048"), (4096, "4096")], + null=True, + verbose_name="Key length", + ), ), migrations.AddField( - model_name='domain', - name='dkim_key_selector', - field=models.CharField(default='modoboa', max_length=30, verbose_name='Key selector'), + model_name="domain", + name="dkim_key_selector", + field=models.CharField( + default="modoboa", max_length=30, verbose_name="Key selector" + ), ), migrations.AddField( - model_name='domain', - name='dkim_private_key_path', + model_name="domain", + name="dkim_private_key_path", field=models.CharField(blank=True, max_length=254), ), migrations.AddField( - model_name='domain', - name='dkim_public_key', + model_name="domain", + name="dkim_public_key", field=models.TextField(blank=True), ), migrations.AddField( - model_name='domain', - name='enable_dkim', - field=models.BooleanField(default=False, help_text='If you activate this feature, a DKIM key will be generated for this domain.', verbose_name='Enable DKIM signing'), + model_name="domain", + name="enable_dkim", + field=models.BooleanField( + default=False, + help_text="If you activate this feature, a DKIM key will be generated for this domain.", + verbose_name="Enable DKIM signing", + ), ), ] diff --git a/modoboa/admin/migrations/0013_auto_20180124_2311.py b/modoboa/admin/migrations/0013_auto_20180124_2311.py index c5bd1d3b5..9985d9b2e 100644 --- a/modoboa/admin/migrations/0013_auto_20180124_2311.py +++ b/modoboa/admin/migrations/0013_auto_20180124_2311.py @@ -5,23 +5,25 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('admin', '0012_auto_20180112_1658'), + ("admin", "0012_auto_20180112_1658"), ] operations = [ migrations.AlterField( - model_name='domain', - name='dkim_key_selector', - field=models.CharField(default='modoboa', max_length=30), + model_name="domain", + name="dkim_key_selector", + field=models.CharField(default="modoboa", max_length=30), ), migrations.AlterField( - model_name='domain', - name='type', - field=models.CharField(default='domain', max_length=20), + model_name="domain", + name="type", + field=models.CharField(default="domain", max_length=20), ), migrations.AlterField( - model_name='mailboxoperation', - name='type', - field=models.CharField(choices=[('rename', 'rename'), ('delete', 'delete')], max_length=20), + model_name="mailboxoperation", + name="type", + field=models.CharField( + choices=[("rename", "rename"), ("delete", "delete")], max_length=20 + ), ), ] diff --git a/modoboa/admin/migrations/0014_auto_20181017_1628.py b/modoboa/admin/migrations/0014_auto_20181017_1628.py index 73b47d5b8..13d91fbe2 100644 --- a/modoboa/admin/migrations/0014_auto_20181017_1628.py +++ b/modoboa/admin/migrations/0014_auto_20181017_1628.py @@ -5,18 +5,20 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('admin', '0013_auto_20180124_2311'), + ("admin", "0013_auto_20180124_2311"), ] operations = [ migrations.AlterField( - model_name='domain', - name='default_mailbox_quota', - field=models.PositiveIntegerField(default=0, verbose_name='Default mailbox quota'), + model_name="domain", + name="default_mailbox_quota", + field=models.PositiveIntegerField( + default=0, verbose_name="Default mailbox quota" + ), ), migrations.AlterField( - model_name='domain', - name='quota', + model_name="domain", + name="quota", field=models.PositiveIntegerField(default=0), ), ] diff --git a/modoboa/admin/migrations/0015_rename_view_permissions.py b/modoboa/admin/migrations/0015_rename_view_permissions.py index 744469a87..456db7c75 100644 --- a/modoboa/admin/migrations/0015_rename_view_permissions.py +++ b/modoboa/admin/migrations/0015_rename_view_permissions.py @@ -5,14 +5,14 @@ import sys from django.db import IntegrityError, migrations, transaction VIEW_PERMISSION_CODENAMES = { - 'view_aliases': 'view_alias', - 'view_domaliases': 'view_domainalias', - 'view_mailboxes': 'view_mailbox', + "view_aliases": "view_alias", + "view_domaliases": "view_domainalias", + "view_mailboxes": "view_mailbox", } def rename_view_permissions(apps, schema_editor): - Permission = apps.get_model('auth', 'Permission') + Permission = apps.get_model("auth", "Permission") db_alias = schema_editor.connection.alias # Iterate over permissions and try to rename the codename for perm in Permission.objects.using(db_alias).filter( @@ -36,7 +36,7 @@ def rename_view_permissions(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('admin', '0014_auto_20181017_1628'), + ("admin", "0014_auto_20181017_1628"), ] operations = [ diff --git a/modoboa/admin/migrations/0016_auto_20200602_1201.py b/modoboa/admin/migrations/0016_auto_20200602_1201.py index 4b273fb07..8e2f4351a 100644 --- a/modoboa/admin/migrations/0016_auto_20200602_1201.py +++ b/modoboa/admin/migrations/0016_auto_20200602_1201.py @@ -6,18 +6,28 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('admin', '0015_rename_view_permissions'), + ("admin", "0015_rename_view_permissions"), ] operations = [ migrations.AddField( - model_name='domain', - name='message_limit', - field=models.PositiveIntegerField(blank=True, help_text='Number of messages this domain can send per day', null=True, verbose_name='Message sending limit'), + model_name="domain", + name="message_limit", + field=models.PositiveIntegerField( + blank=True, + help_text="Number of messages this domain can send per day", + null=True, + verbose_name="Message sending limit", + ), ), migrations.AddField( - model_name='mailbox', - name='message_limit', - field=models.PositiveIntegerField(blank=True, help_text='Number of messages this mailbox can send per day', null=True, verbose_name='Message sending limit'), + model_name="mailbox", + name="message_limit", + field=models.PositiveIntegerField( + blank=True, + help_text="Number of messages this mailbox can send per day", + null=True, + verbose_name="Message sending limit", + ), ), ] diff --git a/modoboa/admin/migrations/0017_alarm.py b/modoboa/admin/migrations/0017_alarm.py index ea955c897..e820ba562 100644 --- a/modoboa/admin/migrations/0017_alarm.py +++ b/modoboa/admin/migrations/0017_alarm.py @@ -8,24 +8,53 @@ import django.utils.timezone class Migration(migrations.Migration): dependencies = [ - ('admin', '0016_auto_20200602_1201'), + ("admin", "0016_auto_20200602_1201"), ] operations = [ migrations.CreateModel( - name='Alarm', + name="Alarm", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(default=django.utils.timezone.now)), - ('closed', models.DateTimeField(blank=True, null=True)), - ('status', models.IntegerField(choices=[(1, 'Opened'), (2, 'Closed')], db_index=True, default=1)), - ('title', models.CharField(max_length=150)), - ('internal_name', models.CharField(max_length=120)), - ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='alarms', to='admin.Domain')), - ('mailbox', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='alarms', to='admin.Mailbox')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created", models.DateTimeField(default=django.utils.timezone.now)), + ("closed", models.DateTimeField(blank=True, null=True)), + ( + "status", + models.IntegerField( + choices=[(1, "Opened"), (2, "Closed")], db_index=True, default=1 + ), + ), + ("title", models.CharField(max_length=150)), + ("internal_name", models.CharField(max_length=120)), + ( + "domain", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="alarms", + to="admin.Domain", + ), + ), + ( + "mailbox", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="alarms", + to="admin.Mailbox", + ), + ), ], options={ - 'ordering': ['created'], + "ordering": ["created"], }, ), ] diff --git a/modoboa/admin/migrations/0018_auto_20201204_0935.py b/modoboa/admin/migrations/0018_auto_20201204_0935.py index 35bdf2f62..4e2503758 100644 --- a/modoboa/admin/migrations/0018_auto_20201204_0935.py +++ b/modoboa/admin/migrations/0018_auto_20201204_0935.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('admin', '0017_alarm'), + ("admin", "0017_alarm"), ] operations = [ migrations.AlterField( - model_name='alarm', - name='title', + 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 index fe3f727d5..be08bf179 100644 --- a/modoboa/admin/migrations/0019_update_disabled_accounts_aliases.py +++ b/modoboa/admin/migrations/0019_update_disabled_accounts_aliases.py @@ -4,14 +4,13 @@ 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') + User = apps.get_model("core", "User") + Alias = apps.get_model("admin", "Alias") - disabled_accounts = User.objects.filter( - mailbox__isnull=False, is_active=False) + disabled_accounts = User.objects.filter(mailbox__isnull=False, is_active=False) Alias.objects.filter( - address__in=list(disabled_accounts.values_list('email', flat=True)), + address__in=list(disabled_accounts.values_list("email", flat=True)), enabled=True, ).update(enabled=False) @@ -19,8 +18,8 @@ def update_disabled_accounts_aliases(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('admin', '0018_auto_20201204_0935'), - ('core', '0022_user_tfa_enabled'), + ("admin", "0018_auto_20201204_0935"), + ("core", "0022_user_tfa_enabled"), ] operations = [ diff --git a/modoboa/admin/migrations/0020_auto_20210130_2226.py b/modoboa/admin/migrations/0020_auto_20210130_2226.py index ea9e90eb7..a2136047a 100644 --- a/modoboa/admin/migrations/0020_auto_20210130_2226.py +++ b/modoboa/admin/migrations/0020_auto_20210130_2226.py @@ -6,13 +6,18 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('admin', '0019_update_disabled_accounts_aliases'), + ("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'), + 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/__init__.py b/modoboa/admin/models/__init__.py index 0b749b51a..e5a6c1d69 100644 --- a/modoboa/admin/models/__init__.py +++ b/modoboa/admin/models/__init__.py @@ -1,10 +1,7 @@ """Admin models.""" from .alarm import Alarm -from .alias import ( - Alias, - AliasRecipient, - validate_alias_address) +from .alias import Alias, AliasRecipient, validate_alias_address from .base import AdminObject from .domain import Domain from .domain_alias import DomainAlias diff --git a/modoboa/admin/models/alarm.py b/modoboa/admin/models/alarm.py index 1e3575663..15267a609 100644 --- a/modoboa/admin/models/alarm.py +++ b/modoboa/admin/models/alarm.py @@ -20,18 +20,23 @@ class Alarm(models.Model): """A simple alarm to attach to a domain and/or mailbox.""" domain = models.ForeignKey( - "admin.Domain", on_delete=models.CASCADE, related_name="alarms", - blank=True, null=True) - mailbox = models.ForeignKey( - "admin.Mailbox", on_delete=models.SET_NULL, + "admin.Domain", + on_delete=models.CASCADE, related_name="alarms", - null=True, blank=True, + blank=True, + null=True, + ) + mailbox = models.ForeignKey( + "admin.Mailbox", + on_delete=models.SET_NULL, + related_name="alarms", + null=True, + blank=True, ) created = models.DateTimeField(default=timezone.now) closed = models.DateTimeField(null=True, blank=True) status = models.IntegerField( - default=constants.ALARM_OPENED, choices=constants.ALARM_STATUSES, - db_index=True + default=constants.ALARM_OPENED, choices=constants.ALARM_STATUSES, db_index=True ) title = models.TextField() @@ -44,7 +49,8 @@ class Alarm(models.Model): def __str__(self): return "[{}] {} - {}".format( - self.created, self.domain, self.get_status_display()) + self.created, self.domain, self.get_status_display() + ) def close(self): """Close this alarm.""" diff --git a/modoboa/admin/models/alias.py b/modoboa/admin/models/alias.py index 28f48eef8..9d870a9fb 100644 --- a/modoboa/admin/models/alias.py +++ b/modoboa/admin/models/alias.py @@ -6,9 +6,7 @@ import random from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse -from django.utils.encoding import ( - force_bytes, force_str, smart_str -) +from django.utils.encoding import force_bytes, force_str, smart_str from django.utils.translation import gettext as _, gettext_lazy from reversion import revisions as reversion @@ -16,7 +14,11 @@ from reversion import revisions as reversion from modoboa.core import signals as core_signals from modoboa.lib.email_utils import split_mailbox from modoboa.lib.exceptions import ( - BadRequest, Conflict, NotFound, ModoboaException, AliasExists + BadRequest, + Conflict, + NotFound, + ModoboaException, + AliasExists, ) from .. import signals from .base import AdminObject @@ -25,8 +27,8 @@ from .mailbox import Mailbox def validate_alias_address( - address, creator, internal=False, instance=None, ignore_existing=False - ): + address, creator, internal=False, instance=None, ignore_existing=False +): """Check if the given alias address can be created by creator.""" local_part, domain = split_mailbox(address.lower()) domain = Domain.objects.filter(name=domain).first() @@ -36,21 +38,21 @@ def validate_alias_address( raise ValidationError(_("Permission denied.")) if not instance or instance.address != address: alias = Alias.objects.filter(address=address, internal=internal) - condition = ( - alias.exists() - and not ignore_existing - ) + condition = alias.exists() and not ignore_existing if condition: raise AliasExists(alias.first().pk) if instance is None: try: # Check creator limits core_signals.can_create_object.send( - sender="validate_alias_address", context=creator, klass=Alias) + sender="validate_alias_address", context=creator, klass=Alias + ) # Check domain limits core_signals.can_create_object.send( - sender="validate_alias_address", context=domain, - object_type="mailbox_aliases") + sender="validate_alias_address", + context=domain, + object_type="mailbox_aliases", + ) except ModoboaException as inst: raise ValidationError(str(inst)) return local_part, domain @@ -78,10 +80,7 @@ class AliasManager(models.Manager): alias.first().add_recipients(recipients) return self.create( - creator=creator, - domain=domain, - address=address, - recipients=recipients + creator=creator, domain=domain, address=address, recipients=recipients ) @@ -89,29 +88,26 @@ class Alias(AdminObject): """Mailbox alias.""" address = models.CharField( - gettext_lazy("address"), max_length=254, - help_text=gettext_lazy( - "The alias address." - ) + gettext_lazy("address"), + max_length=254, + help_text=gettext_lazy("The alias address."), ) domain = models.ForeignKey(Domain, null=True, on_delete=models.CASCADE) enabled = models.BooleanField( gettext_lazy("enabled"), help_text=gettext_lazy("Check to activate this alias"), - default=True + default=True, ) internal = models.BooleanField(default=False) - description = models.TextField( - gettext_lazy("Description"), blank=True) - expire_at = models.DateTimeField( - gettext_lazy("Expire at"), blank=True, null=True) + description = models.TextField(gettext_lazy("Description"), blank=True) + expire_at = models.DateTimeField(gettext_lazy("Expire at"), blank=True, null=True) _objectname = "MailboxAlias" objects = AliasManager() class Meta: ordering = ["address"] - unique_together = (("address", "internal"), ) + unique_together = (("address", "internal"),) app_label = "admin" def __str__(self): @@ -153,6 +149,7 @@ class Alias(AdminObject): def post_create(self, creator): from modoboa.lib.permissions import grant_access_to_object + super(Alias, self).post_create(creator) if creator.is_superuser: for admin in self.domain.admins: @@ -173,33 +170,32 @@ class Alias(AdminObject): continue if self.aliasrecipient_set.filter(address=address).exists(): continue - local_part, domname, extension = ( - split_mailbox(address, return_extension=True)) + local_part, domname, extension = split_mailbox( + address, return_extension=True + ) if domname is None: - raise BadRequest( - u"%s %s" % (_("Invalid address"), address) - ) + raise BadRequest("%s %s" % (_("Invalid address"), address)) domain = Domain.objects.filter(name=domname).first() kwargs = {"address": address, "alias": self} - if ( - (domain is not None) and - ( - any( - r[1] for r in signals.use_external_recipients.send( - self, recipients=address) - ) is False + if (domain is not None) and ( + any( + r[1] + for r in signals.use_external_recipients.send( + self, recipients=address + ) ) + is False ): - rcpt = Mailbox.objects.filter( - domain=domain, address=local_part).first() + rcpt = Mailbox.objects.filter(domain=domain, address=local_part).first() if rcpt is None: rcpt = Alias.objects.filter( address="%s@%s" % (local_part, domname) ).first() if rcpt is None: raise NotFound( - _("Local recipient {}@{} not found") - .format(local_part, domname) + _("Local recipient {}@{} not found").format( + local_part, domname + ) ) if rcpt.address == self.address: raise Conflict @@ -229,15 +225,13 @@ class Alias(AdminObject): self.add_recipients(address_list) # Remove old recipients - self.aliasrecipient_set.exclude( - address__in=address_list).delete() + self.aliasrecipient_set.exclude(address__in=address_list).delete() @property def recipients(self): """Return the recipient list.""" - return ( - self.aliasrecipient_set.order_by("address") - .values_list("address", flat=True) + return self.aliasrecipient_set.order_by("address").values_list( + "address", flat=True ) @property @@ -245,12 +239,7 @@ class Alias(AdminObject): """Return the number of recipients of this alias.""" return self.aliasrecipient_set.count() - def from_csv(self, - user, - row, - expected_elements=5, - formopts=False, - **kwargs): + def from_csv(self, user, row, expected_elements=5, formopts=False, **kwargs): """Create a new alias from a CSV file entry.""" if len(row) < expected_elements: raise BadRequest(_("Invalid line: {}").format(row)) @@ -264,15 +253,16 @@ class Alias(AdminObject): if formopts: continue_if_exists = formopts.get("continue_if_exists", False) if continue_if_exists: - alias = Alias.objects.filter(address=self.address, - internal=False).first() + alias = Alias.objects.filter( + address=self.address, internal=False + ).first() alias.add_recipients([raddress.strip() for raddress in row[3:]]) alias.save() return else: raise e - self.enabled = (row[2].strip().lower() in ["true", "1", "yes", "y"]) + self.enabled = row[2].strip().lower() in ["true", "1", "yes", "y"] self.save() self.set_recipients([raddress.strip() for raddress in row[3:]]) self.post_create(user) @@ -291,30 +281,29 @@ reversion.register(Alias) class AliasRecipient(models.Model): - """An alias recipient.""" address = models.EmailField() alias = models.ForeignKey(Alias, on_delete=models.CASCADE) # if recipient is a local mailbox - r_mailbox = models.ForeignKey(Mailbox, blank=True, null=True, - on_delete=models.CASCADE) + r_mailbox = models.ForeignKey( + Mailbox, blank=True, null=True, on_delete=models.CASCADE + ) # if recipient is a local alias r_alias = models.ForeignKey( - Alias, related_name="alias_recipient_aliases", blank=True, null=True, - on_delete=models.CASCADE) + Alias, + related_name="alias_recipient_aliases", + blank=True, + null=True, + on_delete=models.CASCADE, + ) class Meta: app_label = "admin" db_table = "modoboa_admin_aliasrecipient" - unique_together = [ - ("alias", "r_mailbox"), - ("alias", "r_alias") - ] + unique_together = [("alias", "r_mailbox"), ("alias", "r_alias")] def __str__(self): """Return alias and recipient.""" - return smart_str( - "{} -> {}".format(self.alias.address, self.address) - ) + return smart_str("{} -> {}".format(self.alias.address, self.address)) diff --git a/modoboa/admin/models/base.py b/modoboa/admin/models/base.py index 8a1d33742..0fc6ef7d3 100644 --- a/modoboa/admin/models/base.py +++ b/modoboa/admin/models/base.py @@ -5,9 +5,7 @@ from django.db import models from django.utils import timezone from modoboa.core import models as core_models -from modoboa.lib.permissions import ( - grant_access_to_object, ungrant_access_to_object -) +from modoboa.lib.permissions import grant_access_to_object, ungrant_access_to_object class AdminObjectManager(models.Manager): diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index 3cc25bf90..6a84c2f1a 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -23,47 +23,54 @@ from . import mixins class Domain(mixins.MessageLimitMixin, AdminObject): """Mail domain.""" - name = models.CharField(gettext_lazy("name"), max_length=253, unique=True, - help_text=gettext_lazy("The domain name")) + name = models.CharField( + gettext_lazy("name"), + max_length=253, + unique=True, + help_text=gettext_lazy("The domain name"), + ) quota = models.PositiveIntegerField( default=0, ) default_mailbox_quota = models.PositiveIntegerField( - verbose_name=gettext_lazy("Default mailbox quota"), - default=0 + verbose_name=gettext_lazy("Default mailbox quota"), default=0 ) message_limit = models.PositiveIntegerField( gettext_lazy("Message sending limit"), - null=True, blank=True, - help_text=gettext_lazy( - "Number of messages this domain can send per day" - ) + null=True, + blank=True, + help_text=gettext_lazy("Number of messages this domain can send per day"), ) enabled = models.BooleanField( gettext_lazy("enabled"), help_text=gettext_lazy("Check to activate this domain"), - default=True + default=True, ) type = models.CharField(default="domain", max_length=20) # NOQA:A003 enable_dns_checks = models.BooleanField( - gettext_lazy("Enable DNS checks"), default=True, - help_text=gettext_lazy("Check to enable DNS checks for this domain") + gettext_lazy("Enable DNS checks"), + default=True, + help_text=gettext_lazy("Check to enable DNS checks for this domain"), ) transport = models.OneToOneField( - "transport.Transport", null=True, on_delete=models.SET_NULL) + "transport.Transport", null=True, on_delete=models.SET_NULL + ) enable_dkim = models.BooleanField( gettext_lazy("Enable DKIM signing"), help_text=gettext_lazy( "If you activate this feature, a DKIM key will be " - "generated for this domain."), - default=False + "generated for this domain." + ), + default=False, ) dkim_key_selector = models.CharField(max_length=30, default="modoboa") dkim_key_length = models.PositiveIntegerField( - gettext_lazy("Key length"), choices=constants.DKIM_KEY_LENGTHS, - blank=True, null=True + gettext_lazy("Key length"), + choices=constants.DKIM_KEY_LENGTHS, + blank=True, + null=True, ) dkim_public_key = models.TextField(blank=True) dkim_private_key_path = models.CharField(max_length=254, blank=True) @@ -94,9 +101,7 @@ class Domain(mixins.MessageLimitMixin, AdminObject): @property def identities_count(self) -> int: """Total number of identities in this domain.""" - return ( - self.mailbox_set.count() + - self.alias_set.filter(internal=False).count()) + return self.mailbox_set.count() + self.alias_set.filter(internal=False).count() @property def opened_alarms_count(self) -> int: @@ -111,12 +116,14 @@ class Domain(mixins.MessageLimitMixin, AdminObject): label = dt[1] result = [{"name": self.type, "label": label, "type": "dom"}] if self.transport: - result.append({ - "name": self.transport.service, - "label": self.transport.service, - "type": "srv", - "color": "info" - }) + result.append( + { + "name": self.transport.service, + "label": self.transport.service, + "type": "srv", + "color": "info", + } + ) return result @property @@ -128,7 +135,8 @@ class Domain(mixins.MessageLimitMixin, AdminObject): return User.objects.filter( is_superuser=False, objectaccess__content_type__model="domain", - objectaccess__object_id=self.pk) + objectaccess__object_id=self.pk, + ) @property def aliases(self): @@ -165,7 +173,10 @@ class Domain(mixins.MessageLimitMixin, AdminObject): errors = [] if config["enable_mx_checks"] and not self.mxrecord_set.has_valids(): errors.append("mx") - if config["enable_dnsbl_checks"] and self.dnsblresult_set.blacklisted().exists(): + if ( + config["enable_dnsbl_checks"] + and self.dnsblresult_set.blacklisted().exists() + ): errors.append("dnsbl") if config["enable_spf_checks"]: if self.spf_record is None or not self.spf_record.is_valid: @@ -225,8 +236,7 @@ class Domain(mixins.MessageLimitMixin, AdminObject): return 0 if not self.mailbox_set.exists(): return 0 - return self.mailbox_set.aggregate( - total=models.Sum("quota"))["total"] + return self.mailbox_set.aggregate(total=models.Sum("quota"))["total"] @cached_property def allocated_quota_in_percent(self): @@ -268,14 +278,15 @@ class Domain(mixins.MessageLimitMixin, AdminObject): split_record = [] while record: if len(record) > 74: - split_record.append(" \"%s\"" % record[:74]) + split_record.append(' "%s"' % record[:74]) record = record[74:] else: - split_record.append(" \"%s\"" % record) + split_record.append(' "%s"' % record) break record = "\n".join(split_record) return "{}._domainkey.{}. IN TXT (\n{})".format( - self.dkim_key_selector, self.name, record) + self.dkim_key_selector, self.name, record + ) def add_admin(self, account): """Add a new administrator to this domain. @@ -285,7 +296,8 @@ class Domain(mixins.MessageLimitMixin, AdminObject): from modoboa.lib.permissions import grant_access_to_object core_signals.can_create_object.send( - sender=self.__class__, context=self, object_type="domain_admins") + sender=self.__class__, context=self, object_type="domain_admins" + ) grant_access_to_object(account, self) for mb in self.mailbox_set.all(): if mb.user.has_perm("admin.add_domain"): @@ -315,9 +327,7 @@ class Domain(mixins.MessageLimitMixin, AdminObject): """Store current data if domain is renamed.""" # We check that the instance exists to use m2m relationship if self.pk and self.oldname != self.name: - self.old_mail_homes = { - mb.id: mb.mail_home for mb in self.mailbox_set.all() - } + self.old_mail_homes = {mb.id: mb.mail_home for mb in self.mailbox_set.all()} if self.old_dkim_key_length != self.dkim_key_length: self.dkim_public_key = "" self.dkim_private_key_path = "" @@ -359,38 +369,36 @@ class Domain(mixins.MessageLimitMixin, AdminObject): self.name = row[1].strip().lower() if Domain.objects.filter(name=self.name).exists(): raise Conflict - domains_must_have_authorized_mx = ( - param_tools.get_global_parameter("domains_must_have_authorized_mx") + domains_must_have_authorized_mx = param_tools.get_global_parameter( + "domains_must_have_authorized_mx" ) if domains_must_have_authorized_mx and not user.is_superuser: if not lib.domain_has_authorized_mx(self.name): raise BadRequest( - _("{}: no authorized MX record found for domain") - .format(self.name) + _("{}: no authorized MX record found for domain").format(self.name) ) try: self.quota = int(row[2].strip()) except ValueError: - raise BadRequest( - _("{}: invalid quota value for domain") - .format(self.name) - ) + raise BadRequest(_("{}: invalid quota value for domain").format(self.name)) try: self.default_mailbox_quota = int(row[3].strip()) except ValueError: raise BadRequest( - _("{}: invalid default mailbox quota value for domain") - .format(self.name) + _("{}: invalid default mailbox quota value for domain").format( + self.name + ) ) if self.quota != 0 and self.default_mailbox_quota > self.quota: raise BadRequest( - _("{}: default mailbox quota cannot be greater than domain " - "quota").format(self.name) + _( + "{}: default mailbox quota cannot be greater than domain " "quota" + ).format(self.name) ) - self.enabled = (row[4].strip().lower() in ["true", "1", "yes", "y"]) + self.enabled = row[4].strip().lower() in ["true", "1", "yes", "y"] core_signals.can_create_object.send( - sender=self.__class__, context=user, klass=Domain, - instance=self) + sender=self.__class__, context=user, klass=Domain, instance=self + ) self.save(creator=user) def to_csv_rows(self): @@ -401,7 +409,7 @@ class Domain(mixins.MessageLimitMixin, AdminObject): force_str(self.name), self.quota, self.default_mailbox_quota, - self.enabled + self.enabled, ] ] for dalias in self.domainalias_set.all(): diff --git a/modoboa/admin/models/domain_alias.py b/modoboa/admin/models/domain_alias.py index c312f34ec..32a00b7f9 100644 --- a/modoboa/admin/models/domain_alias.py +++ b/modoboa/admin/models/domain_alias.py @@ -27,20 +27,24 @@ class DomainAliasManager(models.Manager): class DomainAlias(AdminObject): - """Domain aliases.""" - name = models.CharField(gettext_lazy("name"), max_length=100, unique=True, - help_text=gettext_lazy("The alias name")) + name = models.CharField( + gettext_lazy("name"), + max_length=100, + unique=True, + help_text=gettext_lazy("The alias name"), + ) target = models.ForeignKey( - Domain, verbose_name=gettext_lazy("target"), + Domain, + verbose_name=gettext_lazy("target"), help_text=gettext_lazy("The domain this alias points to"), - on_delete=models.CASCADE + on_delete=models.CASCADE, ) enabled = models.BooleanField( gettext_lazy("enabled"), help_text=gettext_lazy("Check to activate this alias"), - default=True + default=True, ) owners = GenericRelation(core_models.ObjectAccess) @@ -74,14 +78,14 @@ class DomainAlias(AdminObject): except Domain.DoesNotExist: raise BadRequest(_("Unknown domain %s") % domname) core_signals.can_create_object.send( - sender="import", context=self.target, object_type="domain_aliases") + sender="import", context=self.target, object_type="domain_aliases" + ) self.enabled = row[3].strip().lower() in ["true", "1", "yes", "y"] self.save(creator=user) def to_csv_row(self): """Export to row that can be included in a CSV file.""" - return ["domainalias", self.name, - self.target.name, self.enabled] + return ["domainalias", self.name, self.target.name, self.enabled] def to_csv(self, csvwriter): """Export a domain alias using CSV format diff --git a/modoboa/admin/models/mailbox.py b/modoboa/admin/models/mailbox.py index 0cdf01e69..e87d76502 100644 --- a/modoboa/admin/models/mailbox.py +++ b/modoboa/admin/models/mailbox.py @@ -25,8 +25,7 @@ class QuotaManager(models.Manager): def get_domain_usage(self, domain): """Return current usage for domain.""" - qset = self.get_queryset().filter( - username__endswith="@{}".format(domain.name)) + qset = self.get_queryset().filter(username__endswith="@{}".format(domain.name)) result = qset.aggregate(usage=models.Sum("bytes")).get("usage", 0) if result is None: result = 0 @@ -34,7 +33,6 @@ class QuotaManager(models.Manager): class Quota(models.Model): - """Keeps track of Mailbox current quota.""" username = models.EmailField(primary_key=True, max_length=254) @@ -48,7 +46,6 @@ class Quota(models.Model): class MailboxManager(Manager): - """Custom manager for Mailbox.""" def get_for_admin(self, admin, squery=None): @@ -66,18 +63,14 @@ class MailboxManager(Manager): parts = squery.split("@") addrfilter = "@".join(parts[:-1]) domfilter = parts[-1] - qf = ( - Q(address__contains=addrfilter) & - Q(domain__name__contains=domfilter) + qf = Q(address__contains=addrfilter) & Q( + domain__name__contains=domfilter ) else: - qf = ( - Q(address__contains=squery) | - Q(domain__name__contains=squery) - ) - ids = admin.objectaccess_set \ - .filter(content_type=ContentType.objects.get_for_model(Mailbox)) \ - .values_list("object_id", flat=True) + qf = Q(address__contains=squery) | Q(domain__name__contains=squery) + ids = admin.objectaccess_set.filter( + content_type=ContentType.objects.get_for_model(Mailbox) + ).values_list("object_id", flat=True) if qf is not None: qf = Q(pk__in=ids) & qf else: @@ -89,16 +82,17 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject): """User mailbox.""" address = models.CharField( - gettext_lazy("address"), max_length=252, - help_text=gettext_lazy( - "Mailbox address (without the @domain.tld part)") + gettext_lazy("address"), + max_length=252, + help_text=gettext_lazy("Mailbox address (without the @domain.tld part)"), ) quota = models.PositiveIntegerField(default=0) use_domain_quota = models.BooleanField(default=False) message_limit = models.PositiveIntegerField( - gettext_lazy("Message sending limit"), null=True, blank=True, - help_text=gettext_lazy( - "Number of messages this mailbox can send per day") + gettext_lazy("Message sending limit"), + null=True, + blank=True, + help_text=gettext_lazy("Number of messages this mailbox can send per day"), ) domain = models.ForeignKey(Domain, on_delete=models.CASCADE) user = models.OneToOneField(User, on_delete=models.CASCADE) @@ -134,7 +128,8 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject): def alias_count(self): return ( self.recipientalias_set.select_related("alias") - .filter(alias__internal=False).count() + .filter(alias__internal=False) + .count() ) @property @@ -155,7 +150,8 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject): code, output = doveadm_cmd("user -f home %s" % self.full_address) if code: raise lib_exceptions.InternalError( - _("Failed to retrieve mailbox location (%s)") % output) + _("Failed to retrieve mailbox location (%s)") % output + ) self.__mail_home = force_str(output.strip()) return self.__mail_home @@ -165,9 +161,8 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject): :rtype: list of string """ - qset = ( - self.aliasrecipient_set.select_related("alias") - .filter(alias__internal=False) + qset = self.aliasrecipient_set.select_related("alias").filter( + alias__internal=False ) aliases = [alr.alias.address for alr in qset] return aliases @@ -177,8 +172,7 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject): """Retrieve the ``Quota`` instance associated to this mailbox.""" if not hasattr(self, "_quota_value"): try: - self._quota_value = Quota.objects.get( - username=self.full_address) + self._quota_value = Quota.objects.get(username=self.full_address) except Quota.DoesNotExist: return None return self._quota_value @@ -195,8 +189,7 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject): def rename_dir(self, old_mail_home): """Rename local directory if needed.""" - hm = param_tools.get_global_parameter( - "handle_mailboxes", raise_exception=False) + hm = param_tools.get_global_parameter("handle_mailboxes", raise_exception=False) if not hm: return MailboxOperation.objects.create( @@ -219,15 +212,15 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject): self.address = address self.domain = domain self.quota_value = Quota.objects.create( - username=self.full_address, bytes=old_qvalue.bytes, - messages=old_qvalue.messages + username=self.full_address, + bytes=old_qvalue.bytes, + messages=old_qvalue.messages, ) old_qvalue.delete() self.rename_dir(old_mail_home) def delete_dir(self): - hm = param_tools.get_global_parameter( - "handle_mailboxes", raise_exception=False) + hm = param_tools.get_global_parameter("handle_mailboxes", raise_exception=False) if not hm: return MailboxOperation.objects.create(type="delete", argument=self.mail_home) @@ -277,16 +270,15 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject): """ if not self.quota: return 0 - return int( - self.quota_value.bytes / float(self.quota * 1048576) * 100 - ) + return int(self.quota_value.bytes / float(self.quota * 1048576) * 100) def post_create(self, creator): from modoboa.lib.permissions import grant_access_to_object + super().post_create(creator) conditions = ( creator.has_perm("admin.add_mailbox"), - not self.user.has_perm("admin.add_domain") + not self.user.has_perm("admin.add_domain"), ) if all(conditions): # An admin is creating a new mailbox. Give @@ -305,8 +297,9 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject): newaddress = None if values["email"] != self.full_address: newaddress = values["email"] - elif (self.user.role == "SimpleUsers" and - self.user.username != self.full_address): + elif ( + self.user.role == "SimpleUsers" and self.user.username != self.full_address + ): newaddress = self.user.username if newaddress is not None: local_part, domname = split_mailbox(newaddress) @@ -319,9 +312,10 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject): self.use_domain_quota = values["use_domain_quota"] if "use_domain_quota" in values or "quota" in values: override_rules = ( - not self.quota or user.is_superuser or - user.has_perm("admin.add_domain") and - not user.userobjectlimit_set.get(name="quota").max_value + not self.quota + or user.is_superuser + or user.has_perm("admin.add_domain") + and not user.userobjectlimit_set.get(name="quota").max_value ) self.set_quota(values["quota"], override_rules) if "message_limit" in values: @@ -343,11 +337,11 @@ class Mailbox(mixins.MessageLimitMixin, AdminObject): if self.pk: qset = qset.exclude(pk=self.pk) if qset.exists(): - raise lib_exceptions.Conflict( - _("Mailbox {} already exists").format(self)) + raise lib_exceptions.Conflict(_("Mailbox {} already exists").format(self)) if self.quota_value is None: self.quota_value, created = Quota.objects.get_or_create( - username=self.full_address) + username=self.full_address + ) super().save(*args, **kwargs) @@ -377,8 +371,9 @@ reversion.register(SenderAddress) class MailboxOperation(models.Model): """An operation on a mailbox.""" - mailbox = models.ForeignKey(Mailbox, blank=True, null=True, - on_delete=models.CASCADE) + mailbox = models.ForeignKey( + Mailbox, blank=True, null=True, on_delete=models.CASCADE + ) type = models.CharField( # NOQA:A003 max_length=20, choices=(("rename", "rename"), ("delete", "delete")) ) diff --git a/modoboa/admin/models/mixins.py b/modoboa/admin/models/mixins.py index 418c9ad12..848b8b6c0 100644 --- a/modoboa/admin/models/mixins.py +++ b/modoboa/admin/models/mixins.py @@ -15,9 +15,7 @@ class MessageLimitMixin: """Return number of sent messages for the current day.""" if self.message_limit is None: return None - return ( - self.message_limit - get_message_counter(self.message_counter_key) - ) + return self.message_limit - get_message_counter(self.message_counter_key) @property def sent_messages_in_percent(self): diff --git a/modoboa/admin/models/mxrecord.py b/modoboa/admin/models/mxrecord.py index c0653ffbe..e0d630c5c 100644 --- a/modoboa/admin/models/mxrecord.py +++ b/modoboa/admin/models/mxrecord.py @@ -29,8 +29,7 @@ class MXRecordManager(models.Manager): from .. import lib now = timezone.now() - records = self.get_queryset().filter( - domain=domain, updated__gt=now) + records = self.get_queryset().filter(domain=domain, updated__gt=now) if records.exists(): for record in records: yield record @@ -47,7 +46,8 @@ class MXRecordManager(models.Manager): domain=domain, name="{}".format(mx_addr.strip(".")), address="{}".format(mx_ip_addr), - updated=now + delta) + updated=now + delta, + ) yield record diff --git a/modoboa/admin/postfix_maps.py b/modoboa/admin/postfix_maps.py index d7c1eda64..1378997f9 100644 --- a/modoboa/admin/postfix_maps.py +++ b/modoboa/admin/postfix_maps.py @@ -1,7 +1,7 @@ """Map file definitions for postfix.""" -class DomainsMap(object): +class DomainsMap(object): """Map to list all domains.""" filename = "sql-domains.cf" @@ -10,8 +10,7 @@ class DomainsMap(object): "WHERE name='%s' AND type='domain' AND enabled=1" ) postgres = ( - "SELECT name FROM admin_domain " - "WHERE name='%s' AND type='domain' AND enabled" + "SELECT name FROM admin_domain " "WHERE name='%s' AND type='domain' AND enabled" ) sqlite = ( "SELECT name FROM admin_domain " @@ -20,7 +19,6 @@ class DomainsMap(object): class DomainsAliasesMap(object): - """Map to list all domain aliases.""" filename = "sql-domain-aliases.cf" @@ -42,7 +40,6 @@ class DomainsAliasesMap(object): class AliasesMap(object): - """A map to list all mailbox aliases.""" filename = "sql-aliases.cf" @@ -67,7 +64,6 @@ class AliasesMap(object): class MaintainMap(object): - """Map files to list non available mailboxes.""" filename = "sql-maintain.cf" diff --git a/modoboa/admin/signals.py b/modoboa/admin/signals.py index f5fdd32d4..69833fa5f 100644 --- a/modoboa/admin/signals.py +++ b/modoboa/admin/signals.py @@ -2,21 +2,21 @@ import django.dispatch -check_extra_account_form = django.dispatch.Signal() # Provides account, form -extra_account_dashboard_widgets = django.dispatch.Signal() # Provides user, account -extra_account_forms = django.dispatch.Signal() # Provides user, account -extra_admin_content = django.dispatch.Signal() # Provides user, location, currentpage -extra_domain_actions = django.dispatch.Signal() # Provides user, domain -extra_domain_dashboard_widgets = django.dispatch.Signal() # Provides user, domain +check_extra_account_form = django.dispatch.Signal() # Provides account, form +extra_account_dashboard_widgets = django.dispatch.Signal() # Provides user, account +extra_account_forms = django.dispatch.Signal() # Provides user, account +extra_admin_content = django.dispatch.Signal() # Provides user, location, currentpage +extra_domain_actions = django.dispatch.Signal() # Provides user, domain +extra_domain_dashboard_widgets = django.dispatch.Signal() # Provides user, domain extra_domain_filters = django.dispatch.Signal() -extra_domain_forms = django.dispatch.Signal() # Provides user, domain -extra_domain_menu_entries = django.dispatch.Signal() # Provides user -extra_domain_qset_filters = django.dispatch.Signal() # Provides domfilter, extrafilters +extra_domain_forms = django.dispatch.Signal() # Provides user, domain +extra_domain_menu_entries = django.dispatch.Signal() # Provides user +extra_domain_qset_filters = django.dispatch.Signal() # Provides domfilter, extrafilters # extra_domain_types = django.dispatch.Signal() extra_domain_wizard_steps = django.dispatch.Signal() -get_account_form_instances = django.dispatch.Signal() # Provides user, account -get_domain_form_instances = django.dispatch.Signal() # Provides user, domain +get_account_form_instances = django.dispatch.Signal() # Provides user, account +get_domain_form_instances = django.dispatch.Signal() # Provides user, domain # get_domain_tags = django.dispatch.Signal(providing_args=["domain"]) -import_object = django.dispatch.Signal() # Provides objtype -use_external_recipients = django.dispatch.Signal() # Provides recipients -extra_account_identities_actions = django.dispatch.Signal() # Provides account +import_object = django.dispatch.Signal() # Provides objtype +use_external_recipients = django.dispatch.Signal() # Provides recipients +extra_account_identities_actions = django.dispatch.Signal() # Provides account diff --git a/modoboa/admin/templatetags/admin_tags.py b/modoboa/admin/templatetags/admin_tags.py index 1e7e74bb8..0da358765 100644 --- a/modoboa/admin/templatetags/admin_tags.py +++ b/modoboa/admin/templatetags/admin_tags.py @@ -15,9 +15,7 @@ from .. import signals register = template.Library() -genders = { - "Enabled": (gettext_lazy("enabled_m"), gettext_lazy("enabled_f")) -} +genders = {"Enabled": (gettext_lazy("enabled_m"), gettext_lazy("enabled_f"))} @register.simple_tag @@ -43,47 +41,57 @@ def domains_menu(selection, user, ajax_mode=True): quota_list_url = domain_list_url + "#quotas/" logs_url = domain_list_url + "#logs/" entries = [ - {"name": "domains", - "label": _("List domains"), - "img": "fa fa-user", - "class": "ajaxnav navigation", - "url": domain_list_url}, - {"name": "quotas", - "label": _("List quotas"), - "img": "fa fa-hdd-o", - "class": "ajaxnav navigation", - "url": quota_list_url}, - {"name": "logs", - "label": _("Message logs"), - "img": "fa fa-list", - "class": "ajaxnav navigation", - "url": logs_url}, + { + "name": "domains", + "label": _("List domains"), + "img": "fa fa-user", + "class": "ajaxnav navigation", + "url": domain_list_url, + }, + { + "name": "quotas", + "label": _("List quotas"), + "img": "fa fa-hdd-o", + "class": "ajaxnav navigation", + "url": quota_list_url, + }, + { + "name": "logs", + "label": _("Message logs"), + "img": "fa fa-list", + "class": "ajaxnav navigation", + "url": logs_url, + }, ] if user.has_perm("admin.add_domain"): extra_entries = signals.extra_domain_menu_entries.send( - sender="domains_menu", user=user) + sender="domains_menu", user=user + ) for entry in extra_entries: entries += entry[1] entries += [ - {"name": "import", - "label": _("Import"), - "img": "fa fa-folder-open", - "url": reverse("admin:domain_import"), - "modal": True, - "modalcb": "admin.importform_cb"}, - {"name": "export", - "label": _("Export"), - "img": "fa fa-share-alt", - "url": reverse("admin:domain_export"), - "modal": True, - "modalcb": "admin.exportform_cb"} + { + "name": "import", + "label": _("Import"), + "img": "fa fa-folder-open", + "url": reverse("admin:domain_import"), + "modal": True, + "modalcb": "admin.importform_cb", + }, + { + "name": "export", + "label": _("Export"), + "img": "fa fa-share-alt", + "url": reverse("admin:domain_export"), + "modal": True, + "modalcb": "admin.exportform_cb", + }, ] - return render_to_string("common/menulist.html", { - "entries": entries, - "selection": selection, - "user": user - }) + return render_to_string( + "common/menulist.html", + {"entries": entries, "selection": selection, "user": user}, + ) @register.simple_tag @@ -103,71 +111,84 @@ def identities_menu(user, selection=None, ajax_mode=True): identity_list_url = reverse("admin:identity_list") quota_list_url = identity_list_url + "#quotas/" entries = [ - {"name": "identities", - "label": _("List identities"), - "img": "fa fa-user", - "class": nav_classes, - "url": identity_list_url}, - {"name": "quotas", - "label": _("List quotas"), - "img": "fa fa-hdd-o", - "class": nav_classes, - "url": quota_list_url}, - {"name": "import", - "label": _("Import"), - "img": "fa fa-folder-open", - "url": reverse("admin:identity_import"), - "modal": True, - "modalcb": "admin.importform_cb"}, - {"name": "export", - "label": _("Export"), - "img": "fa fa-share-alt", - "url": reverse("admin:identity_export"), - "modal": True, - "modalcb": "admin.exportform_cb"} + { + "name": "identities", + "label": _("List identities"), + "img": "fa fa-user", + "class": nav_classes, + "url": identity_list_url, + }, + { + "name": "quotas", + "label": _("List quotas"), + "img": "fa fa-hdd-o", + "class": nav_classes, + "url": quota_list_url, + }, + { + "name": "import", + "label": _("Import"), + "img": "fa fa-folder-open", + "url": reverse("admin:identity_import"), + "modal": True, + "modalcb": "admin.importform_cb", + }, + { + "name": "export", + "label": _("Export"), + "img": "fa fa-share-alt", + "url": reverse("admin:identity_export"), + "modal": True, + "modalcb": "admin.exportform_cb", + }, ] - return render_to_string("common/menulist.html", { - "entries": entries, - "user": user - }) + return render_to_string("common/menulist.html", {"entries": entries, "user": user}) @register.simple_tag def domain_actions(user, domain): actions = [ - {"name": "listidentities", - "url": u"{0}#list/?searchquery=@{1}".format( - reverse("admin:identity_list"), domain.name), - "title": _("View the domain's identities"), - "img": "fa fa-user"}, + { + "name": "listidentities", + "url": "{0}#list/?searchquery=@{1}".format( + reverse("admin:identity_list"), domain.name + ), + "title": _("View the domain's identities"), + "img": "fa fa-user", + }, ] if domain.alarms.opened().exists(): - actions.append({ - "name": "listalarms", - "url": reverse("admin:domain_alarms", args=[domain.pk]), - "title": _("View domain's alarms"), - "img": "fa fa-bell" - }) + actions.append( + { + "name": "listalarms", + "url": reverse("admin:domain_alarms", args=[domain.pk]), + "title": _("View domain's alarms"), + "img": "fa fa-bell", + } + ) if user.has_perm("admin.change_domain"): - actions.append({ - "name": "editdomain", - "title": _("Edit {}").format(domain), - "url": reverse("admin:domain_change", args=[domain.pk]), - "modal": True, - "modalcb": "admin.domainform_cb", - "img": "fa fa-edit" - }) + actions.append( + { + "name": "editdomain", + "title": _("Edit {}").format(domain), + "url": reverse("admin:domain_change", args=[domain.pk]), + "modal": True, + "modalcb": "admin.domainform_cb", + "img": "fa fa-edit", + } + ) if user.has_perm("admin.delete_domain"): - actions.append({ - "name": "deldomain", - "url": reverse("admin:domain_delete", args=[domain.id]), - "title": _("Delete %s?") % domain.name, - "img": "fa fa-trash" - }) + actions.append( + { + "name": "deldomain", + "url": reverse("admin:domain_delete", args=[domain.id]), + "title": _("Delete %s?") % domain.name, + "img": "fa fa-trash", + } + ) - responses = signals.extra_domain_actions.send( - sender=None, user=user, domain=domain) + responses = signals.extra_domain_actions.send(sender=None, user=user, domain=domain) for _receiver, response in responses: if response: actions += response @@ -182,38 +203,43 @@ def identity_actions(user, ident): if name == "User": actions = [] result = core_signals.extra_account_actions.send( - sender="identity_actions", account=ident) + sender="identity_actions", account=ident + ) for action in result: actions += action[1] - url = ( - reverse("admin:account_change", args=[objid]) + - "?active_tab=default" - ) + url = reverse("admin:account_change", args=[objid]) + "?active_tab=default" actions += [ - {"name": "changeaccount", - "url": url, - "img": "fa fa-edit", - "modal": True, - "modalcb": "admin.editaccount_cb", - "title": _("Edit {}").format(ident.username)}, - {"name": "delaccount", - "url": reverse("admin:account_delete", args=[objid]), - "img": "fa fa-trash", - "title": _("Delete %s?") % ident.username}, + { + "name": "changeaccount", + "url": url, + "img": "fa fa-edit", + "modal": True, + "modalcb": "admin.editaccount_cb", + "title": _("Edit {}").format(ident.username), + }, + { + "name": "delaccount", + "url": reverse("admin:account_delete", args=[objid]), + "img": "fa fa-trash", + "title": _("Delete %s?") % ident.username, + }, ] else: actions = [ - {"name": "changealias", - "url": reverse("admin:alias_change", args=[objid]), - "img": "fa fa-edit", - "modal": True, - "modalcb": "admin.aliasform_cb", - "title": _("Edit {}").format(ident)}, - {"name": "delalias", - "url": "{}?selection={}".format( - reverse("admin:alias_delete"), objid), - "img": "fa fa-trash", - "title": _("Delete %s?") % ident.address}, + { + "name": "changealias", + "url": reverse("admin:alias_change", args=[objid]), + "img": "fa fa-edit", + "modal": True, + "modalcb": "admin.aliasform_cb", + "title": _("Edit {}").format(ident), + }, + { + "name": "delalias", + "url": "{}?selection={}".format(reverse("admin:alias_delete"), objid), + "img": "fa fa-trash", + "title": _("Delete %s?") % ident.address, + }, ] return render_actions(actions) @@ -222,8 +248,7 @@ def identity_actions(user, ident): def check_identity_status(identity): """Check if identity is enabled or not.""" if identity.__class__.__name__ == "User": - if hasattr(identity, "mailbox") \ - and not identity.mailbox.domain.enabled: + if hasattr(identity, "mailbox") and not identity.mailbox.domain.enabled: return False elif not identity.is_active: return False @@ -271,13 +296,16 @@ def identity_modify_link(identity, active_tab="default"): @register.simple_tag def domadmin_actions(daid, domid): - actions = [{ - "name": "removeperm", - "url": "{0}?domid={1}&daid={2}".format( - reverse("admin:permission_remove"), domid, daid), - "img": "fa fa-trash", - "title": _("Remove this permission") - }] + actions = [ + { + "name": "removeperm", + "url": "{0}?domid={1}&daid={2}".format( + reverse("admin:permission_remove"), domid, daid + ), + "img": "fa fa-trash", + "title": _("Remove this permission"), + } + ] return render_actions(actions) @@ -294,7 +322,10 @@ def gender(value, target): def get_extra_admin_content(user, target, currentpage): results = signals.extra_admin_content.send( sender="get_extra_admin_content", - user=user, location=target, currentpage=currentpage) + user=user, + location=target, + currentpage=currentpage, + ) if not results: return "" results = reduce(lambda a, b: a + b, [result[1] for result in results]) diff --git a/modoboa/admin/tests/test_account.py b/modoboa/admin/tests/test_account.py index 6f1c7ab88..0fd77ff20 100644 --- a/modoboa/admin/tests/test_account.py +++ b/modoboa/admin/tests/test_account.py @@ -19,9 +19,10 @@ class AuthenticationTestCase(ModoTestCase): """Create test data.""" super(AuthenticationTestCase, cls).setUpTestData() cls.mb = factories.MailboxFactory( - domain__name="test.com", address="user", + domain__name="test.com", + address="user", user__username="user@test.com", - user__groups=("SimpleUsers",) + user__groups=("SimpleUsers",), ) def test_authentication_unicode(self): @@ -46,10 +47,16 @@ class AccountTestCase(ModoTestCase): def test_crud(self): values = { - "username": "tester@test.com", "first_name": "Tester", - "last_name": "Toto", "password1": "Toto1234", - "password2": "Toto1234", "role": "SimpleUsers", "quota_act": True, - "is_active": True, "email": "tester@test.com", "stepid": "step2" + "username": "tester@test.com", + "first_name": "Tester", + "last_name": "Toto", + "password1": "Toto1234", + "password2": "Toto1234", + "role": "SimpleUsers", + "quota_act": True, + "is_active": True, + "email": "tester@test.com", + "stepid": "step2", } self.ajax_post(reverse("admin:account_add"), values) @@ -66,19 +73,24 @@ class AccountTestCase(ModoTestCase): self.assertEqual(mb.domain.mailbox_count, 3) # Check if self alias has been created self.assertTrue( - models.AliasRecipient.objects.select_related("alias").filter( - alias__address=mb.full_address, address=mb.full_address, - alias__internal=True).exists() + models.AliasRecipient.objects.select_related("alias") + .filter( + alias__address=mb.full_address, + address=mb.full_address, + alias__internal=True, + ) + .exists() ) - values.update({ - "username": "pouet@test.com", "language": "en", - "secondary_email": "homer@simpson.com", - "phone_number": "+33612345678" - }) - self.ajax_post( - reverse("admin:account_change", args=[account.id]), values + values.update( + { + "username": "pouet@test.com", + "language": "en", + "secondary_email": "homer@simpson.com", + "phone_number": "+33612345678", + } ) + self.ajax_post(reverse("admin:account_change", args=[account.id]), values) account.refresh_from_db() self.assertEqual(account.secondary_email, values["secondary_email"]) mb = models.Mailbox.objects.get(pk=mb.pk) @@ -86,18 +98,24 @@ class AccountTestCase(ModoTestCase): self.assertEqual(mb.quota_value.username, "pouet@test.com") # Check if self alias has been updated self.assertTrue( - models.AliasRecipient.objects.select_related("alias").filter( - alias__address=mb.full_address, address=mb.full_address, - alias__internal=True).exists() - ) - self.ajax_post( - reverse("admin:account_delete", args=[account.id]), {} + models.AliasRecipient.objects.select_related("alias") + .filter( + alias__address=mb.full_address, + address=mb.full_address, + alias__internal=True, + ) + .exists() ) + self.ajax_post(reverse("admin:account_delete", args=[account.id]), {}) # Check if self alias has been deleted self.assertFalse( - models.AliasRecipient.objects.select_related("alias").filter( - alias__address=mb.full_address, address=mb.full_address, - alias__internal=True).exists() + models.AliasRecipient.objects.select_related("alias") + .filter( + alias__address=mb.full_address, + address=mb.full_address, + alias__internal=True, + ) + .exists() ) @override_settings(LANGUAGE_CODE="fr") @@ -113,7 +131,7 @@ class AccountTestCase(ModoTestCase): "quota_act": True, "is_active": True, "email": "tester@test.com", - "stepid": "step2" + "stepid": "step2", } self.ajax_post(reverse("admin:account_add"), values) account = User.objects.get(username=values["username"]) @@ -148,11 +166,9 @@ class AccountTestCase(ModoTestCase): } self.ajax_post(reverse("admin:account_add"), values) # Check that internal alias has not been updated - alias = models.Alias.objects.get( - address="user@test.com", internal=True) + alias = models.Alias.objects.get(address="user@test.com", internal=True) self.assertTrue(alias.aliasrecipient_set.count() == 1) - alias = models.Alias.objects.get( - address="user@test.com", internal=False) + alias = models.Alias.objects.get(address="user@test.com", internal=False) self.assertTrue(alias.aliasrecipient_set.count() == 1) def test_create_disabled_account(self): @@ -167,7 +183,7 @@ class AccountTestCase(ModoTestCase): "quota_act": True, "is_active": False, "email": "tester@test.com", - "stepid": "step2" + "stepid": "step2", } self.ajax_post(reverse("admin:account_add"), values) account = User.objects.get(username=values["username"]) @@ -186,19 +202,15 @@ class AccountTestCase(ModoTestCase): "quota_act": True, "is_active": False, "email": "user@test.com", - "language": "en" + "language": "en", } - self.ajax_post( - reverse("admin:account_change", args=[account.id]), values - ) + 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 - ) + self.ajax_post(reverse("admin:account_change", args=[account.id]), values) internal.refresh_from_db() self.assertTrue(internal.enabled) forward.refresh_from_db() @@ -217,7 +229,7 @@ class AccountTestCase(ModoTestCase): "email": "new_user@test.com", "language": "en", "create_alias_with_old_address": False, - "aliases_1": "alias@test.com" + "aliases_1": "alias@test.com", } url = reverse("admin:account_change", args=[account.pk]) self.ajax_post(url, values) @@ -237,42 +249,48 @@ class AccountTestCase(ModoTestCase): "is_active": account.is_active, "email": "new_user@test.com", "language": "en", - "create_alias_with_old_address": False + "create_alias_with_old_address": False, } url = reverse("admin:account_change", args=[account.pk]) # Rename while option is set to False -> no alias created self.ajax_post(url, values) self.assertFalse( models.AliasRecipient.objects.filter( - address="new_user@test.com", alias__address="user@test.com", - alias__internal=False + address="new_user@test.com", + alias__address="user@test.com", + alias__internal=False, ).exists() ) # Now rename while option set to True -> alias created - values.update({ - "username": "user@test.com", - "email": "user@test.com", - "create_alias_with_old_address": True} + values.update( + { + "username": "user@test.com", + "email": "user@test.com", + "create_alias_with_old_address": True, + } ) self.ajax_post(url, values) self.assertTrue( models.AliasRecipient.objects.filter( - address="user@test.com", alias__address="new_user@test.com", - alias__internal=False + address="user@test.com", + alias__address="new_user@test.com", + alias__internal=False, ).exists() ) # Change domain while option set to True -> alias created - values.update({ - "username": "new_user@test2.com", - "email": "new_user@test2.com", - }) + values.update( + { + "username": "new_user@test2.com", + "email": "new_user@test2.com", + } + ) self.ajax_post(url, values) self.assertTrue( models.AliasRecipient.objects.filter( address="new_user@test2.com", alias__address="user@test.com", alias__domain__name="test.com", - alias__internal=False + alias__internal=False, ).exists() ) @@ -280,38 +298,45 @@ class AccountTestCase(ModoTestCase): """Check password constraints.""" values = { "username": "tester@test.com", - "first_name": "Tester", "last_name": "Toto", - "password1": "", "password2": "", + "first_name": "Tester", + "last_name": "Toto", + "password1": "", + "password2": "", "role": "SimpleUsers", - "quota_act": True, "is_active": True, "email": "tester@test.com", - "stepid": "step2" + "quota_act": True, + "is_active": True, + "email": "tester@test.com", + "stepid": "step2", } resp = self.ajax_post(reverse("admin:account_add"), values, 400) - self.assertEqual( - resp["form_errors"]["password1"][0], - "This field is required.") + self.assertEqual(resp["form_errors"]["password1"][0], "This field is required.") values["password1"] = "Toto1234" values["password2"] = "Toto12345" resp = self.ajax_post(reverse("admin:account_add"), values, 400) self.assertEqual( - resp["form_errors"]["password2"][0], - "The two password fields didn't match.") + resp["form_errors"]["password2"][0], "The two password fields didn't match." + ) values["password1"] = "toto1234" values["password2"] = "toto1234" resp = self.ajax_post(reverse("admin:account_add"), values, 400) self.assertEqual( resp["form_errors"]["password2"][0], - "Password must contain at least 1 uppercase letter.") + "Password must contain at least 1 uppercase letter.", + ) def test_random_password(self): """Try to create an account with a random password.""" values = { "username": "tester@test.com", - "first_name": "Tester", "last_name": "Toto", - "random_password": True, "role": "SimpleUsers", - "quota_act": True, "is_active": True, "email": "tester@test.com", - "stepid": "step2" + "first_name": "Tester", + "last_name": "Toto", + "random_password": True, + "role": "SimpleUsers", + "quota_act": True, + "is_active": True, + "email": "tester@test.com", + "stepid": "step2", } self.ajax_post(reverse("admin:account_add"), values) @@ -320,62 +345,56 @@ class AccountTestCase(ModoTestCase): values["language"] = "en" # Since 'random_password' is still True, a new password should # be generated - self.ajax_post( - reverse("admin:account_change", args=[account.pk]), values - ) + self.ajax_post(reverse("admin:account_change", args=[account.pk]), values) account.refresh_from_db() self.assertNotEqual(password, account.password) password = account.password values["random_password"] = False - self.ajax_post( - reverse("admin:account_change", args=[account.pk]), values - ) + self.ajax_post(reverse("admin:account_change", args=[account.pk]), values) account.refresh_from_db() self.assertEqual(password, account.password) def test_delete_default_superadmin(self): """Delete default superadmin.""" - sadmin2 = core_factories.UserFactory( - username="admin2", is_superuser=True) + sadmin2 = core_factories.UserFactory(username="admin2", is_superuser=True) sadmin = User.objects.get(username="admin") self.client.force_login(sadmin2) - self.ajax_post( - reverse("admin:account_delete", args=[sadmin.pk]), {} - ) + self.ajax_post(reverse("admin:account_delete", args=[sadmin.pk]), {}) values = { - "username": "user@test.com", "role": "DomainAdmins", - "is_active": True, "email": "user@test.com", - "language": "en" + "username": "user@test.com", + "role": "DomainAdmins", + "is_active": True, + "email": "user@test.com", + "language": "en", } account = User.objects.get(username="user@test.com") - self.ajax_post( - reverse("admin:account_change", args=[account.pk]), values - ) + self.ajax_post(reverse("admin:account_change", args=[account.pk]), values) def test_sender_address(self): """Check if sender addresses are saved.""" account = User.objects.get(username="user@test.com") values = { - "username": "user@test.com", "first_name": "Tester", - "last_name": "Toto", "role": "SimpleUsers", - "quota_act": True, "is_active": True, "email": "user@test.com", - "senderaddress": "test@titi.com", "senderaddress_1": "toto@go.com", - "language": "en" + "username": "user@test.com", + "first_name": "Tester", + "last_name": "Toto", + "role": "SimpleUsers", + "quota_act": True, + "is_active": True, + "email": "user@test.com", + "senderaddress": "test@titi.com", + "senderaddress_1": "toto@go.com", + "language": "en", } - self.ajax_post( - reverse("admin:account_change", args=[account.pk]), values) + self.ajax_post(reverse("admin:account_change", args=[account.pk]), values) self.assertEqual( - models.SenderAddress.objects.filter( - mailbox__address="user").count(), - 2) + models.SenderAddress.objects.filter(mailbox__address="user").count(), 2 + ) del values["senderaddress_1"] - self.ajax_post( - reverse("admin:account_change", args=[account.pk]), values) + self.ajax_post(reverse("admin:account_change", args=[account.pk]), values) self.assertEqual( - models.SenderAddress.objects.filter( - mailbox__address="user").count(), - 1) + models.SenderAddress.objects.filter(mailbox__address="user").count(), 1 + ) def test_sender_address_as_domainadmin(self): """Check that restrictions are applied.""" @@ -383,63 +402,83 @@ class AccountTestCase(ModoTestCase): self.client.force_login(admin) account = User.objects.get(username="user@test.com") values = { - "username": "user@test.com", "first_name": "Tester", - "last_name": "Toto", "role": "SimpleUsers", - "quota_act": True, "is_active": True, "email": "user@test.com", + "username": "user@test.com", + "first_name": "Tester", + "last_name": "Toto", + "role": "SimpleUsers", + "quota_act": True, + "is_active": True, + "email": "user@test.com", "senderaddress": "test@titi.com", - "senderaddress_1": "toto@test2.com" + "senderaddress_1": "toto@test2.com", } response = self.ajax_post( - reverse("admin:account_change", args=[account.pk]), values, 400) + reverse("admin:account_change", args=[account.pk]), values, 400 + ) self.assertEqual( response["form_errors"]["senderaddress_1"][0], - "You don't have access to this domain") + "You don't have access to this domain", + ) def test_conflicts(self): """Check if unicity constraints are respected.""" values = { "username": "user@test.com", - "password1": "Toto1234", "password2": "Toto1234", - "role": "SimpleUsers", "quota_act": True, - "is_active": True, "email": "user@test.com", - "stepid": "step2" + "password1": "Toto1234", + "password2": "Toto1234", + "role": "SimpleUsers", + "quota_act": True, + "is_active": True, + "email": "user@test.com", + "stepid": "step2", } self.ajax_post(reverse("admin:account_add"), values, status=400) - values.update({"username": "fakeuser@test.com", - "email": "fakeuser@test.com"}) + values.update({"username": "fakeuser@test.com", "email": "fakeuser@test.com"}) self.ajax_post(reverse("admin:account_add"), values) account = User.objects.get(username="fakeuser@test.com") values = { "username": "user@test.com", - "role": "SimpleUsers", "quota_act": True, - "is_active": True, "email": "user@test.com", + "role": "SimpleUsers", + "quota_act": True, + "is_active": True, + "email": "user@test.com", } self.ajax_post( - reverse("admin:account_change", args=[account.pk]), values, - status=400 + reverse("admin:account_change", args=[account.pk]), values, status=400 ) def test_utf8_username(self): """Create an account with non-ASCII characters.""" values = { - "username": "téster@test.com", "first_name": "Tester", - "last_name": "Toto", "password1": "Toto1234", - "password2": "Toto1234", "role": "SimpleUsers", "quota_act": True, - "is_active": True, "email": "téster@test.com", "stepid": "step2" + "username": "téster@test.com", + "first_name": "Tester", + "last_name": "Toto", + "password1": "Toto1234", + "password2": "Toto1234", + "role": "SimpleUsers", + "quota_act": True, + "is_active": True, + "email": "téster@test.com", + "stepid": "step2", } self.ajax_post(reverse("admin:account_add"), values) def _set_quota(self, email, value, expected_status=200): account = User.objects.get(username=email) values = { - "username": email, "role": "SimpleUsers", "quota_act": False, - "is_active": True, "quota": value, "email": email, - "language": "en" + "username": email, + "role": "SimpleUsers", + "quota_act": False, + "is_active": True, + "quota": value, + "email": email, + "language": "en", } self.ajax_post( reverse("admin:account_change", args=[account.id]), - values, status=expected_status + values, + status=expected_status, ) def test_set_nul_quota_as_superadmin(self): @@ -448,15 +487,11 @@ class AccountTestCase(ModoTestCase): def test_set_nul_quota_as_domainadmin(self): """Check cases where a domain admin set unlimited quota.""" self.client.logout() - self.assertTrue( - self.client.login(username="admin@test.com", password="toto") - ) + self.assertTrue(self.client.login(username="admin@test.com", password="toto")) # Fails because domain has a quota self._set_quota("user@test.com", 0, 400) self.client.logout() - self.assertTrue( - self.client.login(username="admin@test2.com", password="toto") - ) + self.assertTrue(self.client.login(username="admin@test2.com", password="toto")) # Ok because domain has no quota self._set_quota("user@test2.com", 0) @@ -477,25 +512,29 @@ class AccountTestCase(ModoTestCase): def test_master_user(self): """Validate the master user mode.""" values = { - "username": "masteruser", "role": "SuperAdmins", + "username": "masteruser", + "role": "SuperAdmins", "quota_act": False, - "is_active": True, "master_user": True, "stepid": "step2", - "password1": "Toto1234", "password2": "Toto1234" + "is_active": True, + "master_user": True, + "stepid": "step2", + "password1": "Toto1234", + "password2": "Toto1234", } - self.ajax_post( - reverse("admin:account_add"), values - ) + self.ajax_post(reverse("admin:account_add"), values) self.assertTrue(User.objects.get(username="masteruser").master_user) values = { - "username": "testuser", "role": "DomainAdmins", + "username": "testuser", + "role": "DomainAdmins", "quota_act": False, - "is_active": True, "master_user": True, "stepid": "step2", - "password1": "Toto1234", "password2": "Toto1234" + "is_active": True, + "master_user": True, + "stepid": "step2", + "password1": "Toto1234", + "password2": "Toto1234", } - self.ajax_post( - reverse("admin:account_add"), values, status=400 - ) + self.ajax_post(reverse("admin:account_add"), values, status=400) def test_account_detail_view(self): """Test account detail view.""" @@ -513,8 +552,7 @@ class AccountTestCase(ModoTestCase): def test_quota_list_view(self): """Test quota list view.""" - models.Quota.objects.filter(username="user@test.com").update( - bytes=5 * 1048576) + models.Quota.objects.filter(username="user@test.com").update(bytes=5 * 1048576) url = reverse("admin:identity_quota_list") response = self.ajax_get(url) self.assertIn("5M", response["rows"]) @@ -522,24 +560,23 @@ class AccountTestCase(ModoTestCase): self.assertIn("user@test.com", response["rows"]) old_rows = response["rows"] - response = self.ajax_get( - "{}?sort_order=-quota_value__bytes".format(url)) + response = self.ajax_get("{}?sort_order=-quota_value__bytes".format(url)) self.assertNotEqual(old_rows, response["rows"]) old_rows = response["rows"] - response = self.ajax_get( - "{}?sort_order=-quota_usage".format(url)) + response = self.ajax_get("{}?sort_order=-quota_usage".format(url)) self.assertEqual(old_rows, response["rows"]) - response = self.ajax_get( - "{}?sort_order=-unknown".format(url), status=400) + response = self.ajax_get("{}?sort_order=-unknown".format(url), status=400) @skipIf(NO_LDAP, "No ldap module installed") -@override_settings(AUTHENTICATION_BACKENDS=( - "modoboa.lib.authbackends.LDAPBackend", - "django.contrib.auth.backends.ModelBackend", -)) +@override_settings( + AUTHENTICATION_BACKENDS=( + "modoboa.lib.authbackends.LDAPBackend", + "django.contrib.auth.backends.ModelBackend", + ) +) class LDAPAccountTestCase(test_ldap.LDAPTestCaseMixin, ModoTestCase): """Check LDAP related code.""" @@ -550,10 +587,8 @@ class LDAPAccountTestCase(test_ldap.LDAPTestCaseMixin, ModoTestCase): self.set_global_parameter("auto_create_domain_and_mailbox", False) username = "testuser@example.com" self.authenticate(username, "test") - self.assertFalse( - models.Domain.objects.filter(name="example.com").exists()) - self.assertFalse( - models.Mailbox.objects.filter(address="testuser").exists()) + self.assertFalse(models.Domain.objects.filter(name="example.com").exists()) + self.assertFalse(models.Mailbox.objects.filter(address="testuser").exists()) class PermissionsTestCase(ModoTestCase): @@ -576,9 +611,12 @@ class PermissionsTestCase(ModoTestCase): """Initiate test context.""" super(PermissionsTestCase, self).setUp() self.values = { - "username": self.user.username, "role": "DomainAdmins", - "is_active": self.user.is_active, "email": "user@test.com", - "quota_act": True, "language": "en" + "username": self.user.username, + "role": "DomainAdmins", + "is_active": self.user.is_active, + "email": "user@test.com", + "quota_act": True, + "language": "en", } def tearDown(self): @@ -588,8 +626,7 @@ class PermissionsTestCase(ModoTestCase): factories.DomainFactory(name="test2.com") self.ajax_post( - reverse("admin:account_change", args=[self.user.id]), - self.values + reverse("admin:account_change", args=[self.user.id]), self.values ) self.assertEqual(self.user.role, "DomainAdmins") @@ -599,8 +636,7 @@ class PermissionsTestCase(ModoTestCase): self.values["domains_1"] = "test.com" self.values["domains_2"] = "test2.com" self.ajax_post( - reverse("admin:account_change", args=[self.user.id]), - self.values + reverse("admin:account_change", args=[self.user.id]), self.values ) self.assertIn( self.user, models.Domain.objects.get(name="test.com").admins.all() @@ -611,8 +647,7 @@ class PermissionsTestCase(ModoTestCase): del self.values["domains_2"] self.ajax_post( - reverse("admin:account_change", args=[self.user.id]), - self.values + reverse("admin:account_change", args=[self.user.id]), self.values ) self.assertIn( self.user, models.Domain.objects.get(name="test.com").admins.all() @@ -623,12 +658,9 @@ class PermissionsTestCase(ModoTestCase): self.values["role"] = "SimpleUsers" self.ajax_post( - reverse("admin:account_change", args=[self.user.id]), - self.values - ) - self.assertFalse( - self.user.groups.filter(name="DomainAdmins").exists() + reverse("admin:account_change", args=[self.user.id]), self.values ) + self.assertFalse(self.user.groups.filter(name="DomainAdmins").exists()) self.assertNotIn( self.user, models.Domain.objects.get(name="test.com").admins.all() ) @@ -636,46 +668,36 @@ class PermissionsTestCase(ModoTestCase): def test_superusers(self): self.values["role"] = "SuperAdmins" self.ajax_post( - reverse("admin:account_change", args=[self.user.id]), - self.values - ) - self.assertEqual( - User.objects.get(username="user@test.com").is_superuser, True + reverse("admin:account_change", args=[self.user.id]), self.values ) + self.assertEqual(User.objects.get(username="user@test.com").is_superuser, True) self.values["role"] = "SimpleUsers" self.ajax_post( - reverse("admin:account_change", args=[self.user.id]), - self.values + reverse("admin:account_change", args=[self.user.id]), self.values ) - self.assertEqual(User.objects.get( - username="user@test.com").is_superuser, False) + self.assertEqual(User.objects.get(username="user@test.com").is_superuser, False) def test_self_modif(self): self.client.logout() - self.assertTrue( - self.client.login(username="admin@test.com", password="toto") - ) + self.assertTrue(self.client.login(username="admin@test.com", password="toto")) admin = User.objects.get(username="admin@test.com") values = { - "username": "admin@test.com", "first_name": "Admin", - "password1": "", "password2": "", - "quota": 10, "is_active": True, "email": "admin@test.com", - "language": "en" + "username": "admin@test.com", + "first_name": "Admin", + "password1": "", + "password2": "", + "quota": 10, + "is_active": True, + "email": "admin@test.com", + "language": "en", } - self.ajax_post( - reverse("admin:account_change", args=[admin.id]), - values - ) + self.ajax_post(reverse("admin:account_change", args=[admin.id]), values) self.assertEqual(admin.role, "DomainAdmins") - self.assertTrue(admin.can_access( - models.Domain.objects.get(name="test.com"))) + self.assertTrue(admin.can_access(models.Domain.objects.get(name="test.com"))) values["role"] = "SuperAdmins" - self.ajax_post( - reverse("admin:account_change", args=[admin.id]), - values - ) + self.ajax_post(reverse("admin:account_change", args=[admin.id]), values) admin.refresh_from_db() self.assertEqual(admin.role, "DomainAdmins") @@ -683,33 +705,31 @@ class PermissionsTestCase(ModoTestCase): self.client.login(username=self.reseller.username, password="toto") self.assertTrue(self.reseller.can_access(self.reseller)) values = { - "username": self.reseller.username, "first_name": "Reseller", - "password1": "", "password2": "", - "is_active": True, "language": "en" + "username": self.reseller.username, + "first_name": "Reseller", + "password1": "", + "password2": "", + "is_active": True, + "language": "en", } - self.ajax_post( - reverse("admin:account_change", args=[self.reseller.pk]), - values - ) + self.ajax_post(reverse("admin:account_change", args=[self.reseller.pk]), values) self.assertEqual(self.reseller.role, "Resellers") values["role"] = "SuperAdmins" - self.ajax_post( - reverse("admin:account_change", args=[self.reseller.pk]), - values - ) + self.ajax_post(reverse("admin:account_change", args=[self.reseller.pk]), values) self.assertEqual(self.reseller.role, "Resellers") def test_domadmin_access(self): self.client.logout() self.assertEqual( - self.client.login(username="admin@test.com", password="toto"), - True) + self.client.login(username="admin@test.com", password="toto"), True + ) response = self.client.get(reverse("admin:domain_list")) self.assertEqual(response.status_code, 200) response = self.client.get( reverse("admin:account_change", args=[self.user.id]), - HTTP_X_REQUESTED_WITH="XMLHttpRequest") + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) self.assertNotEqual(response["Content-Type"], "application/json") def test_domainadmin_deletes_superadmin(self): @@ -720,21 +740,21 @@ class PermissionsTestCase(ModoTestCase): to access the super admin. """ values = { - "username": "superadmin2@test.com", "first_name": "Super", - "last_name": "Admin", "password1": "Toto1234", - "password2": "Toto1234", "role": "SuperAdmins", "is_active": True, - "email": "superadmin2@test.com", "stepid": "step2" + "username": "superadmin2@test.com", + "first_name": "Super", + "last_name": "Admin", + "password1": "Toto1234", + "password2": "Toto1234", + "role": "SuperAdmins", + "is_active": True, + "email": "superadmin2@test.com", + "stepid": "step2", } - self.ajax_post( - reverse("admin:account_add"), - values - ) + self.ajax_post(reverse("admin:account_add"), values) account = User.objects.get(username="superadmin2@test.com") self.client.logout() self.client.login(username="admin@test.com", password="toto") - self.ajax_post( - reverse("admin:account_delete", args=[account.id]), {}, 403 - ) + self.ajax_post(reverse("admin:account_delete", args=[account.id]), {}, 403) def test_domainadmin_dlist_local_domain_not_owned(self): """Check if a domain admin can use a local mailbox he can't @@ -743,22 +763,25 @@ class PermissionsTestCase(ModoTestCase): "address": "all@test.com", "recipients": "user@test.com", "recipients_1": "user@test2.com", - "enabled": True + "enabled": True, } self.ajax_post(reverse("admin:alias_add"), values) def test_domainadmin_master_user(self): """Check domain administrator is not allowed to access this feature.""" values = { - "username": "user10@test.com", "first_name": "Test", - "last_name": "Test", "password1": "Toto1234", - "password2": "Toto1234", "role": "SimpleUsers", "is_active": True, - "master_user": True, "email": "user10@test.com", "stepid": "step2" + "username": "user10@test.com", + "first_name": "Test", + "last_name": "Test", + "password1": "Toto1234", + "password2": "Toto1234", + "role": "SimpleUsers", + "is_active": True, + "master_user": True, + "email": "user10@test.com", + "stepid": "step2", } - self.ajax_post( - reverse("admin:account_add"), - values, status=400 - ) + self.ajax_post(reverse("admin:account_add"), values, status=400) def test_domadmins_permissions(self): """ @@ -767,17 +790,24 @@ class PermissionsTestCase(ModoTestCase): """ dom = models.Domain.objects.get(name="test.com") mb = factories.MailboxFactory( - domain=dom, address="admin2", - user__username="admin2@test.com", user__groups=("DomainAdmins", ), - user__password="{PLAIN}toto") + domain=dom, + address="admin2", + user__username="admin2@test.com", + user__groups=("DomainAdmins",), + user__password="{PLAIN}toto", + ) dom.add_admin(mb.user) self.client.logout() - self.assertTrue( - self.client.login(username="admin@test.com", password="toto")) + self.assertTrue(self.client.login(username="admin@test.com", password="toto")) values = { - "username": "new@test.com", "password1": "Toto1234", - "password2": "Toto1234", "role": "SimpleUsers", "quota_act": True, - "is_active": True, "email": "new@test.com", "stepid": "step2" + "username": "new@test.com", + "password1": "Toto1234", + "password2": "Toto1234", + "role": "SimpleUsers", + "quota_act": True, + "is_active": True, + "email": "new@test.com", + "stepid": "step2", } self.ajax_post(reverse("admin:account_add"), values) diff --git a/modoboa/admin/tests/test_alarms.py b/modoboa/admin/tests/test_alarms.py index 560dde817..07691f3f0 100644 --- a/modoboa/admin/tests/test_alarms.py +++ b/modoboa/admin/tests/test_alarms.py @@ -16,8 +16,7 @@ class AlarmsTestCase(ModoTestCase): def test_set_forward(self): domain = Domain.objects.all().first() - alarm = Alarm.objects.create( - domain=domain, mailbox=None, title="Test alarm 3") + alarm = Alarm.objects.create(domain=domain, mailbox=None, title="Test alarm 3") alarm.save() self.assertEqual(alarm.status, 1) alarm.close() diff --git a/modoboa/admin/tests/test_alias.py b/modoboa/admin/tests/test_alias.py index ffb468e0b..94d3a57b7 100644 --- a/modoboa/admin/tests/test_alias.py +++ b/modoboa/admin/tests/test_alias.py @@ -19,29 +19,28 @@ class AliasTestCase(ModoTestCase): def test_alias(self): user = User.objects.get(username="user@test.com") values = { - "username": "user@test.com", "role": user.role, - "is_active": user.is_active, "email": "user@test.com", - "aliases": "toto@test.com", "aliases_1": "titi@test.com", - "language": "en" + "username": "user@test.com", + "role": user.role, + "is_active": user.is_active, + "email": "user@test.com", + "aliases": "toto@test.com", + "aliases_1": "titi@test.com", + "language": "en", } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) - self.assertEqual( - AliasRecipient.objects.filter - (alias__internal=False, r_mailbox=user.mailbox) - .count(), 2 - ) - del values["aliases_1"] - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) + self.ajax_post(reverse("admin:account_change", args=[user.id]), values) self.assertEqual( AliasRecipient.objects.filter( - alias__internal=False, r_mailbox=user.mailbox) - .count(), 1 + alias__internal=False, r_mailbox=user.mailbox + ).count(), + 2, + ) + del values["aliases_1"] + self.ajax_post(reverse("admin:account_change", args=[user.id]), values) + self.assertEqual( + AliasRecipient.objects.filter( + alias__internal=False, r_mailbox=user.mailbox + ).count(), + 1, ) def test_alias_with_duplicated_recipient(self): @@ -50,11 +49,9 @@ class AliasTestCase(ModoTestCase): "address": "badalias@test.com", "recipients": "user@test.com", "recipients_1": "user@test.com", - "enabled": True + "enabled": True, } - self.ajax_post( - reverse("admin:alias_add"), values - ) + self.ajax_post(reverse("admin:alias_add"), values) alias = Alias.objects.get(address="badalias@test.com") self.assertEqual(alias.recipients_count, 1) @@ -62,55 +59,59 @@ class AliasTestCase(ModoTestCase): """Try to create an upper case alias.""" user = User.objects.get(username="user@test.com") values = { - "username": "user@test.com", "role": user.role, - "is_active": user.is_active, "email": "user@test.com", - "aliases": "Toto@test.com", "language": "en" + "username": "user@test.com", + "role": user.role, + "is_active": user.is_active, + "email": "user@test.com", + "aliases": "Toto@test.com", + "language": "en", } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) + self.ajax_post(reverse("admin:account_change", args=[user.id]), values) mb = user.mailbox self.assertEqual( - AliasRecipient.objects - .filter(alias__internal=False, r_mailbox=mb).count(), 1) + AliasRecipient.objects.filter(alias__internal=False, r_mailbox=mb).count(), + 1, + ) self.assertEqual( AliasRecipient.objects.get( - alias__internal=False, r_mailbox=mb).alias.address, - "toto@test.com" + alias__internal=False, r_mailbox=mb + ).alias.address, + "toto@test.com", ) values = { - "address": "Titi@test.com", "recipients": "user@test.com", - "enabled": True + "address": "Titi@test.com", + "recipients": "user@test.com", + "enabled": True, } self.ajax_post(reverse("admin:alias_add"), values) - self.assertTrue( - Alias.objects.filter(address="titi@test.com").exists()) + self.assertTrue(Alias.objects.filter(address="titi@test.com").exists()) def test_append_alias_with_tag(self): """Try to create a alias with tag in recipient address""" user = User.objects.get(username="user@test.com") values = { - "username": "user@test.com", "role": user.role, - "is_active": user.is_active, "email": "user@test.com", - "language": "en" + "username": "user@test.com", + "role": user.role, + "is_active": user.is_active, + "email": "user@test.com", + "language": "en", } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) + self.ajax_post(reverse("admin:account_change", args=[user.id]), values) values = { - "address": "foobar@test.com", "recipients": "user+spam@test.com", - "enabled": True + "address": "foobar@test.com", + "recipients": "user+spam@test.com", + "enabled": True, } self.ajax_post(reverse("admin:alias_add"), values) alias = Alias.objects.get(address="foobar@test.com") self.assertTrue( alias.aliasrecipient_set.filter( - address="user+spam@test.com", r_mailbox__isnull=False, - r_alias__isnull=True).exists() + address="user+spam@test.com", + r_mailbox__isnull=False, + r_alias__isnull=True, + ).exists() ) def test_utf8_alias(self): @@ -120,11 +121,9 @@ class AliasTestCase(ModoTestCase): "recipients": "user@test.com", "recipients_1": "admin@test.com", "recipients_2": "ext@titi.com", - "enabled": True + "enabled": True, } - self.ajax_post( - reverse("admin:alias_add"), values - ) + self.ajax_post(reverse("admin:alias_add"), values) def test_dlist(self): values = { @@ -132,89 +131,68 @@ class AliasTestCase(ModoTestCase): "recipients": "user@test.com", "recipients_1": "admin@test.com", "recipients_2": "ext@titi.com", - "enabled": True + "enabled": True, } - self.ajax_post( - reverse("admin:alias_add"), values - ) + self.ajax_post(reverse("admin:alias_add"), values) user = User.objects.get(username="user@test.com") mb = user.mailbox self.assertEqual( - AliasRecipient.objects.filter( - alias__internal=False, r_mailbox=mb).count(), 2) + AliasRecipient.objects.filter(alias__internal=False, r_mailbox=mb).count(), + 2, + ) admin = User.objects.get(username="admin@test.com") mb = admin.mailbox self.assertEqual( - AliasRecipient.objects.filter( - alias__internal=False, r_mailbox=mb).count(), 1) + AliasRecipient.objects.filter(alias__internal=False, r_mailbox=mb).count(), + 1, + ) dlist = Alias.objects.get(address="all@test.com") self.assertEqual(dlist.recipients_count, 3) del values["recipients_1"] - self.ajax_post( - reverse("admin:alias_change", args=[dlist.id]), - values - ) + self.ajax_post(reverse("admin:alias_change", args=[dlist.id]), values) self.assertEqual(dlist.recipients_count, 2) self.ajax_delete( - "{}?selection={}".format( - reverse("admin:alias_delete"), dlist.id) + "{}?selection={}".format(reverse("admin:alias_delete"), dlist.id) ) - self.assertRaises( - Alias.DoesNotExist, Alias.objects.get, address="all@test.com") + self.assertRaises(Alias.DoesNotExist, Alias.objects.get, address="all@test.com") def test_forward(self): values = {"address": "forward2@test.com", "recipients": "rcpt@dest.com"} - self.ajax_post( - reverse("admin:alias_add"), values - ) + self.ajax_post(reverse("admin:alias_add"), values) fwd = Alias.objects.get(address="forward2@test.com") self.assertEqual(fwd.recipients_count, 1) values["recipients"] = "rcpt2@dest.com" - self.ajax_post( - reverse("admin:alias_change", - args=[fwd.id]), - values - ) + self.ajax_post(reverse("admin:alias_change", args=[fwd.id]), values) self.assertEqual(fwd.recipients_count, 1) - self.ajax_delete( - reverse("admin:alias_delete") + "?selection=%d" - % fwd.id, {} - ) + self.ajax_delete(reverse("admin:alias_delete") + "?selection=%d" % fwd.id, {}) self.assertRaises( - Alias.DoesNotExist, Alias.objects.get, address="forward2@test.com") + Alias.DoesNotExist, Alias.objects.get, address="forward2@test.com" + ) def test_forward_and_local_copies(self): values = {"address": "user@test.com", "recipients": "rcpt@dest.com"} - self.ajax_post( - reverse("admin:alias_add"), values - ) + self.ajax_post(reverse("admin:alias_add"), values) fwd = Alias.objects.get(address="user@test.com", internal=False) self.assertEqual(fwd.recipients_count, 1) values["recipients"] = "rcpt@dest.com" values["recipients_1"] = "user@test.com" - self.ajax_post( - reverse("admin:alias_change", args=[fwd.id]), - values - ) + self.ajax_post(reverse("admin:alias_change", args=[fwd.id]), values) fwd = Alias.objects.get(pk=fwd.pk) self.assertEqual(fwd.aliasrecipient_set.count(), 2) - self.assertEqual( - fwd.aliasrecipient_set.filter(r_alias__isnull=True).count(), 2) + self.assertEqual(fwd.aliasrecipient_set.filter(r_alias__isnull=True).count(), 2) def test_wildcard_alias(self): """Test creation of a wildcard alias.""" values = { "address": "@test.com", "recipients": "user@test.com", - "enabled": True + "enabled": True, } - self.ajax_post( - reverse("admin:alias_add"), values - ) + self.ajax_post(reverse("admin:alias_add"), values) def test_random_alias(self): """Test creation of a random alias.""" @@ -223,7 +201,7 @@ class AliasTestCase(ModoTestCase): "random_address": True, "domain": Domain.objects.get(name="test.com").pk, "recipients": "user@test.com", - "enabled": True + "enabled": True, } self.ajax_post(reverse("admin:alias_add"), values) self.assertEqual(Alias.objects.count(), alias_count + 1) @@ -234,31 +212,34 @@ class AliasTestCase(ModoTestCase): def test_distribution_list_deletion_on_user_update_bug(self): """This test demonstrates an issue with distribution list being - deleted when one of the users which belong to that list is - changed. + deleted when one of the users which belong to that list is + changed. """ values = { "address": "list@test.com", "recipients": "user@test.com", "recipients_1": "admin@test.com", - "enabled": True + "enabled": True, } - self.ajax_post( - reverse("admin:alias_add"), values - ) + self.ajax_post(reverse("admin:alias_add"), values) user = User.objects.get(username="user@test.com") values = { - "username": user.username, "first_name": "Tester", - "last_name": "Toto", "password1": "Toto1234", - "password2": "Toto1234", "role": "SimpleUsers", "quota_act": True, - "is_active": True, "email": user.email, "language": "en" + "username": user.username, + "first_name": "Tester", + "last_name": "Toto", + "password1": "Toto1234", + "password2": "Toto1234", + "role": "SimpleUsers", + "quota_act": True, + "is_active": True, + "email": user.email, + "language": "en", } self.ajax_post(reverse("admin:account_change", args=[user.id]), values) - self.assertEqual( - Alias.objects.filter(address="list@test.com").count(), 1) + self.assertEqual(Alias.objects.filter(address="list@test.com").count(), 1) def test_alias_detail_view(self): """Test alias detail view.""" @@ -276,15 +257,18 @@ class AliasTestCase(ModoTestCase): user = User.objects.get(username="user@test.com") values = { - "username": "user@test.com", "role": user.role, - "is_active": user.is_active, "email": "user@test.com", - "aliases": "toto@test.com", "aliases_1": "titi@test2.com", - "language": "en" + "username": "user@test.com", + "role": user.role, + "is_active": user.is_active, + "email": "user@test.com", + "aliases": "toto@test.com", + "aliases_1": "titi@test2.com", + "language": "en", } response = self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values, 400 + reverse("admin:account_change", args=[user.id]), values, 400 ) self.assertEqual( response["form_errors"]["aliases_1"][0], - "You don't have access to this domain") + "You don't have access to this domain", + ) diff --git a/modoboa/admin/tests/test_domain.py b/modoboa/admin/tests/test_domain.py index 32da141c4..f0b7fcd40 100644 --- a/modoboa/admin/tests/test_domain.py +++ b/modoboa/admin/tests/test_domain.py @@ -20,7 +20,7 @@ from modoboa.core.tests.test_views import SETTINGS_SAMPLE from modoboa.lib.tests import ModoTestCase from modoboa.maillog import factories as ml_factories -from ..import constants +from .. import constants from . import utils from .. import factories from ..models import Alarm, Alias, Domain @@ -38,8 +38,12 @@ class DomainTestCase(ModoTestCase): def test_create(self): """Test the creation of a domain.""" values = { - "name": "pouet.com", "quota": 1000, "default_mailbox_quota": 100, - "create_dom_admin": False, "type": "domain", "stepid": "step3" + "name": "pouet.com", + "quota": 1000, + "default_mailbox_quota": 100, + "create_dom_admin": False, + "type": "domain", + "stepid": "step3", } self.ajax_post(reverse("admin:domain_add"), values) dom = Domain.objects.get(name="pouet.com") @@ -52,12 +56,16 @@ class DomainTestCase(ModoTestCase): def test_create_utf8(self): """Test the creation of a domain with non-ASCII characters.""" values = { - "name": "pouét.com", "quota": 1000, "default_mailbox_quota": 100, - "create_dom_admin": False, "type": "domain", "stepid": "step3" + "name": "pouét.com", + "quota": 1000, + "default_mailbox_quota": 100, + "create_dom_admin": False, + "type": "domain", + "stepid": "step3", } self.ajax_post(reverse("admin:domain_add"), values) - dom = Domain.objects.get(name=u"pouét.com") - self.assertEqual(dom.name, u"pouét.com") + dom = Domain.objects.get(name="pouét.com") + self.assertEqual(dom.name, "pouét.com") self.assertEqual(dom.quota, 1000) self.assertEqual(dom.default_mailbox_quota, 100) self.assertEqual(dom.enabled, False) @@ -66,83 +74,101 @@ class DomainTestCase(ModoTestCase): def test_create_with_template(self): """Test the creation of a domain with a template.""" values = { - "name": "pouet.com", "quota": 1000, "default_mailbox_quota": 100, - "create_dom_admin": True, "dom_admin_username": "toto", - "with_mailbox": True, "create_aliases": True, "type": "domain", - "stepid": "step3" + "name": "pouet.com", + "quota": 1000, + "default_mailbox_quota": 100, + "create_dom_admin": True, + "dom_admin_username": "toto", + "with_mailbox": True, + "create_aliases": True, + "type": "domain", + "stepid": "step3", } self.ajax_post(reverse("admin:domain_add"), values) dom = Domain.objects.get(name="pouet.com") da = User.objects.get(username="toto@pouet.com") self.assertIn(da, dom.admins) al = Alias.objects.get(address="postmaster@pouet.com") - self.assertTrue( - al.aliasrecipient_set.filter(r_mailbox=da.mailbox) - .exists() - ) + self.assertTrue(al.aliasrecipient_set.filter(r_mailbox=da.mailbox).exists()) self.assertTrue(da.can_access(al)) values = { - "name": "pouet2.com", "quota": 1000, "default_mailbox_quota": 100, - "create_dom_admin": True, "dom_admin_username": "postmaster", - "create_aliases": True, "type": "domain", "stepid": "step3", - "with_mailbox": True + "name": "pouet2.com", + "quota": 1000, + "default_mailbox_quota": 100, + "create_dom_admin": True, + "dom_admin_username": "postmaster", + "create_aliases": True, + "type": "domain", + "stepid": "step3", + "with_mailbox": True, } self.ajax_post(reverse("admin:domain_add"), values) - self.assertTrue( - User.objects.filter(username="postmaster@pouet2.com").exists()) + self.assertTrue(User.objects.filter(username="postmaster@pouet2.com").exists()) self.assertFalse( Alias.objects.filter( - address="postmaster@pouet2.com", internal=False).exists()) + address="postmaster@pouet2.com", internal=False + ).exists() + ) def test_create_with_template_and_random_password(self): """Test domain creation with administrator and random password.""" values = { - "name": "pouet.com", "quota": 1000, "default_mailbox_quota": 100, - "create_dom_admin": True, "dom_admin_username": "toto", - "with_mailbox": True, "create_aliases": True, "type": "domain", - "stepid": "step3", "random_password": True + "name": "pouet.com", + "quota": 1000, + "default_mailbox_quota": 100, + "create_dom_admin": True, + "dom_admin_username": "toto", + "with_mailbox": True, + "create_aliases": True, + "type": "domain", + "stepid": "step3", + "random_password": True, } self.ajax_post(reverse("admin:domain_add"), values) self.assertFalse( - self.client.login(username="admin@pouet.com", password="password")) + self.client.login(username="admin@pouet.com", password="password") + ) def test_create_with_template_and_no_mailbox(self): """Test domain creation with administrator but no mailbox.""" values = { - "name": "pouet.com", "quota": 1000, "default_mailbox_quota": 100, - "create_dom_admin": True, "dom_admin_username": "toto", - "with_mailbox": False, "create_aliases": True, "type": "domain", - "stepid": "step3" + "name": "pouet.com", + "quota": 1000, + "default_mailbox_quota": 100, + "create_dom_admin": True, + "dom_admin_username": "toto", + "with_mailbox": False, + "create_aliases": True, + "type": "domain", + "stepid": "step3", } self.ajax_post(reverse("admin:domain_add"), values) dom = Domain.objects.get(name="pouet.com") da = User.objects.get(username="toto@pouet.com") self.assertIn(da, dom.admins) self.assertFalse(hasattr(da, "mailbox")) - self.assertFalse( - Alias.objects.filter(address="postmaster@pouet.com").exists()) + self.assertFalse(Alias.objects.filter(address="postmaster@pouet.com").exists()) def test_create_with_template_and_empty_quota(self): """Test the creation of a domain with a template and no quota""" values = { - "name": "pouet.com", "quota": 0, "default_mailbox_quota": 0, - "create_dom_admin": True, "dom_admin_username": "toto", - "create_aliases": True, "type": "domain", "stepid": "step3", - "with_mailbox": True + "name": "pouet.com", + "quota": 0, + "default_mailbox_quota": 0, + "create_dom_admin": True, + "dom_admin_username": "toto", + "create_aliases": True, + "type": "domain", + "stepid": "step3", + "with_mailbox": True, } - self.ajax_post( - reverse("admin:domain_add"), - values - ) + self.ajax_post(reverse("admin:domain_add"), values) dom = Domain.objects.get(name="pouet.com") da = User.objects.get(username="toto@pouet.com") self.assertIn(da, dom.admins) al = Alias.objects.get(address="postmaster@pouet.com") - self.assertTrue( - al.aliasrecipient_set.filter(r_mailbox=da.mailbox) - .exists() - ) + self.assertTrue(al.aliasrecipient_set.filter(r_mailbox=da.mailbox).exists()) self.assertTrue(da.can_access(al)) def test_create_with_template_and_custom_password(self): @@ -150,61 +176,62 @@ class DomainTestCase(ModoTestCase): password = "Toto1000" self.set_global_parameter("default_password", password, app="core") values = { - "name": "pouet.com", "quota": 0, "default_mailbox_quota": 0, - "create_dom_admin": True, "dom_admin_username": "toto", - "create_aliases": True, "type": "domain", "stepid": "step3", - "with_mailbox": True + "name": "pouet.com", + "quota": 0, + "default_mailbox_quota": 0, + "create_dom_admin": True, + "dom_admin_username": "toto", + "create_aliases": True, + "type": "domain", + "stepid": "step3", + "with_mailbox": True, } - self.ajax_post( - reverse("admin:domain_add"), - values - ) + self.ajax_post(reverse("admin:domain_add"), values) dom = Domain.objects.get(name="pouet.com") da = User.objects.get(username="toto@pouet.com") self.assertIn(da, dom.admins) al = Alias.objects.get(address="postmaster@pouet.com") - self.assertTrue( - al.aliasrecipient_set.filter(r_mailbox=da.mailbox) - .exists() - ) + self.assertTrue(al.aliasrecipient_set.filter(r_mailbox=da.mailbox).exists()) self.assertTrue(da.can_access(al)) - self.assertTrue( - self.client.login(username="toto@pouet.com", password=password)) + self.assertTrue(self.client.login(username="toto@pouet.com", password=password)) def test_quota_constraints(self): """Check quota constraints.""" values = { - "name": "pouet.com", "quota": 10, "default_mailbox_quota": 100, - "create_dom_admin": True, "dom_admin_username": "toto", - "create_aliases": True, "type": "domain", "stepid": "step3" + "name": "pouet.com", + "quota": 10, + "default_mailbox_quota": 100, + "create_dom_admin": True, + "dom_admin_username": "toto", + "create_aliases": True, + "type": "domain", + "stepid": "step3", } - response = self.ajax_post( - reverse("admin:domain_add"), - values, status=400 - ) + response = self.ajax_post(reverse("admin:domain_add"), values, status=400) self.assertEqual( response["form_errors"]["default_mailbox_quota"][0], - "Cannot be greater than domain quota") + "Cannot be greater than domain quota", + ) dom = Domain.objects.get(name="test.com") values["name"] = dom.name values["quota"] = 10 values["default_mailbox_quota"] = 100 response = self.ajax_post( - reverse("admin:domain_change", args=[dom.pk]), - values, status=400 + reverse("admin:domain_change", args=[dom.pk]), values, status=400 ) self.assertEqual( response["form_errors"]["default_mailbox_quota"][0], - "Cannot be greater than domain quota") + "Cannot be greater than domain quota", + ) def test_create_using_default_quota(self): """Check that default value is used for creation.""" self.set_global_parameter("default_domain_quota", 500) self.set_global_parameter("default_mailbox_quota", 50) response = self.client.get(reverse("admin:domain_add")) - self.assertContains(response, "value=\"500\"") - self.assertContains(response, "value=\"50\"") + self.assertContains(response, 'value="500"') + self.assertContains(response, 'value="50"') @mock.patch.object(dns.resolver.Resolver, "resolve") @mock.patch("socket.getaddrinfo") @@ -214,7 +241,8 @@ class DomainTestCase(ModoTestCase): mock_getaddrinfo.side_effect = utils.mock_ip_query_result reseller = core_factories.UserFactory( - username="reseller", groups=("Resellers", )) + username="reseller", groups=("Resellers",) + ) self.client.force_login(reseller) self.set_global_parameter("enable_admin_limits", False, app="limits") @@ -222,15 +250,18 @@ class DomainTestCase(ModoTestCase): self.set_global_parameter("domains_must_have_authorized_mx", True) values = { - "name": "no-mx.example.com", "quota": 0, + "name": "no-mx.example.com", + "quota": 0, "default_mailbox_quota": 0, - "create_dom_admin": True, "dom_admin_username": "toto", - "create_aliases": True, "type": "domain", "stepid": "step3", - "with_mailbox": True + "create_dom_admin": True, + "dom_admin_username": "toto", + "create_aliases": True, + "type": "domain", + "stepid": "step3", + "with_mailbox": True, } self.ajax_post(reverse("admin:domain_add"), values, 400) - self.assertFalse( - Domain.objects.filter(name=values["name"]).exists()) + self.assertFalse(Domain.objects.filter(name=values["name"]).exists()) values["name"] = "pouet.com" mock_getaddrinfo.side_effect = utils.mock_ip_query_result @@ -242,21 +273,20 @@ class DomainTestCase(ModoTestCase): Rename 'test.com' domain to 'pouet.com' """ values = { - "name": "pouet.com", "quota": 1000, "default_mailbox_quota": 100, - "type": "domain", "enabled": True + "name": "pouet.com", + "quota": 1000, + "default_mailbox_quota": 100, + "type": "domain", + "enabled": True, } dom = Domain.objects.get(name="test.com") - self.ajax_post( - reverse("admin:domain_change", args=[dom.id]), - values - ) + self.ajax_post(reverse("admin:domain_change", args=[dom.id]), values) dom = Domain.objects.get(name="pouet.com") self.assertEqual(dom.name, "pouet.com") self.assertTrue(dom.enabled) # Check if aliases were renamed too - self.assertTrue( - dom.alias_set.filter(address="postmaster@pouet.com").exists()) + self.assertTrue(dom.alias_set.filter(address="postmaster@pouet.com").exists()) def test_delete(self): """Test the removal of a domain.""" @@ -264,15 +294,13 @@ class DomainTestCase(ModoTestCase): self.ajax_post(reverse("admin:domain_delete", args=[dom.id])) with self.assertRaises(Domain.DoesNotExist): Domain.objects.get(pk=1) - self.assertTrue( - User.objects.filter(username="user@test.com").exists()) + self.assertTrue(User.objects.filter(username="user@test.com").exists()) self.set_global_parameter("auto_account_removal", True) dom = Domain.objects.get(name="test2.com") self.ajax_post(reverse("admin:domain_delete", args=[dom.id])) with self.assertRaises(Domain.DoesNotExist): Domain.objects.get(pk=1) - self.assertFalse( - User.objects.filter(username="admin@test2.com").exists()) + self.assertFalse(User.objects.filter(username="admin@test2.com").exists()) def test_domain_counters(self): """Check counters at domain level.""" @@ -347,8 +375,10 @@ class DomainTestCase(ModoTestCase): domain = Domain.objects.get(name="test.com") ml_factories.MaillogFactory(from_domain=domain) ml_factories.MaillogFactory( - to_domain=domain, status="received", - date=timezone.now() + relativedelta(days=1)) + to_domain=domain, + status="received", + date=timezone.now() + relativedelta(days=1), + ) ml_factories.MaillogFactory(date=timezone.now() + relativedelta(days=2)) ml_factories.MaillogFactory(date=timezone.now() + relativedelta(days=3)) url = reverse("admin:domain_logs_list") @@ -375,8 +405,7 @@ class DomainTestCase(ModoTestCase): self.assertEqual(response.status_code, 200) self.assertIn("Global statistics", response.content.decode("utf-8")) - self.client.force_login( - User.objects.get(username="admin@test.com")) + self.client.force_login(User.objects.get(username="admin@test.com")) response = self.client.get(url) self.assertEqual(response.status_code, 200) content = response.content.decode("utf-8") @@ -386,7 +415,8 @@ class DomainTestCase(ModoTestCase): def test_page_loader(self): """Test page loader view.""" factories.AlarmFactory( - domain__name="test.com", mailbox=None, title="Test alarm") + domain__name="test.com", mailbox=None, title="Test alarm" + ) url = reverse("admin:domain_page") response = self.ajax_get(url) self.assertIn("handle_mailboxes", response) @@ -418,10 +448,13 @@ class DKIMTestCase(ModoTestCase): settings["admin-dkim_keys_storage_dir"] = "/wrong" response = self.client.post(url, settings, format="json") self.assertEqual(response.status_code, 400) - compare(response.json(), { - "form_errors": {"dkim_keys_storage_dir": ["Directory not found."]}, - "prefix": "admin" - }) + compare( + response.json(), + { + "form_errors": {"dkim_keys_storage_dir": ["Directory not found."]}, + "prefix": "admin", + }, + ) settings["admin-dkim_keys_storage_dir"] = self.workdir response = self.client.post(url, settings, format="json") self.assertEqual(response.status_code, 200) @@ -429,20 +462,26 @@ class DKIMTestCase(ModoTestCase): def test_dkim_key_creation(self): """Test DKIM key creation.""" values = { - "name": "pouet.com", "quota": 1000, "default_mailbox_quota": 100, - "create_dom_admin": False, "type": "domain", "stepid": "step3", - "enable_dkim": True, "dkim_key_selector": "" + "name": "pouet.com", + "quota": 1000, + "default_mailbox_quota": 100, + "create_dom_admin": False, + "type": "domain", + "stepid": "step3", + "enable_dkim": True, + "dkim_key_selector": "", } response = self.ajax_post(reverse("admin:domain_add"), values, 400) self.assertEqual( response["form_errors"]["enable_dkim"][0], - "DKIM keys storage directory not configured") + "DKIM keys storage directory not configured", + ) self.set_global_parameter("dkim_keys_storage_dir", self.workdir) response = self.ajax_post(reverse("admin:domain_add"), values, 400) self.assertEqual( - response["form_errors"]["dkim_key_selector"][0], - "This field is required.") + response["form_errors"]["dkim_key_selector"][0], "This field is required." + ) values["dkim_key_selector"] = "default" self.ajax_post(reverse("admin:domain_add"), values) @@ -453,30 +492,33 @@ class DKIMTestCase(ModoTestCase): domain = Domain.objects.get(name=values["name"]) # Closing the alarm and retry generating dkim keys - Alarm.objects.get(domain=domain, - internal_name=constants.DKIM_WRITE_ERROR - ).close() + Alarm.objects.get( + domain=domain, internal_name=constants.DKIM_WRITE_ERROR + ).close() call_command("modo", "manage_dkim_keys") self.assertEqual( - Alarm.objects.get(domain=domain, - internal_name=constants.DKIM_WRITE_ERROR).status, - constants.ALARM_OPENED) + Alarm.objects.get( + domain=domain, internal_name=constants.DKIM_WRITE_ERROR + ).status, + constants.ALARM_OPENED, + ) # Generate normally self.set_global_parameter("dkim_keys_storage_dir", self.workdir) call_command("modo", "manage_dkim_keys") self.assertEqual( - Alarm.objects.get(domain=domain, - internal_name=constants.DKIM_WRITE_ERROR).status, - constants.ALARM_CLOSED) + Alarm.objects.get( + domain=domain, internal_name=constants.DKIM_WRITE_ERROR + ).status, + constants.ALARM_CLOSED, + ) key_path = os.path.join(self.workdir, "{}.pem".format(values["name"])) self.assertTrue(os.path.exists(key_path)) domain.refresh_from_db() url = reverse("admin:domain_detail", args=[domain.pk]) response = self.client.get(url) - self.assertContains( - response, escape(domain.bind_format_dkim_public_key)) + self.assertContains(response, escape(domain.bind_format_dkim_public_key)) # Try generating DKIM key for a targetted domain domain.dkim_private_key_path = "" @@ -488,14 +530,13 @@ class DKIMTestCase(ModoTestCase): # Try disabling DKIM and checking that dkim_private_key_path is emptied values = { - "name": "pouet.com", "enable_dkim": False, - "quota": 1000, "default_mailbox_quota": 100, - "type": "domain" + "name": "pouet.com", + "enable_dkim": False, + "quota": 1000, + "default_mailbox_quota": 100, + "type": "domain", } - self.ajax_post( - reverse("admin:domain_change", args=[domain.id]), - values - ) + self.ajax_post(reverse("admin:domain_change", args=[domain.id]), values) domain.refresh_from_db() self.assertEqual(domain.dkim_private_key_path, "") @@ -503,9 +544,14 @@ class DKIMTestCase(ModoTestCase): """ """ self.set_global_parameter("dkim_keys_storage_dir", self.workdir) values = { - "name": "pouet.com", "quota": 1000, "default_mailbox_quota": 100, - "create_dom_admin": False, "type": "domain", "stepid": "step3", - "enable_dkim": True, "dkim_key_selector": "default" + "name": "pouet.com", + "quota": 1000, + "default_mailbox_quota": 100, + "create_dom_admin": False, + "type": "domain", + "stepid": "step3", + "enable_dkim": True, + "dkim_key_selector": "default", } self.ajax_post(reverse("admin:domain_add"), values) call_command("modo", "manage_dkim_keys") diff --git a/modoboa/admin/tests/test_domain_alias.py b/modoboa/admin/tests/test_domain_alias.py index 7559c7d8a..6cf4f07cf 100644 --- a/modoboa/admin/tests/test_domain_alias.py +++ b/modoboa/admin/tests/test_domain_alias.py @@ -21,37 +21,30 @@ class DomainAliasTestCase(ModoTestCase): domal.target = dom domal.save() self.assertEqual(dom.domainalias_count, 1) - self.assertTrue( - Alias.objects.filter( - address="@{}".format(domal.name)).exists()) + self.assertTrue(Alias.objects.filter(address="@{}".format(domal.name)).exists()) domal.name = "domalias.org" domal.save() domal.delete() self.assertFalse( - Alias.objects.filter( - address="@{}".format(domal.name)).exists()) + Alias.objects.filter(address="@{}".format(domal.name)).exists() + ) def test_form(self): dom = Domain.objects.get(name="test.com") values = { - "name": dom.name, "quota": dom.quota, + "name": dom.name, + "quota": dom.quota, "default_mailbox_quota": dom.default_mailbox_quota, - "enabled": dom.enabled, "aliases": "domalias.net", - "aliases_1": "domaliasé.com", "type": "domain" + "enabled": dom.enabled, + "aliases": "domalias.net", + "aliases_1": "domaliasé.com", + "type": "domain", } - self.ajax_post( - reverse("admin:domain_change", - args=[dom.id]), - values - ) + self.ajax_post(reverse("admin:domain_change", args=[dom.id]), values) self.assertEqual(dom.domainalias_set.count(), 2) del values["aliases_1"] - self.ajax_post( - reverse("admin:domain_change", - args=[dom.id]), - values - ) + self.ajax_post(reverse("admin:domain_change", args=[dom.id]), values) self.assertEqual(dom.domainalias_set.count(), 1) diff --git a/modoboa/admin/tests/test_export.py b/modoboa/admin/tests/test_export.py index 9317dd7ad..0de2728e0 100644 --- a/modoboa/admin/tests/test_export.py +++ b/modoboa/admin/tests/test_export.py @@ -22,20 +22,19 @@ class ExportTestCase(ModoTestCase): def __export_domains(self, domfilter=""): self.client.get( - "{}?domfilter={}".format( - reverse("admin:_domain_list"), domfilter)) + "{}?domfilter={}".format(reverse("admin:_domain_list"), domfilter) + ) return self.client.post( reverse("admin:domain_export"), {"filename": "test.csv"} ) def __export_identities(self, idtfilter="", grpfilter=""): self.client.get( - reverse("admin:_identity_list") + - "?grpfilter=%s&idtfilter=%s" % (grpfilter, idtfilter) + reverse("admin:_identity_list") + + "?grpfilter=%s&idtfilter=%s" % (grpfilter, idtfilter) ) return self.client.post( - reverse("admin:identity_export"), - {"filename": "test.csv"} + reverse("admin:identity_export"), {"filename": "test.csv"} ) def test_export_domains(self): @@ -49,8 +48,7 @@ class ExportTestCase(ModoTestCase): "domain;test2.com;0;0;True", ] self.assertCountEqual( - expected_response, - force_str(response.content.strip()).split("\r\n") + expected_response, force_str(response.content.strip()).split("\r\n") ) # Test management command too. @@ -59,7 +57,8 @@ class ExportTestCase(ModoTestCase): response = sys.stdout.getvalue() sys.stdout = stdout_backup self.assertCountEqual( - expected_response, force_str(response.strip()).split("\r\n")) + expected_response, force_str(response.strip()).split("\r\n") + ) def test_export_identities(self): response = self.__export_identities() @@ -69,16 +68,15 @@ class ExportTestCase(ModoTestCase): admin_row = received_content[0].split(";") admin_row[2] = "" received_content[0] = ";".join(admin_row) - self.assertCountEqual( - expected_response.strip().split("\r\n"), - received_content - ) + self.assertCountEqual(expected_response.strip().split("\r\n"), received_content) def test_export_simpleusers(self): factories.MailboxFactory( user__username="toto@test.com", - user__first_name="Léon", user__groups=("SimpleUsers", ), - address="toto", domain__name="test.com", + user__first_name="Léon", + user__groups=("SimpleUsers",), + address="toto", + domain__name="test.com", ) response = self.__export_identities( idtfilter="account", grpfilter="SimpleUsers" @@ -86,7 +84,7 @@ class ExportTestCase(ModoTestCase): expected_response = "account;user@test.com;{PLAIN}toto;;;True;SimpleUsers;user@test.com;10\r\naccount;user@test2.com;{PLAIN}toto;;;True;SimpleUsers;user@test2.com;10\r\naccount;toto@test.com;{PLAIN}toto;Léon;;True;SimpleUsers;toto@test.com;10" # NOQA:E501 self.assertCountEqual( expected_response.split("\r\n"), - force_str(response.content.strip()).split("\r\n") + force_str(response.content.strip()).split("\r\n"), ) def test_export_superadmins(self): @@ -100,9 +98,7 @@ class ExportTestCase(ModoTestCase): elements = response.content.decode().strip().split(";") self.assertEqual(len(elements), 9) elements[2] = "" - self.assertEqual( - ";".join(elements), "account;admin;;;;True;SuperAdmins;;" - ) + self.assertEqual(";".join(elements), "account;admin;;;;True;SuperAdmins;;") def test_export_domainadmins(self): response = self.__export_identities( @@ -111,12 +107,12 @@ class ExportTestCase(ModoTestCase): expected_response = "account;admin@test.com;{PLAIN}toto;;;True;DomainAdmins;admin@test.com;10;test.com\r\naccount;admin@test2.com;{PLAIN}toto;;;True;DomainAdmins;admin@test2.com;10;test2.com" # NOQA:E501 self.assertCountEqual( expected_response.split("\r\n"), - force_str(response.content.strip()).split("\r\n") + force_str(response.content.strip()).split("\r\n"), ) def test_export_aliases(self): response = self.__export_identities(idtfilter="alias") self.assertEqual( response.content.decode().strip(), - "alias;alias@test.com;True;user@test.com\r\nalias;forward@test.com;True;user@external.com\r\nalias;postmaster@test.com;True;test@truc.fr;toto@titi.com" # NOQA:E501 + "alias;alias@test.com;True;user@test.com\r\nalias;forward@test.com;True;user@external.com\r\nalias;postmaster@test.com;True;test@truc.fr;toto@titi.com", # NOQA:E501 ) diff --git a/modoboa/admin/tests/test_import_.py b/modoboa/admin/tests/test_import_.py index e9e89038f..5a72ba6d5 100644 --- a/modoboa/admin/tests/test_import_.py +++ b/modoboa/admin/tests/test_import_.py @@ -22,8 +22,7 @@ class ImportTestCase(ModoTestCase): def setUpTestData(cls): # NOQA:N802 """Create test data.""" super(ImportTestCase, cls).setUpTestData() - cls.localconfig.parameters.set_value( - "enable_admin_limits", False, app="limits") + cls.localconfig.parameters.set_value("enable_admin_limits", False, app="limits") cls.localconfig.save() factories.populate_database() @@ -32,15 +31,14 @@ class ImportTestCase(ModoTestCase): self.assertEqual(response.status_code, 200) self.assertIn("Provide a CSV", response.content.decode()) - f = ContentFile(b"""domain; domain1.com; 1000; 100; True + f = ContentFile( + b"""domain; domain1.com; 1000; 100; True domain; domain2.com; 1000; 200; False domainalias; domalias1.com; domain1.com; True -""", name="domains.csv") - self.client.post( - reverse("admin:domain_import"), { - "sourcefile": f - } +""", + name="domains.csv", ) + self.client.post(reverse("admin:domain_import"), {"sourcefile": f}) admin = User.objects.get(username="admin") dom = Domain.objects.get(name="domain1.com") self.assertEqual(dom.quota, 1000) @@ -59,25 +57,21 @@ domainalias; domalias1.com; domain1.com; True def test_domain_import_bad_syntax(self): """Check errors handling.""" url = reverse("admin:domain_import") - f = ContentFile("domain; domain1.com; 100; True", - name="domains.csv") + f = ContentFile("domain; domain1.com; 100; True", name="domains.csv") response = self.client.post(url, {"sourcefile": f}) self.assertContains(response, "Invalid line") - f = ContentFile("domain; domain1.com; XX; 100; True", - name="domains.csv") + f = ContentFile("domain; domain1.com; XX; 100; True", name="domains.csv") response = self.client.post(url, {"sourcefile": f}) self.assertContains(response, "domain1.com: invalid quota value") - f = ContentFile("domain; domain1.com; 100; XX; True", - name="domains.csv") + f = ContentFile("domain; domain1.com; 100; XX; True", name="domains.csv") response = self.client.post(url, {"sourcefile": f}) - self.assertContains( - response, "domain1.com: invalid default mailbox quota") - f = ContentFile("domain; domain1.com; 10; 100; True", - name="domains.csv") + self.assertContains(response, "domain1.com: invalid default mailbox quota") + f = ContentFile("domain; domain1.com; 10; 100; True", name="domains.csv") response = self.client.post(url, {"sourcefile": f}) self.assertContains( response, - "domain1.com: default mailbox quota cannot be greater than domain quota") + "domain1.com: default mailbox quota cannot be greater than domain quota", + ) @mock.patch.object(dns.resolver.Resolver, "resolve") @mock.patch("socket.getaddrinfo") @@ -86,60 +80,49 @@ domainalias; domalias1.com; domain1.com; True mock_query.side_effect = utils.mock_dns_query_result mock_getaddrinfo.side_effect = utils.mock_ip_query_result reseller = core_factories.UserFactory( - username="reseller", groups=("Resellers", )) + username="reseller", groups=("Resellers",) + ) self.client.force_login(reseller) self.set_global_parameter("valid_mxs", "192.0.2.1 2001:db8::1") self.set_global_parameter("domains_must_have_authorized_mx", True) - f = ContentFile( - b"domain; test3.com; 100; 1; True", name="domains.csv") - resp = self.client.post( - reverse("admin:domain_import"), { - "sourcefile": f - } - ) - self.assertContains( - resp, "test3.com: no authorized MX record found for domain") + f = ContentFile(b"domain; test3.com; 100; 1; True", name="domains.csv") + resp = self.client.post(reverse("admin:domain_import"), {"sourcefile": f}) + self.assertContains(resp, "test3.com: no authorized MX record found for domain") mock_getaddrinfo.side_effect = utils.mock_ip_query_result - f = ContentFile( - b"domain; domain1.com; 100; 1; True", name="domains.csv") - resp = self.client.post( - reverse("admin:domain_import"), { - "sourcefile": f - } - ) - self.assertTrue( - Domain.objects.filter(name="domain1.com").exists()) + f = ContentFile(b"domain; domain1.com; 100; 1; True", name="domains.csv") + resp = self.client.post(reverse("admin:domain_import"), {"sourcefile": f}) + self.assertTrue(Domain.objects.filter(name="domain1.com").exists()) def test_import_domains_with_conflict(self): - f = ContentFile(b"""domain;test.alias;100;10;True + f = ContentFile( + b"""domain;test.alias;100;10;True domainalias;test.alias;test.com;True -""", name="domains.csv") - resp = self.client.post( - reverse("admin:domain_import"), { - "sourcefile": f - } +""", + name="domains.csv", ) - self.assertIn( - "Object already exists: domainalias", resp.content.decode()) + resp = self.client.post(reverse("admin:domain_import"), {"sourcefile": f}) + self.assertIn("Object already exists: domainalias", resp.content.decode()) def test_identities_import(self): response = self.client.get(reverse("admin:identity_import")) self.assertEqual(response.status_code, 200) self.assertIn("Provide a CSV", response.content.decode()) - f = ContentFile(""" + f = ContentFile( + """ account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 0 account; Truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 5; test.com alias; alias1@test.com; True; user1@test.com forward; alias2@test.com; True; user1+ext@test.com forward; fwd1@test.com; True; user@extdomain.com dlist; dlist@test.com; True; user1@test.com; user@extdomain.com -""", name="identities.csv") # NOQA:E501 +""", + name="identities.csv", + ) # NOQA:E501 self.client.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True} + reverse("admin:identity_import"), {"sourcefile": f, "crypt_password": True} ) admin = User.objects.get(username="admin") u1 = User.objects.get(username="user1@test.com") @@ -154,13 +137,11 @@ dlist; dlist@test.com; True; user1@test.com; user@extdomain.com self.assertEqual(mb1.quota, 0) self.assertTrue(admin.is_owner(mb1)) self.assertEqual(mb1.full_address, "user1@test.com") - self.assertTrue( - self.client.login(username="user1@test.com", password="toto") - ) + self.assertTrue(self.client.login(username="user1@test.com", password="toto")) da = User.objects.get(username="truc@test.com") damb = da.mailbox - self.assertEqual(da.first_name, u"René") + self.assertEqual(da.first_name, "René") self.assertEqual(da.role, "DomainAdmins") self.assertEqual(damb.quota, 5) self.assertFalse(damb.use_domain_quota) @@ -171,52 +152,48 @@ dlist; dlist@test.com; True; user1@test.com; user@extdomain.com self.assertTrue(da.can_access(u)) al = Alias.objects.get(address="alias1@test.com") - self.assertTrue( - al.aliasrecipient_set - .filter(r_mailbox=u1.mailbox).exists() - ) + self.assertTrue(al.aliasrecipient_set.filter(r_mailbox=u1.mailbox).exists()) self.assertTrue(admin.is_owner(al)) fwd = Alias.objects.get(address="fwd1@test.com") self.assertTrue( - fwd.aliasrecipient_set - .filter( - address="user@extdomain.com", r_mailbox__isnull=True, - r_alias__isnull=True) - .exists() + fwd.aliasrecipient_set.filter( + address="user@extdomain.com", + r_mailbox__isnull=True, + r_alias__isnull=True, + ).exists() ) self.assertTrue(admin.is_owner(fwd)) dlist = Alias.objects.get(address="dlist@test.com") + self.assertTrue(dlist.aliasrecipient_set.filter(r_mailbox=u1.mailbox).exists()) self.assertTrue( - dlist.aliasrecipient_set - .filter(r_mailbox=u1.mailbox).exists() - ) - self.assertTrue( - dlist.aliasrecipient_set.filter(address="user@extdomain.com") - .exists() + dlist.aliasrecipient_set.filter(address="user@extdomain.com").exists() ) self.assertTrue(admin.is_owner(dlist)) def test_import_for_nonlocal_domain(self): """Try to import an account for nonlocal domain.""" - f = ContentFile(b""" + f = ContentFile( + b""" account; user1@nonlocal.com; toto; User; One; True; SimpleUsers; user1@nonlocal.com; 0 -""", name="identities.csv") # NOQA:E501 +""", + name="identities.csv", + ) # NOQA:E501 self.client.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True} + reverse("admin:identity_import"), {"sourcefile": f, "crypt_password": True} ) - self.assertFalse( - User.objects.filter(username="user1@nonlocal.com").exists()) + self.assertFalse(User.objects.filter(username="user1@nonlocal.com").exists()) def test_import_invalid_quota(self): - f = ContentFile(b""" + f = ContentFile( + b""" account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; ; test.com -""", name="identities.csv") # NOQA:E501 +""", + name="identities.csv", + ) # NOQA:E501 resp = self.client.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True} + reverse("admin:identity_import"), {"sourcefile": f, "crypt_password": True} ) self.assertIn("wrong quota value", resp.content.decode()) @@ -224,62 +201,69 @@ account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; ; t """Check if a domain admin is not allowed to import a domain.""" self.client.logout() self.client.login(username="admin@test.com", password="toto") - f = ContentFile(b""" + f = ContentFile( + b""" domain; domain2.com; 1000; 200; False -""", name="identities.csv") - resp = self.client.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True} +""", + name="identities.csv", ) - self.assertIn( - "You are not allowed to import domains", - resp.content.decode()) - f = ContentFile(b""" + resp = self.client.post( + reverse("admin:identity_import"), {"sourcefile": f, "crypt_password": True} + ) + self.assertIn("You are not allowed to import domains", resp.content.decode()) + f = ContentFile( + b""" domainalias; domalias1.com; test.com; True -""", name="identities.csv") +""", + name="identities.csv", + ) resp = self.client.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True} + reverse("admin:identity_import"), {"sourcefile": f, "crypt_password": True} ) self.assertIn( - "You are not allowed to import domain aliases", - resp.content.decode()) + "You are not allowed to import domain aliases", resp.content.decode() + ) def test_import_quota_too_big(self): self.client.logout() self.client.login(username="admin@test.com", password="toto") - f = ContentFile(b""" + f = ContentFile( + b""" account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 40 -""", name="identities.csv") +""", + name="identities.csv", + ) resp = self.client.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True} + reverse("admin:identity_import"), {"sourcefile": f, "crypt_password": True} ) self.assertIn("test.com: domain quota exceeded", resp.content.decode()) def test_import_missing_quota(self): - f = ContentFile(b""" + f = ContentFile( + b""" account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com -""", name="identities.csv") +""", + name="identities.csv", + ) self.client.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True} + reverse("admin:identity_import"), {"sourcefile": f, "crypt_password": True} ) account = User.objects.get(username="user1@test.com") self.assertEqual( - account.mailbox.quota, - account.mailbox.domain.default_mailbox_quota + account.mailbox.quota, account.mailbox.domain.default_mailbox_quota ) def test_import_duplicate(self): - f = ContentFile(""" + f = ContentFile( + """ account; admin@test.com; toto; Admin; ; True; DomainAdmins; admin@test.com; 0; test.com account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 0; test.com -""", name="identities.csv") # NOQA:E501 +""", + name="identities.csv", + ) # NOQA:E501 self.client.post( reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True, - "continue_if_exists": True} + {"sourcefile": f, "crypt_password": True, "continue_if_exists": True}, ) admin = User.objects.get(username="admin") u1 = User.objects.get(username="truc@test.com") @@ -291,58 +275,63 @@ account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 0; Expected result: no """ self.client.logout() - self.assertTrue( - self.client.login(username="admin@test.com", password="toto") - ) - f = ContentFile(b""" + self.assertTrue(self.client.login(username="admin@test.com", password="toto")) + f = ContentFile( + b""" account; sa@test.com; toto; Super; Admin; True; SuperAdmins; superadmin@test.com; 50 -""", name="identities.csv") # NOQA:E501 +""", + name="identities.csv", + ) # NOQA:E501 self.client.post( reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True, - "continue_if_exists": True} + {"sourcefile": f, "crypt_password": True, "continue_if_exists": True}, ) with self.assertRaises(User.DoesNotExist): User.objects.get(username="sa@test.com") def test_import_alias_with_empty_values(self): - f = ContentFile(b""" + f = ContentFile( + b""" alias;user.alias@test.com;True;user@test.com;;;;;;;;;;;;;;;; -""", name="identities.csv") +""", + name="identities.csv", + ) self.client.post( reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True, - "continue_if_exists": True} + {"sourcefile": f, "crypt_password": True, "continue_if_exists": True}, ) alias = Alias.objects.get(address="user.alias@test.com") self.assertEqual(alias.type, "alias") def test_import_account_alias_conflict(self): """Specific test for #1144.""" - f = ContentFile(b""" + f = ContentFile( + b""" alias;user@test.com;True;admin@test.com -""", name="identities.csv") +""", + name="identities.csv", + ) self.client.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True} + reverse("admin:identity_import"), {"sourcefile": f, "crypt_password": True} ) self.assertTrue( - Alias.objects.filter( - address="user@test.com", internal=False).exists()) + Alias.objects.filter(address="user@test.com", internal=False).exists() + ) def test_domains_import_utf8(self): response = self.client.get(reverse("admin:domain_import")) self.assertEqual(response.status_code, 200) self.assertIn("Provide a CSV", response.content.decode()) - f = ContentFile("""domain; dómªin1.com; 1000; 100; True + f = ContentFile( + """domain; dómªin1.com; 1000; 100; True dómain; dómªin2.com; 1000; 100; True -""".encode("utf8"), name="dómains.csv") - self.client.post( - reverse("admin:domain_import"), { - "sourcefile": f - } +""".encode( + "utf8" + ), + name="dómains.csv", ) + self.client.post(reverse("admin:domain_import"), {"sourcefile": f}) admin = User.objects.get(username="admin") dom = Domain.objects.get(name="dómªin1.com") self.assertEqual(dom.quota, 1000) @@ -360,8 +349,7 @@ dómain; dómªin2.com; 1000; 100; True def test_import_command(self): test_file = os.path.join( - os.path.dirname(__file__), - "test_data/import_domains.csv" + os.path.dirname(__file__), "test_data/import_domains.csv" ) call_command("modo", "import", test_file) @@ -381,8 +369,7 @@ dómain; dómªin2.com; 1000; 100; True def test_import_command_non_utf8(self): test_file = os.path.join( - os.path.dirname(__file__), - "test_data/import_domains_iso8859.csv" + os.path.dirname(__file__), "test_data/import_domains_iso8859.csv" ) call_command("modo", "import", test_file) dom = Domain.objects.get(name="dómain2.com") @@ -390,8 +377,7 @@ dómain; dómªin2.com; 1000; 100; True def test_import_command_duplicates(self): test_file = os.path.join( - os.path.dirname(__file__), - "test_data/import_domains_duplicates.csv" + os.path.dirname(__file__), "test_data/import_domains_duplicates.csv" ) with self.assertRaises(CommandError) as cm: call_command("modo", "import", test_file) @@ -400,8 +386,7 @@ dómain; dómªin2.com; 1000; 100; True def test_import_command_duplicates_continue(self): test_file = os.path.join( - os.path.dirname(__file__), - "test_data/import_domains_duplicates.csv" + os.path.dirname(__file__), "test_data/import_domains_duplicates.csv" ) call_command("modo", "import", "--continue-if-exists", test_file) @@ -420,20 +405,21 @@ dómain; dómªin2.com; 1000; 100; True self.assertTrue(admin.is_owner(dom)) def test_import_aliases(self): - f = ContentFile(""" + f = ContentFile( + """ account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 0 account; Truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 5; test.com alias; alias1@test.com; True; user1@test.com -""", name="identities.csv") # NOQA:E501 +""", + name="identities.csv", + ) # NOQA:E501 resp = self.client.post( - reverse("admin:identity_import"), - {"sourcefile": f, "crypt_password": True} + reverse("admin:identity_import"), {"sourcefile": f, "crypt_password": True} ) self.assertEqual(resp.status_code, 200) test_file = os.path.join( - os.path.dirname(__file__), - "test_data/import_aliases.csv" + os.path.dirname(__file__), "test_data/import_aliases.csv" ) with self.assertRaises(CommandError) as cm: call_command("modo", "import", test_file) diff --git a/modoboa/admin/tests/test_mailbox_operations.py b/modoboa/admin/tests/test_mailbox_operations.py index fb2462987..40003cc62 100644 --- a/modoboa/admin/tests/test_mailbox_operations.py +++ b/modoboa/admin/tests/test_mailbox_operations.py @@ -13,8 +13,7 @@ from modoboa.lib.tests import ModoTestCase from .. import factories, models -@override_settings( - DOVECOT_LOOKUP_PATH=["{}/dovecot".format(os.path.dirname(__file__))]) +@override_settings(DOVECOT_LOOKUP_PATH=["{}/dovecot".format(os.path.dirname(__file__))]) class MailboxOperationTestCase(ModoTestCase): """Test management command.""" @@ -43,10 +42,9 @@ class MailboxOperationTestCase(ModoTestCase): path = "{}/test.com/admin".format(self.workdir) mail_home_mock.__get__ = mock.Mock(return_value=path) mb = models.Mailbox.objects.select_related("user").get( - address="admin", domain__name="test.com") - self.ajax_post( - reverse("admin:account_delete", args=[mb.user.pk]), {} + address="admin", domain__name="test.com" ) + self.ajax_post(reverse("admin:account_delete", args=[mb.user.pk]), {}) call_command("handle_mailbox_operations") self.assertFalse(models.MailboxOperation.objects.exists()) self.assertFalse(os.path.exists(mb.mail_home)) @@ -57,15 +55,16 @@ class MailboxOperationTestCase(ModoTestCase): path = "{}/test.com/admin".format(self.workdir) mail_home_mock.__get__ = mock.Mock(return_value=path) mb = models.Mailbox.objects.select_related("user").get( - address="admin", domain__name="test.com") - values = { - "username": "admin2@test.com", "role": "DomainAdmins", - "is_active": True, "email": "admin2@test.com", - "language": "en" - } - self.ajax_post( - reverse("admin:account_change", args=[mb.user.pk]), values + address="admin", domain__name="test.com" ) + values = { + "username": "admin2@test.com", + "role": "DomainAdmins", + "is_active": True, + "email": "admin2@test.com", + "language": "en", + } + self.ajax_post(reverse("admin:account_change", args=[mb.user.pk]), values) path = "{}/test.com/admin2".format(self.workdir) mail_home_mock.__get__ = mock.Mock(return_value=path) call_command("handle_mailbox_operations") diff --git a/modoboa/admin/tests/test_mapfiles.py b/modoboa/admin/tests/test_mapfiles.py index ecc51951d..6af7eb568 100644 --- a/modoboa/admin/tests/test_mapfiles.py +++ b/modoboa/admin/tests/test_mapfiles.py @@ -11,20 +11,22 @@ from modoboa.lib.test_utils import MapFilesTestCaseMixin class MapFilesTestCase(MapFilesTestCaseMixin, TestCase): - """Test case for admin.""" MAP_FILES = [ - "sql-domains.cf", "sql-domain-aliases.cf", "sql-aliases.cf", - "sql-maintain.cf", "sql-sender-login-map.cf" + "sql-domains.cf", + "sql-domain-aliases.cf", + "sql-aliases.cf", + "sql-maintain.cf", + "sql-sender-login-map.cf", ] def test_map_upgrade(self): """Check that map content is used.""" dburl = "postgres://user:password@localhost/testdb" call_command( - "generate_postfix_maps", - "--dburl", dburl, "--destdir", self.workdir) + "generate_postfix_maps", "--dburl", dburl, "--destdir", self.workdir + ) # Now upgrade files. Map credentials should be preserved. call_command("generate_postfix_maps", "--destdir", self.workdir) @@ -36,8 +38,8 @@ class MapFilesTestCase(MapFilesTestCaseMixin, TestCase): # Now force overwrite, credentials should be different call_command( - "generate_postfix_maps", "--destdir", self.workdir, - "--force-overwrite") + "generate_postfix_maps", "--destdir", self.workdir, "--force-overwrite" + ) dbsettings = settings.DATABASES["default"] for f in self.MAP_FILES: mapcontent = parse_map_file(os.path.join(self.workdir, f)) @@ -45,8 +47,7 @@ class MapFilesTestCase(MapFilesTestCaseMixin, TestCase): self.assertEqual(mapcontent["dbpath"], dbsettings["NAME"]) else: self.assertEqual(mapcontent["user"], dbsettings["USER"]) - self.assertEqual( - mapcontent["password"], dbsettings["PASSWORD"]) + self.assertEqual(mapcontent["password"], dbsettings["PASSWORD"]) self.assertEqual(mapcontent["dbname"], dbsettings["NAME"]) # Now modify a file manually diff --git a/modoboa/admin/tests/test_mx.py b/modoboa/admin/tests/test_mx.py index a8718483d..12bbfd3ce 100644 --- a/modoboa/admin/tests/test_mx.py +++ b/modoboa/admin/tests/test_mx.py @@ -10,8 +10,7 @@ from django.test import override_settings from django.urls import reverse from django.utils.translation import gettext as _ -from modoboa.core import ( - models as core_models, factories as core_factories) +from modoboa.core import models as core_models, factories as core_factories from modoboa.lib.tests import ModoTestCase from . import utils from .. import factories, models @@ -27,23 +26,22 @@ class MXTestCase(ModoTestCase): super(MXTestCase, cls).setUpTestData() cls.domain = factories.DomainFactory(name="modoboa.org") # should not exist - cls.bad_domain = factories.DomainFactory( - name="does-not-exist.example.com") + cls.bad_domain = factories.DomainFactory(name="does-not-exist.example.com") # Add domain admin with mailbox mb = factories.MailboxFactory( - address="admin", domain=cls.bad_domain, + address="admin", + domain=cls.bad_domain, user__username="admin@does-not-exist.example.com", - user__groups=("DomainAdmins", ) + user__groups=("DomainAdmins",), ) cls.bad_domain.add_admin(mb.user) # Add domain admin with no mailbox admin = core_factories.UserFactory( - username="admin2@does-not-exist.example.com", - groups=("DomainAdmins", )) + username="admin2@does-not-exist.example.com", groups=("DomainAdmins",) + ) cls.bad_domain.add_admin(admin) - cls.localconfig.parameters.set_value( - "valid_mxs", "192.0.2.1 2001:db8::1") + cls.localconfig.parameters.set_value("valid_mxs", "192.0.2.1 2001:db8::1") cls.localconfig.save() models.MXRecord.objects.all().delete() @@ -51,15 +49,15 @@ class MXTestCase(ModoTestCase): @mock.patch("socket.getaddrinfo") @mock.patch.object(dns.resolver.Resolver, "resolve") def test_management_command( - self, mock_query, mock_getaddrinfo, mock_g_gethostbyname): + self, mock_query, mock_getaddrinfo, mock_g_gethostbyname + ): """Check that command works fine.""" mock_query.side_effect = utils.mock_dns_query_result mock_getaddrinfo.side_effect = utils.mock_ip_query_result mock_g_gethostbyname.return_value = "1.2.3.4" self.assertEqual(models.MXRecord.objects.count(), 0) management.call_command("modo", "check_mx", "--no-dnsbl", "--ttl=0") - self.assertTrue( - models.MXRecord.objects.filter(domain=self.domain).exists()) + self.assertTrue(models.MXRecord.objects.filter(domain=self.domain).exists()) # we passed a ttl to 0. this will reset the cache this time qs = models.MXRecord.objects.filter(domain=self.domain) @@ -79,32 +77,30 @@ class MXTestCase(ModoTestCase): @mock.patch("socket.getaddrinfo") @mock.patch.object(dns.resolver.Resolver, "resolve") def test_single_domain_update( - self, mock_query, mock_getaddrinfo, mock_g_gethostbyname): + self, mock_query, mock_getaddrinfo, mock_g_gethostbyname + ): """Update only one domain.""" mock_query.side_effect = utils.mock_dns_query_result mock_getaddrinfo.side_effect = utils.mock_ip_query_result mock_g_gethostbyname.return_value = "1.2.3.4" - management.call_command( - "modo", "check_mx", "--domain", self.domain.name) - self.assertTrue( - models.MXRecord.objects.filter(domain=self.domain).exists()) + management.call_command("modo", "check_mx", "--domain", self.domain.name) + self.assertTrue(models.MXRecord.objects.filter(domain=self.domain).exists()) self.assertFalse( - models.MXRecord.objects.filter(domain=self.bad_domain).exists()) + models.MXRecord.objects.filter(domain=self.bad_domain).exists() + ) - management.call_command( - "modo", "check_mx", "--domain", str(self.bad_domain.pk)) + management.call_command("modo", "check_mx", "--domain", str(self.bad_domain.pk)) self.assertFalse( - models.MXRecord.objects.filter(domain=self.bad_domain).exists()) + models.MXRecord.objects.filter(domain=self.bad_domain).exists() + ) self.assertEqual(len(mail.outbox), 1) - management.call_command( - "modo", "check_mx", "--domain", "toto.com") + management.call_command("modo", "check_mx", "--domain", "toto.com") @mock.patch("gevent.socket.gethostbyname") @mock.patch("socket.getaddrinfo") @mock.patch.object(dns.resolver.Resolver, "resolve") - def test_invalid_mx( - self, mock_query, mock_getaddrinfo, mock_g_gethostbyname): + def test_invalid_mx(self, mock_query, mock_getaddrinfo, mock_g_gethostbyname): """Test to check if invalid MX records are detected.""" mock_query.side_effect = utils.mock_dns_query_result mock_getaddrinfo.side_effect = utils.mock_ip_query_result @@ -112,16 +108,19 @@ class MXTestCase(ModoTestCase): domain = factories.DomainFactory(name="invalid-mx.com") # Add domain admin with mailbox mb = factories.MailboxFactory( - address="admin", domain=domain, + address="admin", + domain=domain, user__username="admin@{}".format(domain.name), - user__groups=("DomainAdmins", ) + user__groups=("DomainAdmins",), ) domain.add_admin(mb.user) management.call_command( - "modo", "check_mx", "--domain", domain.name, "--no-dnsbl") + "modo", "check_mx", "--domain", domain.name, "--no-dnsbl" + ) self.assertEqual(domain.alarms.count(), 1) management.call_command( - "modo", "check_mx", "--domain", domain.name, "--no-dnsbl") + "modo", "check_mx", "--domain", domain.name, "--no-dnsbl" + ) self.assertEqual(domain.alarms.count(), 1) self.assertEqual(len(mail.outbox), 1) @@ -149,27 +148,41 @@ class MXTestCase(ModoTestCase): get_domain_mx_list("no-answer.example.Com") get_domain_mx_list("bad-response.example.com") log.check( - ("modoboa.admin", "ERROR", - _("No DNS record found for %s") - % "does-not-exist.example.com"), - ("modoboa.admin", "ERROR", - _("No MX record for %s") % "no-mx.example.com"), + ( + "modoboa.admin", + "ERROR", + _("No DNS record found for %s") % "does-not-exist.example.com", + ), + ("modoboa.admin", "ERROR", _("No MX record for %s") % "no-mx.example.com"), ("modoboa.admin", "ERROR", _("No working name servers found")), - ("modoboa.admin", "WARNING", + ( + "modoboa.admin", + "WARNING", _("DNS resolution timeout, unable to query %s at the moment") - % "timeout.example.com"), - ("modoboa.admin", "ERROR", - _("No DNS record found for %s") - % "does-not-exist.example.com"), - ("modoboa.admin", "ERROR", - _("No DNS record found for %s") - % "does-not-exist.example.com"), - ("modoboa.admin", "WARNING", - _("Invalid IP address format for %(domain)s; %(addr)s") - % {"domain": "bad-response.example.com", "addr": "000.0.0.0"}), - ("modoboa.admin", "WARNING", - _("Invalid IP address format for %(domain)s; %(addr)s") - % {"domain": "bad-response.example.com", "addr": "000.0.0.0"}), + % "timeout.example.com", + ), + ( + "modoboa.admin", + "ERROR", + _("No DNS record found for %s") % "does-not-exist.example.com", + ), + ( + "modoboa.admin", + "ERROR", + _("No DNS record found for %s") % "does-not-exist.example.com", + ), + ( + "modoboa.admin", + "WARNING", + _("Invalid IP address format for %(domain)s; %(addr)s") + % {"domain": "bad-response.example.com", "addr": "000.0.0.0"}, + ), + ( + "modoboa.admin", + "WARNING", + _("Invalid IP address format for %(domain)s; %(addr)s") + % {"domain": "bad-response.example.com", "addr": "000.0.0.0"}, + ), ) @mock.patch("ipaddress.ip_address") @@ -184,9 +197,7 @@ class MXTestCase(ModoTestCase): with LogCapture("modoboa.admin") as log: get_domain_mx_list("test3.com") log.check( - ("modoboa.admin", "ERROR", - _("No AAAA record for %s") - % "mx3.example.com") + ("modoboa.admin", "ERROR", _("No AAAA record for %s") % "mx3.example.com") ) localconfig = core_models.LocalConfig.objects.first() localconfig.parameters.set_value("enable_ipv6_mx_checks", False, "admin") @@ -207,28 +218,32 @@ class DNSBLTestCase(ModoTestCase): cls.domain = factories.DomainFactory(name="modoboa.org") factories.DomainFactory(name="does-not-exist.example.com") cls.domain2 = factories.DomainFactory( - name="test.localhost") # Should not be checked + name="test.localhost" + ) # Should not be checked cls.domain3 = factories.DomainFactory( - name="modoboa.com", enable_dns_checks=False) + name="modoboa.com", enable_dns_checks=False + ) models.DNSBLResult.objects.all().delete() @mock.patch("gevent.socket.gethostbyname") @mock.patch("socket.getaddrinfo") @mock.patch.object(dns.resolver.Resolver, "resolve") def test_management_command( - self, mock_query, mock_getaddrinfo, mock_g_gethostbyname): + self, mock_query, mock_getaddrinfo, mock_g_gethostbyname + ): """Check that command works fine.""" mock_query.side_effect = utils.mock_dns_query_result mock_getaddrinfo.side_effect = utils.mock_ip_query_result mock_g_gethostbyname.return_value = "1.2.3.4" self.assertEqual(models.DNSBLResult.objects.count(), 0) management.call_command("modo", "check_mx") - self.assertTrue( - models.DNSBLResult.objects.filter(domain=self.domain).exists()) + self.assertTrue(models.DNSBLResult.objects.filter(domain=self.domain).exists()) self.assertFalse( - models.DNSBLResult.objects.filter(domain=self.domain3).exists()) + models.DNSBLResult.objects.filter(domain=self.domain3).exists() + ) response = self.client.get( - reverse("admin:dnsbl_domain_detail", args=[self.domain.pk])) + reverse("admin:dnsbl_domain_detail", args=[self.domain.pk]) + ) self.assertEqual(response.status_code, 200) self.assertFalse(self.domain.uses_a_reserved_tld) self.assertTrue(self.domain2.uses_a_reserved_tld) @@ -236,44 +251,43 @@ class DNSBLTestCase(ModoTestCase): @mock.patch("gevent.socket.gethostbyname") @mock.patch("socket.getaddrinfo") @mock.patch.object(dns.resolver.Resolver, "resolve") - def test_notifications( - self, mock_query, mock_getaddrinfo, mock_g_gethostbyname): + def test_notifications(self, mock_query, mock_getaddrinfo, mock_g_gethostbyname): """Check notifications.""" mock_query.side_effect = utils.mock_dns_query_result mock_getaddrinfo.side_effect = utils.mock_ip_query_result mock_g_gethostbyname.return_value = "127.0.0.4" - management.call_command( - "modo", "check_mx", "--email", "user@example.test") + management.call_command("modo", "check_mx", "--email", "user@example.test") self.assertEqual(len(mail.outbox), 2) @mock.patch("gevent.socket.gethostbyname") @mock.patch("socket.getaddrinfo") @mock.patch.object(dns.resolver.Resolver, "resolve") def test_notifications_wrong_dnsbl_response( - self, mock_query, mock_getaddrinfo, mock_g_gethostbyname): + self, mock_query, mock_getaddrinfo, mock_g_gethostbyname + ): """Check notifications.""" mock_query.side_effect = utils.mock_dns_query_result mock_getaddrinfo.side_effect = utils.mock_ip_query_result - mock_g_gethostbyname.return_value = "127.255.255.254" #<--Spamhaus response when querying from an open resolver - management.call_command( - "modo", "check_mx", "--email", "user@example.test") + mock_g_gethostbyname.return_value = "127.255.255.254" # <--Spamhaus response when querying from an open resolver + management.call_command("modo", "check_mx", "--email", "user@example.test") self.assertEqual(len(mail.outbox), 1) @mock.patch("gevent.socket.gethostbyname") @mock.patch("socket.getaddrinfo") @mock.patch.object(dns.resolver.Resolver, "resolve") def test_management_command_no_dnsbl( - self, mock_query, mock_getaddrinfo, mock_g_gethostbyname): + self, mock_query, mock_getaddrinfo, mock_g_gethostbyname + ): """Check that command works fine without dnsbl.""" mock_query.side_effect = utils.mock_dns_query_result mock_getaddrinfo.side_effect = utils.mock_ip_query_result mock_g_gethostbyname.return_value = "1.2.3.4" self.assertEqual(models.DNSBLResult.objects.count(), 0) management.call_command("modo", "check_mx", "--no-dnsbl") - self.assertFalse( - models.DNSBLResult.objects.filter(domain=self.domain).exists()) + self.assertFalse(models.DNSBLResult.objects.filter(domain=self.domain).exists()) response = self.client.get( - reverse("admin:dnsbl_domain_detail", args=[self.domain.pk])) + reverse("admin:dnsbl_domain_detail", args=[self.domain.pk]) + ) self.assertEqual(response.status_code, 200) @@ -290,7 +304,8 @@ class DNSChecksTestCase(ModoTestCase): @mock.patch("socket.getaddrinfo") @mock.patch.object(dns.resolver.Resolver, "resolve") def test_management_command( - self, mock_query, mock_getaddrinfo, mock_g_gethostbyname): + self, mock_query, mock_getaddrinfo, mock_g_gethostbyname + ): """Check that command works fine.""" mock_query.side_effect = utils.mock_dns_query_result mock_getaddrinfo.side_effect = utils.mock_ip_query_result diff --git a/modoboa/admin/tests/test_need_dovecot_update.py b/modoboa/admin/tests/test_need_dovecot_update.py index d55956a91..287348f37 100644 --- a/modoboa/admin/tests/test_need_dovecot_update.py +++ b/modoboa/admin/tests/test_need_dovecot_update.py @@ -48,21 +48,24 @@ class NeedDovecotUpdateTestCase(ModoTestCase): self.assertTrue(self.localconfig.need_dovecot_update) tmp_file = os.path.join(self.workdir, "test-dovecot-ldap.conf") - self.set_global_parameters({ - "authentication_type": "ldap", - "ldap_dovecot_sync": True, - "ldap_dovecot_conf_file": tmp_file, - "ldap_server_address": "localhost", - "ldap_server_port": "636", - "ldap_enable_secondary_server": True, - "ldap_secondary_server_address": "localhost2", - "ldap_secondary_server_port": "636", - "ldap_secured": "ssl", - "ldap_bind_dn": "DC=test,DC=lan", - "ldap_bind_password": "test", - "ldap_search_base": "CN=Users,DC=test,DC=lan", - "ldap_search_filter": "(& (objectClass=user) (|(mail=%(user)s)(sAMAccountName=%(user)s)) )" - }, app="core") + self.set_global_parameters( + { + "authentication_type": "ldap", + "ldap_dovecot_sync": True, + "ldap_dovecot_conf_file": tmp_file, + "ldap_server_address": "localhost", + "ldap_server_port": "636", + "ldap_enable_secondary_server": True, + "ldap_secondary_server_address": "localhost2", + "ldap_secondary_server_port": "636", + "ldap_secured": "ssl", + "ldap_bind_dn": "DC=test,DC=lan", + "ldap_bind_password": "test", + "ldap_search_base": "CN=Users,DC=test,DC=lan", + "ldap_search_filter": "(& (objectClass=user) (|(mail=%(user)s)(sAMAccountName=%(user)s)) )", + }, + app="core", + ) # Generated file checks call_command("update_dovecot_conf") @@ -71,11 +74,17 @@ class NeedDovecotUpdateTestCase(ModoTestCase): with open(tmp_file) as tmp: content = tmp.read() self.assertIn("uris = ldaps://localhost:636 ldaps://localhost2:636", content) - self.assertIn("dn = \"DC=test,DC=lan\"", content) - self.assertIn("dnpass = \'test\'", content) + self.assertIn('dn = "DC=test,DC=lan"', content) + self.assertIn("dnpass = 'test'", content) self.assertIn("base = CN=Users,DC=test,DC=lan", content) - self.assertIn("user_filter = (& (objectClass=user) (|(mail=%u)(sAMAccountName=%u)) )", content) - self.assertIn("pass_filter = (& (objectClass=user) (|(mail=%u)(sAMAccountName=%u)) )", content) + self.assertIn( + "user_filter = (& (objectClass=user) (|(mail=%u)(sAMAccountName=%u)) )", + content, + ) + self.assertIn( + "pass_filter = (& (objectClass=user) (|(mail=%u)(sAMAccountName=%u)) )", + content, + ) self.localconfig.refresh_from_db() self.assertFalse(self.localconfig.need_dovecot_update) diff --git a/modoboa/admin/tests/test_password_schemes.py b/modoboa/admin/tests/test_password_schemes.py index 26ecbe41c..de48eda3f 100644 --- a/modoboa/admin/tests/test_password_schemes.py +++ b/modoboa/admin/tests/test_password_schemes.py @@ -15,15 +15,18 @@ class PasswordSchemesTestCase(ModoTestCase): def _create_account(self): values = { - "username": "tester@test.com", "first_name": "Tester", - "last_name": "Toto", "password1": "Toto1234", - "password2": "Toto1234", "role": "SimpleUsers", "quota_act": True, - "is_active": True, "email": "tester@test.com", "stepid": "step2" + "username": "tester@test.com", + "first_name": "Tester", + "last_name": "Toto", + "password1": "Toto1234", + "password2": "Toto1234", + "role": "SimpleUsers", + "quota_act": True, + "is_active": True, + "email": "tester@test.com", + "stepid": "step2", } - self.ajax_post( - reverse("admin:account_add"), - values - ) + self.ajax_post(reverse("admin:account_add"), values) def _test_scheme(self, name, startpattern): self.set_global_parameter("password_scheme", name, app="core") diff --git a/modoboa/admin/tests/test_repair.py b/modoboa/admin/tests/test_repair.py index 4a12b6876..6a6a1e6bb 100644 --- a/modoboa/admin/tests/test_repair.py +++ b/modoboa/admin/tests/test_repair.py @@ -51,10 +51,11 @@ class RepairTestCase(ModoTestCase): def test_management_command_with_no_alias(self): """Check that problem is fixed.""" count, detail = models.Alias.objects.filter( - address="user@test.com", internal=True).delete() + address="user@test.com", internal=True + ).delete() self.assertEqual(count, 3) ret = management.call_command("modo", "repair", "--quiet") assert ret is None self.assertTrue( - models.Alias.objects.filter( - address="user@test.com", internal=True).exists()) + models.Alias.objects.filter(address="user@test.com", internal=True).exists() + ) diff --git a/modoboa/admin/tests/test_user.py b/modoboa/admin/tests/test_user.py index 346300cce..f4614aab4 100644 --- a/modoboa/admin/tests/test_user.py +++ b/modoboa/admin/tests/test_user.py @@ -19,10 +19,7 @@ class ForwardTestCase(ModoTestCase): self.client.logout() self.client.login(username="user@test.com", password="toto") url = reverse("user_forward") - self.ajax_post( - url, - {"dest": "user@extdomain.com", "keepcopies": True} - ) + self.ajax_post(url, {"dest": "user@extdomain.com", "keepcopies": True}) response = self.client.get(url) self.assertEqual(response.status_code, 200) content = response.json() @@ -38,10 +35,7 @@ class ForwardTestCase(ModoTestCase): self.assertTrue(selfalias.enabled) # Deactivate local copies - self.ajax_post( - url, - {"dest": "user@extdomain.com", "keepcopies": False} - ) + self.ajax_post(url, {"dest": "user@extdomain.com", "keepcopies": False}) selfalias.refresh_from_db() self.assertFalse(selfalias.enabled) diff --git a/modoboa/admin/tests/utils.py b/modoboa/admin/tests/utils.py index b124dc91b..f1b9eeaf5 100644 --- a/modoboa/admin/tests/utils.py +++ b/modoboa/admin/tests/utils.py @@ -31,12 +31,17 @@ _SPF_RECORD = TXT("IN", "TXT", ["v=spf1 mx -all"]) _DMARC_RECORD = TXT("IN", "TXT", ["v=DMARC1 p=reject"]) _DKIM_RECORD = TXT("IN", "TXT", ["v=DKIM1 p=XXXXX"]) _BAD_MX_RECORD = MX("IN", "MX", 10, Name("bad-response.example.com".split("."))) -_DNE_MX_RECORD = MX("IN", "MX", 10, Name( - "does-not-exist.example.com".split("."))) +_DNE_MX_RECORD = MX("IN", "MX", 10, Name("does-not-exist.example.com".split("."))) _MX_RECORDS = [_MX_RECORD_1] _IP_SET_RECORDS = [RRset()] -_IPV4_RECORD_REDIS = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("127.0.0.1", int(settings.REDIS_PORT))) +_IPV4_RECORD_REDIS = ( + socket.AF_INET, + socket.SOCK_STREAM, + 6, + "", + ("127.0.0.1", int(settings.REDIS_PORT)), +) _IPV4_RECORD_1 = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.1", 25)) _IPV4_RECORD_2 = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.2", 25)) _IPV6_RECORD = (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("2001:db8::1", 25)) @@ -53,7 +58,7 @@ _POSSIBLE_DNS_RESULTS = { "no-lookup.example.com": [_DNE_MX_RECORD], "no-answer.example.com": [_DNE_MX_RECORD], "dns-checks.com": [_MX_RECORD_2], - "invalid-mx.com": [_MX_RECORD_3] + "invalid-mx.com": [_MX_RECORD_3], } _POSSIBLE_DNS_RESULTS_NO_MX = { @@ -62,12 +67,16 @@ _POSSIBLE_DNS_RESULTS_NO_MX = { "no-lookup.example.com": NXDOMAIN(), "no-answer.example.com": NoAnswer(), "bad-response.example.com": [RRsetInvalid()], - "dns-checks.com": [_SPF_RECORD, ], + "dns-checks.com": [ + _SPF_RECORD, + ], "modoboa._domainkey.dns-checks.com": [_DKIM_RECORD], - "_dmarc.dns-checks.com": [_DMARC_RECORD, ], + "_dmarc.dns-checks.com": [ + _DMARC_RECORD, + ], "autoconfig.dns-checks.com": [_A_RECORD], "autodiscover.dns-checks.com": [_A_RECORD], - "mx3.example.com": [_A_RECORD] + "mx3.example.com": [_A_RECORD], } _POSSIBLE_IP_RESULTS = { settings.REDIS_HOST: [_IPV4_RECORD_REDIS], @@ -82,8 +91,8 @@ _POSSIBLE_IP_RESULTS = { _POSSIBLE_DNS_IPV6_RESULTS = { settings.REDIS_HOST: socket.getaddrinfo(settings.REDIS_HOST, settings.REDIS_PORT), - "mx3.example.com": NoAnswer() - } + "mx3.example.com": NoAnswer(), +} def mock_dns_query_result(qname, *args, **kwargs): diff --git a/modoboa/admin/urls.py b/modoboa/admin/urls.py index 5b741ea53..ec3bd4b05 100644 --- a/modoboa/admin/urls.py +++ b/modoboa/admin/urls.py @@ -4,81 +4,76 @@ from django.urls import path from . import views from .views import ( - alias as alias_views, domain as domain_views, export as export_views, - identity as identity_views, import_ as import_views + alias as alias_views, + domain as domain_views, + export as export_views, + identity as identity_views, + import_ as import_views, ) app_name = "admin" urlpatterns = [ - path('', domain_views.index, name="index"), - path('domains/', domain_views.domains, name="domain_list"), - path('domains//', views.DomainDetailView.as_view(), - name="domain_detail"), - path('domains//alarms/', views.DomainAlarmsView.as_view(), - name="domain_alarms"), - path('domains//dnsbl/', views.DNSBLDomainDetailView.as_view(), - name="dnsbl_domain_detail"), - path('domains//mx/', views.MXDomainDetailView.as_view(), - name="mx_domain_detail"), - path('domains/list/', domain_views._domains, name="_domain_list"), - path('domains/quotas/', domain_views.list_quotas, - name="domain_quota_list"), - path('domains/logs/', domain_views.list_logs, - name="domain_logs_list"), - path('domains/flatlist/', domain_views.domains_list, - name="domain_flat_list"), - path('domains/new/', domain_views.newdomain, name="domain_add"), - path('domains//edit/', domain_views.editdomain, - name="domain_change"), - path('domains//delete/', domain_views.deldomain, - name="domain_delete"), - path('domains/page/', domain_views.get_next_page, - name="domain_page"), + path("", domain_views.index, name="index"), + path("domains/", domain_views.domains, name="domain_list"), + path("domains//", views.DomainDetailView.as_view(), name="domain_detail"), + path( + "domains//alarms/", + views.DomainAlarmsView.as_view(), + name="domain_alarms", + ), + path( + "domains//dnsbl/", + views.DNSBLDomainDetailView.as_view(), + name="dnsbl_domain_detail", + ), + path( + "domains//mx/", + views.MXDomainDetailView.as_view(), + name="mx_domain_detail", + ), + path("domains/list/", domain_views._domains, name="_domain_list"), + path("domains/quotas/", domain_views.list_quotas, name="domain_quota_list"), + path("domains/logs/", domain_views.list_logs, name="domain_logs_list"), + path("domains/flatlist/", domain_views.domains_list, name="domain_flat_list"), + path("domains/new/", domain_views.newdomain, name="domain_add"), + path("domains//edit/", domain_views.editdomain, name="domain_change"), + path("domains//delete/", domain_views.deldomain, name="domain_delete"), + path("domains/page/", domain_views.get_next_page, name="domain_page"), ] urlpatterns += [ - path('permissions/remove/', identity_views.remove_permission, - name="permission_remove"), - - path('identities/', identity_views.identities, name="identity_list"), - path('identities/list/', identity_views._identities, - name="_identity_list"), - path('identities/quotas/', identity_views.list_quotas, - name="identity_quota_list"), - path('identities/page/', identity_views.get_next_page, - name="identity_page"), - - path('accounts/list/', identity_views.accounts_list, - name="account_list"), - path('accounts/new/', identity_views.newaccount, name="account_add"), - path('accounts//', views.AccountDetailView.as_view(), - name="account_detail"), - path('accounts//edit/', identity_views.editaccount, - name="account_change"), - path('accounts//delete/', identity_views.delaccount, - name="account_delete"), + path( + "permissions/remove/", + identity_views.remove_permission, + name="permission_remove", + ), + path("identities/", identity_views.identities, name="identity_list"), + path("identities/list/", identity_views._identities, name="_identity_list"), + path("identities/quotas/", identity_views.list_quotas, name="identity_quota_list"), + path("identities/page/", identity_views.get_next_page, name="identity_page"), + path("accounts/list/", identity_views.accounts_list, name="account_list"), + path("accounts/new/", identity_views.newaccount, name="account_add"), + path( + "accounts//", views.AccountDetailView.as_view(), name="account_detail" + ), + path("accounts//edit/", identity_views.editaccount, name="account_change"), + path("accounts//delete/", identity_views.delaccount, name="account_delete"), ] urlpatterns += [ - path('aliases/new/', alias_views.newalias, name="alias_add"), - path('aliases//', views.AliasDetailView.as_view(), - name="alias_detail"), - path('aliases//edit/', alias_views.editalias, - name="alias_change"), - path('aliases/delete/', alias_views.delalias, name="alias_delete"), + path("aliases/new/", alias_views.newalias, name="alias_add"), + path("aliases//", views.AliasDetailView.as_view(), name="alias_detail"), + path("aliases//edit/", alias_views.editalias, name="alias_change"), + path("aliases/delete/", alias_views.delalias, name="alias_delete"), ] urlpatterns += [ - path('domains/import/', import_views.import_domains, - name="domain_import"), - path('identities/import/', import_views.import_identities, - name="identity_import"), + path("domains/import/", import_views.import_domains, name="domain_import"), + path("identities/import/", import_views.import_identities, name="identity_import"), ] urlpatterns += [ - path('domains/export/', export_views.export_domains, - name="domain_export"), - path('identities/export/', export_views.export_identities, - name="identity_export"), + path("domains/export/", export_views.export_domains, name="domain_export"), + path("identities/export/", export_views.export_identities, name="identity_export"), ] diff --git a/modoboa/admin/views/alias.py b/modoboa/admin/views/alias.py index b22132dca..ac491811d 100644 --- a/modoboa/admin/views/alias.py +++ b/modoboa/admin/views/alias.py @@ -35,18 +35,15 @@ def _validate_alias(request, form, successmsg, callback=None): return render_to_json_response({"form_errors": form.errors}, status=400) -def _new_alias(request, title, action, successmsg, - tplname="admin/aliasform.html"): - core_signals.can_create_object.send( - "new_alias", context=request.user, klass=Alias) +def _new_alias(request, title, action, successmsg, tplname="admin/aliasform.html"): + core_signals.can_create_object.send("new_alias", context=request.user, klass=Alias) if request.method == "POST": + def callback(user, alias): alias.post_create(user) form = AliasForm(request.user, request.POST) - return _validate_alias( - request, form, successmsg, callback - ) + return _validate_alias(request, form, successmsg, callback) ctx = { "title": title, @@ -54,7 +51,7 @@ def _new_alias(request, title, action, successmsg, "formid": "aliasform", "action_label": _("Create"), "action_classes": "submit", - "form": AliasForm(request.user) + "form": AliasForm(request.user), } return render(request, tplname, ctx) @@ -64,8 +61,7 @@ def _new_alias(request, title, action, successmsg, @reversion.create_revision() def newalias(request): return _new_alias( - request, _("New alias"), reverse("admin:alias_add"), - _("Alias created") + request, _("New alias"), reverse("admin:alias_add"), _("Alias created") ) @@ -87,7 +83,7 @@ def editalias(request, alid, tplname="admin/aliasform.html"): "title": alias.address, "action_label": _("Update"), "action_classes": "submit", - "form": AliasForm(request.user, instance=alias) + "form": AliasForm(request.user, instance=alias), } return render(request, tplname, ctx) @@ -106,8 +102,7 @@ def delalias(request): return render_to_json_response(msg) -class AliasDetailView( - auth_mixins.PermissionRequiredMixin, generic.DetailView): +class AliasDetailView(auth_mixins.PermissionRequiredMixin, generic.DetailView): """DetailView for Alias.""" model = Alias diff --git a/modoboa/admin/views/dns.py b/modoboa/admin/views/dns.py index 3a46b84c5..cfdde03eb 100644 --- a/modoboa/admin/views/dns.py +++ b/modoboa/admin/views/dns.py @@ -7,8 +7,7 @@ from django.views import generic from .. import models -class MXDomainDetailView( - auth_mixins.PermissionRequiredMixin, generic.DetailView): +class MXDomainDetailView(auth_mixins.PermissionRequiredMixin, generic.DetailView): """View to display MX records.""" model = models.Domain @@ -18,20 +17,19 @@ class MXDomainDetailView( def get_queryset(self): """Add some prefetching.""" return ( - super(MXDomainDetailView, self).get_queryset() - .prefetch_related("mxrecord_set")) + super(MXDomainDetailView, self) + .get_queryset() + .prefetch_related("mxrecord_set") + ) def get_context_data(self, **kwargs): """Add extra variables.""" context = super(MXDomainDetailView, self).get_context_data(**kwargs) - context.update({ - "title": _("MX records of {}").format(self.object.name) - }) + context.update({"title": _("MX records of {}").format(self.object.name)}) return context -class DNSBLDomainDetailView( - auth_mixins.PermissionRequiredMixin, generic.DetailView): +class DNSBLDomainDetailView(auth_mixins.PermissionRequiredMixin, generic.DetailView): """View to display DNSBL summary.""" model = models.Domain @@ -41,13 +39,13 @@ class DNSBLDomainDetailView( def get_queryset(self): """Add some prefetching.""" return ( - super(DNSBLDomainDetailView, self).get_queryset() - .prefetch_related("dnsblresult_set")) + super(DNSBLDomainDetailView, self) + .get_queryset() + .prefetch_related("dnsblresult_set") + ) def get_context_data(self, **kwargs): """Add extra variables.""" context = super(DNSBLDomainDetailView, self).get_context_data(**kwargs) - context.update({ - "title": _("DNSBL summary for {}").format(self.object.name) - }) + context.update({"title": _("DNSBL summary for {}").format(self.object.name)}) return context diff --git a/modoboa/admin/views/domain.py b/modoboa/admin/views/domain.py index 83c1218e9..b16980e7a 100644 --- a/modoboa/admin/views/domain.py +++ b/modoboa/admin/views/domain.py @@ -6,7 +6,9 @@ from reversion import revisions as reversion from django.contrib.auth import mixins as auth_mixins from django.contrib.auth.decorators import ( - login_required, permission_required, user_passes_test + login_required, + permission_required, + user_passes_test, ) from django.db.models import Q, Sum from django.http import HttpResponseRedirect @@ -37,15 +39,15 @@ def index(request): @login_required @user_passes_test( - lambda u: u.has_perm("admin.view_domain") or - u.has_perm("admin.view_mailbox") + lambda u: u.has_perm("admin.view_domain") or u.has_perm("admin.view_mailbox") ) def _domains(request): sort_order, sort_dir = get_sort_order(request.GET, "name") extra_filters = signals.extra_domain_filters.send(sender="_domains") if extra_filters: extra_filters = reduce( - lambda a, b: a + b, [result[1] for result in extra_filters]) + lambda a, b: a + b, [result[1] for result in extra_filters] + ) filters = { flt: request.GET.get(flt, None) for flt in ["domfilter", "searchquery"] + extra_filters @@ -54,17 +56,19 @@ def _domains(request): domainlist = get_domains(request.user, **filters) if sort_order == "name": domainlist = sorted( - domainlist, - key=lambda d: getattr(d, sort_order), reverse=sort_dir == "-" + domainlist, key=lambda d: getattr(d, sort_order), reverse=sort_dir == "-" ) else: - domainlist = sorted(domainlist, key=lambda d: d.tags[0]["name"], - reverse=sort_dir == "-") + domainlist = sorted( + domainlist, key=lambda d: d.tags[0]["name"], reverse=sort_dir == "-" + ) context = { "handle_mailboxes": request.localconfig.parameters.get_value( - "handle_mailboxes", raise_exception=False), + "handle_mailboxes", raise_exception=False + ), "auto_account_removal": request.localconfig.parameters.get_value( - "auto_account_removal"), + "auto_account_removal" + ), } page = get_listing_page(domainlist, request.GET.get("page", 1)) parameters = request.localconfig.parameters @@ -73,9 +77,8 @@ def _domains(request): "enable_spf_checks": parameters.get_value("enable_spf_checks"), "enable_dkim_checks": parameters.get_value("enable_dkim_checks"), "enable_dmarc_checks": parameters.get_value("enable_dmarc_checks"), - "enable_autoconfig_checks": ( - parameters.get_value("enable_autoconfig_checks")), - "enable_dnsbl_checks": parameters.get_value("enable_dnsbl_checks") + "enable_autoconfig_checks": (parameters.get_value("enable_autoconfig_checks")), + "enable_dnsbl_checks": parameters.get_value("enable_dnsbl_checks"), } context["headers"] = render_to_string( "admin/domain_headers.html", dns_checks, request @@ -97,28 +100,31 @@ def _domains(request): def domains(request, tplname="admin/domains.html"): if not request.user.has_perm("admin.view_domain"): if request.user.has_perm("admin.view_mailbox"): - return HttpResponseRedirect( - reverse("admin:identity_list") - ) + return HttpResponseRedirect(reverse("admin:identity_list")) return HttpResponseRedirect(reverse("core:user_index")) parameters = request.localconfig.parameters - return render(request, tplname, { - "selection": "domains", - "enable_mx_checks": parameters.get_value("enable_mx_checks"), - "enable_spf_checks": parameters.get_value("enable_spf_checks"), - "enable_dkim_checks": parameters.get_value("enable_dkim_checks"), - "enable_dmarc_checks": parameters.get_value("enable_dmarc_checks"), - "enable_autoconfig_checks": ( - parameters.get_value("enable_autoconfig_checks")), - "enable_dnsbl_checks": parameters.get_value("enable_dnsbl_checks") - }) + return render( + request, + tplname, + { + "selection": "domains", + "enable_mx_checks": parameters.get_value("enable_mx_checks"), + "enable_spf_checks": parameters.get_value("enable_spf_checks"), + "enable_dkim_checks": parameters.get_value("enable_dkim_checks"), + "enable_dmarc_checks": parameters.get_value("enable_dmarc_checks"), + "enable_autoconfig_checks": ( + parameters.get_value("enable_autoconfig_checks") + ), + "enable_dnsbl_checks": parameters.get_value("enable_dnsbl_checks"), + }, + ) @login_required @user_passes_test( - lambda u: u.has_perm("admin.view_domain") or - u.has_perm("admin.view_mailbox") or - u.has_perm("admin.add_domain") + lambda u: u.has_perm("admin.view_domain") + or u.has_perm("admin.view_mailbox") + or u.has_perm("admin.add_domain") ) def get_next_page(request): """Return the next page of the domain or quota list.""" @@ -146,15 +152,12 @@ def list_quotas(request): if sort_order in ["name", "quota"]: domains = domains.order_by("{}{}".format(sort_dir, sort_order)) elif sort_order == "allocated_quota": - domains = ( - domains.annotate(allocated_quota=Sum("mailbox__quota")) - .order_by("{}{}".format(sort_dir, sort_order)) + domains = domains.annotate(allocated_quota=Sum("mailbox__quota")).order_by( + "{}{}".format(sort_dir, sort_order) ) page = get_listing_page(domains, request.GET.get("page", 1)) context = { - "headers": render_to_string( - "admin/domains_quota_headers.html", {}, request - ) + "headers": render_to_string("admin/domains_quota_headers.html", {}, request) } if page is None: context["length"] = 0 @@ -182,16 +185,14 @@ def list_logs(request): logs = logs.order_by("{}{}".format(sort_dir, sort_order)) if search: logs = logs.filter( - Q(sender__icontains=search) | - Q(rcpt__icontains=search) | - Q(queue_id__icontains=search) | - Q(status__icontains=search) + Q(sender__icontains=search) + | Q(rcpt__icontains=search) + | Q(queue_id__icontains=search) + | Q(status__icontains=search) ) page = get_listing_page(logs, request.GET.get("page", 1)) context = { - "headers": render_to_string( - "admin/domains_log_headers.html", {}, request - ) + "headers": render_to_string("admin/domains_log_headers.html", {}, request) } if page is None: context["length"] = 0 @@ -207,8 +208,7 @@ def list_logs(request): @permission_required("admin.add_domain") @reversion.create_revision() def newdomain(request): - core_signals.can_create_object.send( - "newdomain", context=request.user, klass=Domain) + core_signals.can_create_object.send("newdomain", context=request.user, klass=Domain) return DomainWizard(request).process() @@ -224,7 +224,8 @@ def editdomain(request, dom_id): instances = {"general": domain} results = signals.get_domain_form_instances.send( - sender="editdomain", user=request.user, domain=domain) + sender="editdomain", user=request.user, domain=domain + ) for result in results: instances.update(result[1]) return DomainForm(request, instances=instances).process() @@ -251,8 +252,7 @@ def deldomain(request, dom_id): return render_to_json_response(msg) -class DomainDetailView( - auth_mixins.PermissionRequiredMixin, generic.DetailView): +class DomainDetailView(auth_mixins.PermissionRequiredMixin, generic.DetailView): """DetailView for Domain.""" model = Domain @@ -260,39 +260,40 @@ class DomainDetailView( def get_queryset(self): """Add some prefetching.""" - return ( - Domain.objects.get_for_admin(self.request.user) - .prefetch_related("domainalias_set", "mailbox_set", "alias_set") + return Domain.objects.get_for_admin(self.request.user).prefetch_related( + "domainalias_set", "mailbox_set", "alias_set" ) def get_context_data(self, **kwargs): """Include extra widgets.""" context = super(DomainDetailView, self).get_context_data(**kwargs) result = signals.extra_domain_dashboard_widgets.send( - self.__class__, user=self.request.user, domain=self.object) + self.__class__, user=self.request.user, domain=self.object + ) parameters = self.request.localconfig.parameters - context.update({ - "templates": {"left": [], "right": []}, - "enable_mx_checks": parameters.get_value("enable_mx_checks"), - "enable_spf_checks": parameters.get_value("enable_spf_checks"), - "enable_dkim_checks": parameters.get_value("enable_dkim_checks"), - "enable_dmarc_checks": parameters.get_value("enable_dmarc_checks"), - "enable_autoconfig_checks": ( - parameters.get_value("enable_autoconfig_checks")), - "enable_dnsbl_checks": parameters.get_value("enable_dnsbl_checks"), - }) + context.update( + { + "templates": {"left": [], "right": []}, + "enable_mx_checks": parameters.get_value("enable_mx_checks"), + "enable_spf_checks": parameters.get_value("enable_spf_checks"), + "enable_dkim_checks": parameters.get_value("enable_dkim_checks"), + "enable_dmarc_checks": parameters.get_value("enable_dmarc_checks"), + "enable_autoconfig_checks": ( + parameters.get_value("enable_autoconfig_checks") + ), + "enable_dnsbl_checks": parameters.get_value("enable_dnsbl_checks"), + } + ) for _receiver, widgets in result: for widget in widgets: - context["templates"][widget["column"]].append( - widget["template"]) + context["templates"][widget["column"]].append(widget["template"]) # FIXME: can raise conflicts... context.update(widget["context"]) return context -class DomainAlarmsView( - auth_mixins.PermissionRequiredMixin, generic.DetailView): +class DomainAlarmsView(auth_mixins.PermissionRequiredMixin, generic.DetailView): """A view to list domain alarms.""" model = Domain @@ -301,7 +302,6 @@ class DomainAlarmsView( def get_queryset(self): """Add some prefetching.""" - return ( - Domain.objects.get_for_admin(self.request.user) - .prefetch_related("alarms") + return Domain.objects.get_for_admin(self.request.user).prefetch_related( + "alarms" ) diff --git a/modoboa/admin/views/export.py b/modoboa/admin/views/export.py index b395a1865..ccb9ec6cc 100644 --- a/modoboa/admin/views/export.py +++ b/modoboa/admin/views/export.py @@ -4,7 +4,9 @@ import csv from io import StringIO from django.contrib.auth.decorators import ( - login_required, permission_required, user_passes_test + login_required, + permission_required, + user_passes_test, ) from django.http import HttpResponse from django.shortcuts import render @@ -48,7 +50,8 @@ def export_identities(request): fp = StringIO() csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"]) identities = get_identities( - request.user, **request.session["identities_filters"]) + request.user, **request.session["identities_filters"] + ) for ident in identities: ident.to_csv(csvwriter) content = fp.getvalue() @@ -75,8 +78,7 @@ def export_domains(request): form.is_valid() fp = StringIO() csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"]) - for dom in get_domains(request.user, - **request.session["domains_filters"]): + for dom in get_domains(request.user, **request.session["domains_filters"]): dom.to_csv(csvwriter) content = fp.getvalue() fp.close() diff --git a/modoboa/admin/views/identity.py b/modoboa/admin/views/identity.py index 2d9ea3786..09aa11a8f 100644 --- a/modoboa/admin/views/identity.py +++ b/modoboa/admin/views/identity.py @@ -4,7 +4,9 @@ from reversion import revisions as reversion from django.contrib.auth import mixins as auth_mixins from django.contrib.auth.decorators import ( - login_required, permission_required, user_passes_test + login_required, + permission_required, + user_passes_test, ) from django.shortcuts import render from django.template.loader import render_to_string @@ -25,8 +27,7 @@ from ..models import Domain, Mailbox @login_required @user_passes_test( - lambda u: u.has_perm("core.add_user") or - u.has_perm("admin.add_alias") + lambda u: u.has_perm("core.add_user") or u.has_perm("admin.add_alias") ) def _identities(request): filters = { @@ -35,28 +36,29 @@ def _identities(request): } request.session["identities_filters"] = filters idents_list = get_identities(request.user, **filters) - sort_order, sort_dir = get_sort_order(request.GET, "identity", - ["identity", "name_or_rcpt", "tags"]) + sort_order, sort_dir = get_sort_order( + request.GET, "identity", ["identity", "name_or_rcpt", "tags"] + ) if sort_order in ["identity", "name_or_rcpt"]: - objects = sorted(idents_list, key=lambda o: getattr(o, sort_order), - reverse=sort_dir == "-") + objects = sorted( + idents_list, key=lambda o: getattr(o, sort_order), reverse=sort_dir == "-" + ) else: - objects = sorted(idents_list, key=lambda o: o.tags[0]["label"], - reverse=sort_dir == "-") + objects = sorted( + idents_list, key=lambda o: o.tags[0]["label"], reverse=sort_dir == "-" + ) context = { "handle_mailboxes": request.localconfig.parameters.get_value( - "handle_mailboxes", raise_exception=False) + "handle_mailboxes", raise_exception=False + ) } page = get_listing_page(objects, request.GET.get("page", 1)) - context["headers"] = render_to_string( - "admin/identity_headers.html", {}, request) + context["headers"] = render_to_string("admin/identity_headers.html", {}, request) if page is None: context["length"] = 0 else: context["rows"] = render_to_string( - "admin/identities_table.html", { - "identities": page.object_list - }, request + "admin/identities_table.html", {"identities": page.object_list}, request ) context["pages"] = [page.number] return render_to_json_response(context) @@ -85,7 +87,7 @@ def list_quotas(request): select={"quota_value__bytes": "admin_quota.bytes"}, where=["admin_quota.username=%s" % where], tables=["admin_quota", "admin_domain"], - order_by=["%s%s" % (sort_dir, sort_order)] + order_by=["%s%s" % (sort_dir, sort_order)], ) else: if db_type == "postgres": @@ -102,15 +104,13 @@ def list_quotas(request): select={"quota_usage": select}, where=["admin_quota.username=%s" % where], tables=["admin_quota", "admin_domain"], - order_by=["%s%s" % (sort_dir, sort_order)] + order_by=["%s%s" % (sort_dir, sort_order)], ) else: raise BadRequest(_("Invalid request")) page = get_listing_page(mboxes, request.GET.get("page", 1)) context = { - "headers": render_to_string( - "admin/identities_quota_headers.html", {}, request - ) + "headers": render_to_string("admin/identities_quota_headers.html", {}, request) } if page is None: context["length"] = 0 @@ -124,9 +124,9 @@ def list_quotas(request): @login_required @user_passes_test( - lambda u: u.has_perm("core.add_user") or - u.has_perm("admin.add_alias") or - u.has_perm("admin.add_mailbox") + lambda u: u.has_perm("core.add_user") + or u.has_perm("admin.add_alias") + or u.has_perm("admin.add_mailbox") ) def get_next_page(request): """Return the next page of the identity list.""" @@ -137,22 +137,17 @@ def get_next_page(request): @login_required @user_passes_test( - lambda u: u.has_perm("core.add_user") or - u.has_perm("admin.add_alias") + lambda u: u.has_perm("core.add_user") or u.has_perm("admin.add_alias") ) @ensure_csrf_cookie def identities(request, tplname="admin/identities.html"): - return render(request, tplname, { - "selection": "identities", - "deflocation": "list/" - }) + return render(request, tplname, {"selection": "identities", "deflocation": "list/"}) @login_required @permission_required("core.add_user") def accounts_list(request): - accs = User.objects.filter(is_superuser=False) \ - .exclude(groups__name="SimpleUsers") + accs = User.objects.filter(is_superuser=False).exclude(groups__name="SimpleUsers") res = [a.username for a in accs.all()] return render_to_json_response(res) @@ -175,11 +170,10 @@ def editaccount(request, pk): raise PermDeniedException mb = account.mailbox if hasattr(account, "mailbox") else None - instances = { - "general": account, "profile": account, "mail": mb, "perms": account - } + instances = {"general": account, "profile": account, "mail": mb, "perms": account} results = signals.get_account_form_instances.send( - sender="editaccount", user=request.user, account=account) + sender="editaccount", user=request.user, account=account + ) for result in results: instances.update(result[1]) return AccountForm(request, instances=instances).process() @@ -190,9 +184,7 @@ def editaccount(request, pk): @require_http_methods(["POST"]) def delaccount(request, pk): User.objects.get(pk=pk).delete() - return render_to_json_response( - ngettext("Account deleted", "Accounts deleted", 1) - ) + return render_to_json_response(ngettext("Account deleted", "Accounts deleted", 1)) @login_required @@ -208,15 +200,13 @@ def remove_permission(request): domain = Domain.objects.get(pk=domid) except (User.DoesNotExist, Domain.DoesNotExist): raise BadRequest(_("Invalid request")) - if not request.user.can_access(account) or \ - not request.user.can_access(domain): + if not request.user.can_access(account) or not request.user.can_access(domain): raise PermDeniedException domain.remove_admin(account) return render_to_json_response({}) -class AccountDetailView( - auth_mixins.PermissionRequiredMixin, generic.DetailView): +class AccountDetailView(auth_mixins.PermissionRequiredMixin, generic.DetailView): """DetailView for Account.""" model = User @@ -235,12 +225,12 @@ class AccountDetailView( context = super(AccountDetailView, self).get_context_data(**kwargs) del context["user"] result = signals.extra_account_dashboard_widgets.send( - self.__class__, user=self.request.user, account=self.object) + self.__class__, user=self.request.user, account=self.object + ) context["templates"] = {"left": [], "right": []} for _receiver, widgets in result: for widget in widgets: - context["templates"][widget["column"]].append( - widget["template"]) + context["templates"][widget["column"]].append(widget["template"]) context.update(widget["context"]) if self.object.role in ["Resellers", "DomainAdmins"]: context["domains"] = Domain.objects.get_for_admin(self.object) diff --git a/modoboa/admin/views/import_.py b/modoboa/admin/views/import_.py index 575eb3be1..80744e10d 100644 --- a/modoboa/admin/views/import_.py +++ b/modoboa/admin/views/import_.py @@ -6,7 +6,9 @@ import io from reversion import revisions as reversion from django.contrib.auth.decorators import ( - login_required, permission_required, user_passes_test + login_required, + permission_required, + user_passes_test, ) from django.db import transaction from django.shortcuts import render @@ -35,14 +37,13 @@ def importdata(request, formclass=ImportDataForm): form = formclass(request.POST, request.FILES) if form.is_valid(): status, msg = lib.import_data( - request.user, request.FILES["sourcefile"], form.cleaned_data) + request.user, request.FILES["sourcefile"], form.cleaned_data + ) if status: - return render(request, "admin/import_done.html", { - "status": "ok", "msg": msg - }) - return render(request, "admin/import_done.html", { - "status": "ko", "msg": msg - }) + return render( + request, "admin/import_done.html", {"status": "ok", "msg": msg} + ) + return render(request, "admin/import_done.html", {"status": "ko", "msg": msg}) @login_required @@ -66,8 +67,7 @@ def import_domains(request): @login_required @user_passes_test( - lambda u: u.has_perm("core.add_user") or - u.has_perm("admin.add_alias") + lambda u: u.has_perm("core.add_user") or u.has_perm("admin.add_alias") ) def import_identities(request): if request.method == "POST": @@ -81,6 +81,6 @@ def import_identities(request): "formid": "importform", "enctype": "multipart/form-data", "target": "import_target", - "form": ImportIdentitiesForm() + "form": ImportIdentitiesForm(), } return render(request, "admin/import_identities_form.html", ctx) diff --git a/modoboa/admin/views/user.py b/modoboa/admin/views/user.py index f21de52c7..22039b368 100644 --- a/modoboa/admin/views/user.py +++ b/modoboa/admin/views/user.py @@ -23,37 +23,34 @@ def forward(request, tplname="admin/forward.html"): if form.is_valid(): recipients = form.cleaned_data["dest"] if not recipients: - Alias.objects.filter( - address=mb.full_address, internal=False).delete() + Alias.objects.filter(address=mb.full_address, internal=False).delete() # Make sure internal self-alias is enabled - Alias.objects.filter( - address=mb.full_address, internal=True - ).update(enabled=True) + Alias.objects.filter(address=mb.full_address, internal=True).update( + enabled=True + ) else: if al is None: al = Alias.objects.create( address=mb.full_address, domain=mb.domain, - enabled=mb.user.is_active + enabled=mb.user.is_active, ) al.post_create(request.user) if form.cleaned_data["keepcopies"]: # Make sure internal self-alias is enabled - Alias.objects.filter( - address=mb.full_address, internal=True - ).update(enabled=True) + Alias.objects.filter(address=mb.full_address, internal=True).update( + enabled=True + ) else: # Deactivate internal self-alias to avoid storing # local copies... - Alias.objects.filter( - address=mb.full_address, internal=True - ).update(enabled=False) + Alias.objects.filter(address=mb.full_address, internal=True).update( + enabled=False + ) al.set_recipients(recipients) return render_to_json_response(_("Forward updated")) - return render_to_json_response( - {"form_errors": form.errors}, status=400 - ) + return render_to_json_response({"form_errors": form.errors}, status=400) form = ForwardForm() if al is not None and al.recipients: @@ -62,6 +59,6 @@ def forward(request, tplname="admin/forward.html"): form.fields["keepcopies"].initial = True recipients.remove(mb.full_address) form.fields["dest"].initial = "\n".join(recipients) - return render_to_json_response({ - "content": render_to_string(tplname, {"form": form}, request) - }) + return render_to_json_response( + {"content": render_to_string(tplname, {"form": form}, request)} + ) diff --git a/modoboa/core/api/v1/serializers.py b/modoboa/core/api/v1/serializers.py index c3f115530..50af8729c 100644 --- a/modoboa/core/api/v1/serializers.py +++ b/modoboa/core/api/v1/serializers.py @@ -39,7 +39,7 @@ class CheckPasswordTFASerializer(serializers.Serializer): logger.warning( _("Failed TFA settings editing attempt from '%s' as user '%s'"), self.context["remote_addr"], - escape(user.username) + escape(user.username), ) raise serializers.ValidationError(_("Invalid password")) return value diff --git a/modoboa/core/api/v1/tests.py b/modoboa/core/api/v1/tests.py index 0719a3ba9..0d82ed941 100644 --- a/modoboa/core/api/v1/tests.py +++ b/modoboa/core/api/v1/tests.py @@ -4,7 +4,7 @@ from modoboa.lib.tests import ModoTestCase class OpenAPITestCase(ModoTestCase): - openapi_schema_url = reverse_lazy('schema-v1-legacy') + openapi_schema_url = reverse_lazy("schema-v1-legacy") def test_unauthorized(self): self.client.logout() @@ -17,7 +17,10 @@ class OpenAPITestCase(ModoTestCase): response = self.client.get(self.openapi_schema_url) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['info'], { - 'title': "Modoboa API", - 'version': "v1", - }) + self.assertEqual( + response.json()["info"], + { + "title": "Modoboa API", + "version": "v1", + }, + ) diff --git a/modoboa/core/api/v1/viewsets.py b/modoboa/core/api/v1/viewsets.py index fc99450d0..da8c8b634 100644 --- a/modoboa/core/api/v1/viewsets.py +++ b/modoboa/core/api/v1/viewsets.py @@ -22,31 +22,28 @@ class AccountViewSet(GetThrottleViewsetMixin, viewsets.ViewSet): Contains endpoints used to manipulate current user's account. """ - permission_classes = (permissions.IsAuthenticated, ) + permission_classes = (permissions.IsAuthenticated,) serializer_class = None @action(methods=["post"], detail=False, url_path="tfa/setup") def tfa_setup(self, request): """Initiate TFA setup.""" instance, created = TOTPDevice.objects.get_or_create( - user=request.user, - defaults={"name": "{} TOTP device".format(request.user)} + user=request.user, defaults={"name": "{} TOTP device".format(request.user)} ) return response.Response() - @extend_schema( - request=serializers.CheckTFASetupSerializer - ) + @extend_schema(request=serializers.CheckTFASetupSerializer) @action(methods=["post"], detail=False, url_path="tfa/setup/check") def tfa_setup_check(self, request): """Check TFA setup.""" serializer = serializers.CheckTFASetupSerializer( - data=request.data, context={"user": request.user}) + data=request.data, context={"user": request.user} + ) serializer.is_valid(raise_exception=True) # create static device for recovery purposes device = StaticDevice.objects.create( - user=request.user, - name="{} static device".format(request.user) + user=request.user, name="{} static device".format(request.user) ) for cpt in range(10): token = StaticToken.random_token() @@ -59,42 +56,34 @@ class AccountViewSet(GetThrottleViewsetMixin, viewsets.ViewSet): """Disable TFA.""" serializer = serializers.CheckPasswordTFASerializer( data=request.data, - context={ - "user": request.user, - "remote_addr": request.META["REMOTE_ADDR"] - } + context={"user": request.user, "remote_addr": request.META["REMOTE_ADDR"]}, ) serializer.is_valid(raise_exception=True) if not request.user.tfa_enabled: # We include it as "password" to display the error - return response.Response({"error": _("2FA is not enabled")}, - status=403) + return response.Response({"error": _("2FA is not enabled")}, status=403) request.user.totpdevice_set.all().delete() request.user.staticdevice_set.all().delete() request.user.tfa_enabled = False request.user.save() return response.Response() - @extend_schema(tags=['account']) + @extend_schema(tags=["account"]) @action(methods=["post"], detail=False, url_path="tfa/reset_codes") def tfa_reset_codes(self, request, *args, **kwargs): """Reset recovery codes.""" serializer = serializers.CheckPasswordTFASerializer( data=request.data, - context={ - "user": request.user, - "remote_addr": request.META["REMOTE_ADDR"] - } + context={"user": request.user, "remote_addr": request.META["REMOTE_ADDR"]}, ) serializer.is_valid(raise_exception=True) device = request.user.staticdevice_set.first() if device is None: - return response.Response({"error": _("2FA is not enabled")}, - status=403) + return response.Response({"error": _("2FA is not enabled")}, status=403) device.token_set.all().delete() for cpt in range(10): token = StaticToken.random_token() device.token_set.create(token=token) - return response.Response({ - "tokens": device.token_set.all().values_list("token", flat=True) - }) + return response.Response( + {"tokens": device.token_set.all().values_list("token", flat=True)} + ) diff --git a/modoboa/core/api/v2/serializers.py b/modoboa/core/api/v2/serializers.py index e72387123..d52d86acd 100644 --- a/modoboa/core/api/v2/serializers.py +++ b/modoboa/core/api/v2/serializers.py @@ -1,4 +1,5 @@ """Core API v2 serializers.""" + import django_otp import oath @@ -22,7 +23,11 @@ from rest_framework import serializers, status from rest_framework.exceptions import APIException from modoboa.core import ( - constants, models, sms_backends, app_settings, context_processors + constants, + models, + sms_backends, + app_settings, + context_processors, ) from modoboa.core.models import User from modoboa.lib import fields as lib_fields, cryptutils @@ -30,7 +35,7 @@ from modoboa.lib import fields as lib_fields, cryptutils class CustomValidationError(APIException): status_code = status.HTTP_400_BAD_REQUEST - default_detail = 'A server error occurred.' + default_detail = "A server error occurred." def __init__(self, detail, status_code): if status_code is not None: @@ -38,16 +43,17 @@ class CustomValidationError(APIException): if detail is not None: self.detail = detail else: - self.detail = {'detail': force_str(self.default_detail)} + self.detail = {"detail": force_str(self.default_detail)} class NoSMSAvailable(Exception): - """ Raised when no sms totp is available for password reset (to try email).""" + """Raised when no sms totp is available for password reset (to try email).""" + pass class PasswordRequirementsFailure(Exception): - """ Raised when django is not happy with password provided.""" + """Raised when django is not happy with password provided.""" def __init__(self, message_list, *args: object) -> None: super().__init__(*args) @@ -59,46 +65,41 @@ class CoreGlobalParametersSerializer(serializers.Serializer): # General settings authentication_type = serializers.ChoiceField( - choices=[("local", gettext_lazy("Local")), - ("ldap", "LDAP")], - default="local" + choices=[("local", gettext_lazy("Local")), ("ldap", "LDAP")], default="local" ) password_scheme = serializers.ChoiceField( - choices=[("sha512crypt", "sha512crypt")], - required=False + choices=[("sha512crypt", "sha512crypt")], required=False ) rounds_number = serializers.IntegerField(default=70000) update_scheme = serializers.BooleanField(default=True) default_password = serializers.CharField(default="ChangeMe1!") random_password_length = serializers.IntegerField(min_value=8, default=8) - update_password_url = serializers.URLField( - required=False, allow_blank=True) - password_recovery_msg = serializers.CharField( - required=False, allow_blank=True) + update_password_url = serializers.URLField(required=False, allow_blank=True) + password_recovery_msg = serializers.CharField(required=False, allow_blank=True) sms_password_recovery = serializers.BooleanField(default=False) sms_provider = serializers.ChoiceField( - choices=constants.SMS_BACKENDS, required=False, allow_null=True) + choices=constants.SMS_BACKENDS, required=False, allow_null=True + ) # LDAP settings ldap_server_address = serializers.CharField(default="localhost") ldap_server_port = serializers.IntegerField(default=389) ldap_enable_secondary_server = serializers.BooleanField(default=False) ldap_secondary_server_address = serializers.CharField(required=False) - ldap_secondary_server_port = serializers.IntegerField( - default=389, required=False) + ldap_secondary_server_port = serializers.IntegerField(default=389, required=False) ldap_secured = serializers.ChoiceField( - choices=constants.LDAP_SECURE_MODES, - default="none" + choices=constants.LDAP_SECURE_MODES, default="none" ) ldap_is_active_directory = serializers.BooleanField(default=False) ldap_admin_groups = serializers.CharField( - default="", required=False, allow_blank=True) + default="", required=False, allow_blank=True + ) ldap_group_type = serializers.ChoiceField( - default="posixgroup", - choices=constants.LDAP_GROUP_TYPES + default="posixgroup", choices=constants.LDAP_GROUP_TYPES ) ldap_groups_search_base = serializers.CharField( - default="", required=False, allow_blank=True) + default="", required=False, allow_blank=True + ) ldap_password_attribute = serializers.CharField(default="userPassword") # LDAP auth settings @@ -106,32 +107,31 @@ class CoreGlobalParametersSerializer(serializers.Serializer): choices=constants.LDAP_AUTH_METHODS, default="searchbind", ) - ldap_bind_dn = serializers.CharField( - default="", required=False, allow_blank=True) + ldap_bind_dn = serializers.CharField(default="", required=False, allow_blank=True) ldap_bind_password = serializers.CharField( - default="", required=False, allow_blank=True) + default="", required=False, allow_blank=True + ) ldap_search_base = serializers.CharField( - default="", required=False, allow_blank=True) + default="", required=False, allow_blank=True + ) ldap_search_filter = serializers.CharField( - default="(mail=%(user)s)", required=False, - allow_blank=True) + default="(mail=%(user)s)", required=False, allow_blank=True + ) ldap_user_dn_template = serializers.CharField( - default="", required=False, allow_blank=True) + default="", required=False, allow_blank=True + ) # LDAP sync settings ldap_sync_bind_dn = serializers.CharField(required=False, allow_blank=True) - ldap_sync_bind_password = serializers.CharField( - required=False, allow_blank=True) + ldap_sync_bind_password = serializers.CharField(required=False, allow_blank=True) ldap_enable_sync = serializers.BooleanField(default=False) ldap_sync_delete_remote_account = serializers.BooleanField(default=False) ldap_sync_account_dn_template = serializers.CharField( - required=False, allow_blank=True) - ldap_enable_import = serializers.BooleanField(default=False) - ldap_import_search_base = serializers.CharField( - required=False, allow_blank=True) - ldap_import_search_filter = serializers.CharField( - default="(cn=*)", required=False + required=False, allow_blank=True ) + ldap_enable_import = serializers.BooleanField(default=False) + ldap_import_search_base = serializers.CharField(required=False, allow_blank=True) + ldap_import_search_filter = serializers.CharField(default="(cn=*)", required=False) ldap_import_username_attr = serializers.CharField(default="cn") ldap_dovecot_sync = serializers.BooleanField(default=False) ldap_dovecot_conf_file = serializers.CharField( @@ -140,12 +140,12 @@ class CoreGlobalParametersSerializer(serializers.Serializer): # Dashboard settings rss_feed_url = serializers.URLField( - allow_blank=True, required=False, allow_null=True) + allow_blank=True, required=False, allow_null=True + ) hide_features_widget = serializers.BooleanField(default=False) # Notification settings - sender_address = lib_fields.DRFEmailFieldUTF8( - default="noreply@yourdomain.test") + sender_address = lib_fields.DRFEmailFieldUTF8(default="noreply@yourdomain.test") # API settings enable_api_communication = serializers.BooleanField(default=True) @@ -161,16 +161,18 @@ class CoreGlobalParametersSerializer(serializers.Serializer): log_maximum_age = serializers.IntegerField(default=365) items_per_page = serializers.IntegerField(default=30) default_top_redirection = serializers.ChoiceField( - default="user", choices=[("user", _("User profile"))], required=False) + default="user", choices=[("user", _("User profile"))], required=False + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) sms_backend_fields = sms_backends.get_all_backend_serializer_settings() for field, definition in sms_backend_fields.items(): - self.fields[field] = definition["type"]( - **definition["attrs"]) + self.fields[field] = definition["type"](**definition["attrs"]) # Choices serializer for default_top_redirection field - self.fields["default_top_redirection"].choices = app_settings.enabled_applications() + self.fields["default_top_redirection"].choices = ( + app_settings.enabled_applications() + ) # Populate choices of password_scheme self.fields["password_scheme"].choices = app_settings.get_password_scheme() @@ -286,12 +288,15 @@ class PasswordRecoveryEmailSerializer(serializers.Serializer): def validate(self, data): clean_email = data["email"] - self.context["user"] = (User.objects.filter( - email__iexact=clean_email, is_active=True) - .exclude(Q(secondary_email__isnull=True) | Q(secondary_email="")) + self.context["user"] = ( + User.objects.filter(email__iexact=clean_email, is_active=True).exclude( + Q(secondary_email__isnull=True) | Q(secondary_email="") + ) ).first() if self.context["user"] is None: - raise CustomValidationError({"type": "email", "reason": "No valid user found."}, 404) + raise CustomValidationError( + {"type": "email", "reason": "No valid user found."}, 404 + ) return data def save(self): @@ -301,30 +306,33 @@ class PasswordRecoveryEmailSerializer(serializers.Serializer): site_name = current_site.name domain = current_site.domain context = { - 'email': to_email, - 'domain': domain, - 'site_name': site_name, - 'uid': urlsafe_base64_encode(force_bytes(user.pk)), - 'user': user, - 'token': default_token_generator.make_token(user), - 'protocol': 'https', + "email": to_email, + "domain": domain, + "site_name": site_name, + "uid": urlsafe_base64_encode(force_bytes(user.pk)), + "user": user, + "token": default_token_generator.make_token(user), + "protocol": "https", } context.update(context_processors.new_admin_url()) subject = loader.render_to_string( - "registration/password_reset_subject.txt", context) + "registration/password_reset_subject.txt", context + ) # Email subject *must not* contain newlines - subject = ''.join(subject.splitlines()) + subject = "".join(subject.splitlines()) body = loader.render_to_string( - "registration/password_reset_email_v2.html", context) + "registration/password_reset_email_v2.html", context + ) send_mail(subject, body, None, [to_email], fail_silently=False) def send_sms_code(secret, backend, user): - """ Send a sms containing a TOTP code for password reset. """ + """Send a sms containing a TOTP code for password reset.""" code = oath.totp(secret) text = _( - "Please use the following code to recover your Modoboa password: {}" - .format(code) + "Please use the following code to recover your Modoboa password: {}".format( + code + ) ) if not backend.send(text, [str(user.phone_number)]): raise NoSMSAvailable() @@ -340,14 +348,14 @@ class PasswordRecoverySmsSerializer(serializers.Serializer): user = User.objects.filter(email__iexact=clean_email, is_active=True).first() if user is None: - raise CustomValidationError({ - "type": "sms", "reason": "No valid user found." - }, 404) + raise CustomValidationError( + {"type": "sms", "reason": "No valid user found."}, 404 + ) self.context["user"] = ( - User.objects.filter( - email__iexact=clean_email, is_active=True) - .exclude(Q(phone_number__isnull=True) | Q(phone_number="")) + User.objects.filter(email__iexact=clean_email, is_active=True).exclude( + Q(phone_number__isnull=True) | Q(phone_number="") + ) ).first() if self.context["user"] is None: raise NoSMSAvailable() @@ -358,8 +366,7 @@ class PasswordRecoverySmsSerializer(serializers.Serializer): user = self.context["user"] if not user: raise NoSMSAvailable() - backend = sms_backends.get_active_backend( - request.localconfig.parameters) + backend = sms_backends.get_active_backend(request.localconfig.parameters) secret = cryptutils.random_hex_key(20) send_sms_code(secret, backend, user) request.session["totp_secret"] = secret @@ -376,13 +383,15 @@ class PasswordRecoverySmsConfirmSerializer(serializers.Serializer): self.context["totp_secret"] = self.context["request"].session["totp_secret"] except KeyError: raise CustomValidationError( - {"reason": "totp secret not set in session"}, 403) + {"reason": "totp secret not set in session"}, 403 + ) if not oath.accept_totp(self.context["totp_secret"], data["sms_totp"])[0]: raise CustomValidationError({"reason": "Wrong totp"}, 400) # Attempt to get user, will raise an error if pk is not valid self.context["user"] = User.objects.filter( - pk=self.context["request"].session["user_pk"]).first() + pk=self.context["request"].session["user_pk"] + ).first() if self.context["user"] is None: raise CustomValidationError({"reason": "Invalid user"}, 400) @@ -404,11 +413,13 @@ class PasswordRecoverySmsResendSerializer(serializers.Serializer): self.context["totp_secret"] = self.context["request"].session["totp_secret"] except KeyError: raise CustomValidationError( - {"reason": "totp secret not set in session"}, status.HTTP_403_FORBIDDEN) + {"reason": "totp secret not set in session"}, status.HTTP_403_FORBIDDEN + ) # Attempt to get user, will raise an error if pk is not valid self.context["user"] = User.objects.filter( - pk=self.context["request"].session["user_pk"]).first() + pk=self.context["request"].session["user_pk"] + ).first() if self.context["user"] is None: raise CustomValidationError({"reason": "Invalid user"}, 400) @@ -416,7 +427,8 @@ class PasswordRecoverySmsResendSerializer(serializers.Serializer): def save(self): backend = sms_backends.get_active_backend( - self.context["request"].localconfig.parameters) + self.context["request"].localconfig.parameters + ) send_sms_code(self.context["totp_secret"], backend, self.context["user"]) return True @@ -442,9 +454,11 @@ class PasswordRecoveryConfirmSerializer(serializers.Serializer): def validate(self, data): # Validate password - if data["new_password1"] == "" or data["new_password1"] != data["new_password2"]: - raise serializers.ValidationError( - "Password is empty or does not match") + if ( + data["new_password1"] == "" + or data["new_password1"] != data["new_password2"] + ): + raise serializers.ValidationError("Password is empty or does not match") user = self.get_user(data["id"]) diff --git a/modoboa/core/api/v2/tests.py b/modoboa/core/api/v2/tests.py index b2fa8b7a7..75ea99c3b 100644 --- a/modoboa/core/api/v2/tests.py +++ b/modoboa/core/api/v2/tests.py @@ -11,8 +11,11 @@ from django.urls import reverse from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode -from modoboa.admin import (factories, models as admin_models, - constants as admin_constants) +from modoboa.admin import ( + factories, + models as admin_models, + constants as admin_constants, +) from modoboa.core import models, constants from modoboa.core.tests import utils from modoboa.lib.tests import ModoAPITestCase @@ -72,7 +75,7 @@ CORE_SETTINGS = { "top_notifications_check_interval": 30, "log_maximum_age": 365, "items_per_page": 30, - "default_top_redirection": "user" + "default_top_redirection": "user", } @@ -82,8 +85,9 @@ class ParametersAPITestCase(ModoAPITestCase): super().setUp() cache.delete("password_scheme_choice") - @override_settings(DOVEADM_LOOKUP_PATH=[DOVEADM_TEST_PATH], - DOVECOT_USER=DOVECOT_USER) + @override_settings( + DOVEADM_LOOKUP_PATH=[DOVEADM_TEST_PATH], DOVECOT_USER=DOVECOT_USER + ) def test_update(self): url = reverse("v2:parameter-detail", args=["core"]) data = copy.copy(CORE_SETTINGS) @@ -91,9 +95,7 @@ class ParametersAPITestCase(ModoAPITestCase): self.assertEqual(resp.status_code, 200) self.assertIsNotNone(cache.get("password_scheme_choice")) - self.assertIn(('plain', 'plain (weak)'), - cache.get("password_scheme_choice") - ) + self.assertIn(("plain", "plain (weak)"), cache.get("password_scheme_choice")) # Modify SMS related settings data["sms_password_recovery"] = True resp = self.client.put(url, data, format="json") @@ -102,20 +104,19 @@ class ParametersAPITestCase(ModoAPITestCase): data["sms_provider"] = "test" resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 400) - data.update({ - "sms_provider": "ovh", - "sms_ovh_application_key": "key", - "sms_ovh_application_secret": "secret", - "sms_ovh_consumer_key": "consumer" - }) + data.update( + { + "sms_provider": "ovh", + "sms_ovh_application_key": "key", + "sms_ovh_application_secret": "secret", + "sms_ovh_consumer_key": "consumer", + } + ) resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) # Modify some LDAP settings - data.update({ - "authentication_type": "ldap", - "ldap_auth_method": "searchbind" - }) + data.update({"authentication_type": "ldap", "ldap_auth_method": "searchbind"}) resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 400) self.assertIn("ldap_search_base", resp.json()) @@ -125,16 +126,18 @@ class ParametersAPITestCase(ModoAPITestCase): self.assertEqual(resp.status_code, 400) self.assertIn("ldap_user_dn_template", resp.json()) - data.update({ - "ldap_user_dn_template": "%(user)s", - "ldap_sync_account_dn_template": "%(user)s", - "ldap_search_filter": "mail=%(user)s" - }) + data.update( + { + "ldap_user_dn_template": "%(user)s", + "ldap_sync_account_dn_template": "%(user)s", + "ldap_search_filter": "mail=%(user)s", + } + ) resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) def test_doveadm_alarm(self): - """ Test that an alarm is opened, closed or reopened + """Test that an alarm is opened, closed or reopened depending on the result of the doveadm command for the password scheme """ # Test case where doveadm command fails @@ -147,22 +150,24 @@ class ParametersAPITestCase(ModoAPITestCase): self.assertIsNone(cache.get("password_scheme_choice")) doveadm_alarm = admin_models.Alarm.objects.filter( - internal_name=constants.DOVEADM_PASS_SCHEME_ALARM) + internal_name=constants.DOVEADM_PASS_SCHEME_ALARM + ) self.assertEqual(doveadm_alarm.count(), 1) cache.delete("job_cache_available_password_hasher") - with self.settings(DOVEADM_LOOKUP_PATH=[DOVEADM_TEST_PATH], - DOVECOT_USER=DOVECOT_USER): + with self.settings( + DOVEADM_LOOKUP_PATH=[DOVEADM_TEST_PATH], DOVECOT_USER=DOVECOT_USER + ): # The command should work and close the alarm resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 200) self.assertIsNotNone(cache.get("password_scheme_choice")) doveadm_alarm = admin_models.Alarm.objects.filter( - internal_name=constants.DOVEADM_PASS_SCHEME_ALARM) + internal_name=constants.DOVEADM_PASS_SCHEME_ALARM + ) self.assertEqual(doveadm_alarm.count(), 1) - self.assertEqual(doveadm_alarm.first().status, - admin_constants.ALARM_CLOSED) + self.assertEqual(doveadm_alarm.first().status, admin_constants.ALARM_CLOSED) cache.delete("job_cache_available_password_hasher") # And lastly check that the alarm is reopened if the issue starts again @@ -172,10 +177,10 @@ class ParametersAPITestCase(ModoAPITestCase): self.assertEqual(resp.status_code, 200) doveadm_alarm = admin_models.Alarm.objects.filter( - internal_name=constants.DOVEADM_PASS_SCHEME_ALARM) + internal_name=constants.DOVEADM_PASS_SCHEME_ALARM + ) self.assertEqual(doveadm_alarm.count(), 1) - self.assertEqual(doveadm_alarm.first().status, - admin_constants.ALARM_OPENED) + self.assertEqual(doveadm_alarm.first().status, admin_constants.ALARM_OPENED) class AccountViewSetTestCase(ModoAPITestCase): @@ -227,11 +232,9 @@ class AccountViewSetTestCase(ModoAPITestCase): self.assertEqual(resp.status_code, 404) @mock.patch("django_otp.plugins.otp_totp.models.TOTPDevice.verify_token") - def test_tfa_setup_modify(self, - verify_mock, - password_ko="Toto1234", - password_ok="password" - ): + def test_tfa_setup_modify( + self, verify_mock, password_ko="Toto1234", password_ok="password" + ): user = models.User.objects.get(username="admin") user.totpdevice_set.create(name="Device") @@ -277,10 +280,7 @@ class AccountViewSetTestCase(ModoAPITestCase): def test_api_token(self): # 1. Obtain a JWT token so we can safely play with basic token url = reverse("v2:token_obtain_pair") - data = { - "username": "admin", - "password": "password" - } + data = {"username": "admin", "password": "password"} resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 200) self.client.credentials( @@ -300,19 +300,16 @@ class AccountViewSetTestCase(ModoAPITestCase): """Simulate a failed login attempt and check that it is logged.""" url = reverse("v2:token_obtain_pair") - data = { - "username": "clearly_non_existent_user", - "password": "password" - } + data = {"username": "clearly_non_existent_user", "password": "password"} - with self.assertLogs(logger='modoboa.auth', level='WARNING') as log: + with self.assertLogs(logger="modoboa.auth", level="WARNING") as log: resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 401) self.assertIn( "WARNING:modoboa.auth:Failed connection attempt from '127.0.0.1'" " as user 'clearly_non_existent_user'", - log.output + log.output, ) @@ -328,7 +325,8 @@ class PasswordResetTestCase(AccountViewSetTestCase): super().setUpTestData() factories.populate_database() cls.da_token = Token.objects.create( - user=models.User.objects.get(username="admin@test.com")) + user=models.User.objects.get(username="admin@test.com") + ) @mock.patch("ovh.Client.get") @mock.patch("ovh.Client.post") @@ -361,13 +359,16 @@ class PasswordResetTestCase(AccountViewSetTestCase): self.assertEqual(response.json()["type"], "email") # SMS reset test - self.set_global_parameters({ - "sms_password_recovery": True, - "sms_provider": "ovh", - "sms_ovh_application_key": "key", - "sms_ovh_application_secret": "secret", - "sms_ovh_consumer_key": "consumer" - }, app="core") + self.set_global_parameters( + { + "sms_password_recovery": True, + "sms_provider": "ovh", + "sms_ovh_application_key": "key", + "sms_ovh_application_secret": "secret", + "sms_ovh_consumer_key": "consumer", + }, + app="core", + ) # Phone number account.phone_number = "+33612345678" @@ -395,13 +396,15 @@ class PasswordResetTestCase(AccountViewSetTestCase): # Send sms client_get.return_value = ["service"] client_post.return_value = {"totalCreditsRemoved": 1} - self.set_global_parameters({ - "sms_password_recovery": True, - "sms_provider": "ovh", - "sms_ovh_application_key": "key", - "sms_ovh_application_secret": "secret", - "sms_ovh_consumer_key": "consumer" - }) + self.set_global_parameters( + { + "sms_password_recovery": True, + "sms_provider": "ovh", + "sms_ovh_application_key": "key", + "sms_ovh_application_secret": "secret", + "sms_ovh_consumer_key": "consumer", + } + ) self.client.logout() self.create_session() data = {"email": account.email} @@ -447,13 +450,15 @@ class PasswordResetTestCase(AccountViewSetTestCase): client_get.return_value = ["service"] client_post.return_value = {"totalCreditsRemoved": 1} - self.set_global_parameters({ - "sms_password_recovery": True, - "sms_provider": "ovh", - "sms_ovh_application_key": "key", - "sms_ovh_application_secret": "secret", - "sms_ovh_consumer_key": "consumer" - }) + self.set_global_parameters( + { + "sms_password_recovery": True, + "sms_provider": "ovh", + "sms_ovh_application_key": "key", + "sms_ovh_application_secret": "secret", + "sms_ovh_consumer_key": "consumer", + } + ) # Prepare account account = models.User.objects.get(username="user@test.com") @@ -477,30 +482,54 @@ class PasswordResetTestCase(AccountViewSetTestCase): token_ok = response.json()["token"] # Password differs - data = {"new_password1": "123456", "new_password2": "123457", "token": token_ok, "id": id_ok} + data = { + "new_password1": "123456", + "new_password2": "123457", + "token": token_ok, + "id": id_ok, + } response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 400) # Wrong ID id_ko = urlsafe_base64_encode(force_bytes(-1)) - data = {"new_password1": "123456", "new_password2": "123456", "token": token_ok, "id": id_ko} + data = { + "new_password1": "123456", + "new_password2": "123456", + "token": token_ok, + "id": id_ko, + } response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 404) # Wrong Token token_ko = "123456" - data = {"new_password1": "123456", "new_password2": "123456", "token": token_ko, "id": id_ok} + data = { + "new_password1": "123456", + "new_password2": "123456", + "token": token_ko, + "id": id_ok, + } response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 403) # Failed password requirements - data = {"new_password1": "123456", "new_password2": "123456", "token": token_ok, "id": id_ok} + data = { + "new_password1": "123456", + "new_password2": "123456", + "token": token_ok, + "id": id_ok, + } response = self.client.post(url, data, format="json") self.assertEqual(response.json()["type"], "password_requirement") # All good - data = {"new_password1": "MyHardenedPass1!", "new_password2": "MyHardenedPass1!", "token": token_ok, - "id": id_ok} + data = { + "new_password1": "MyHardenedPass1!", + "new_password2": "MyHardenedPass1!", + "token": token_ok, + "id": id_ok, + } response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 200) # TODO: See why user doesn't update it's password --> self.test_me_password(password_ok="MyHardenedPass1!") @@ -512,15 +541,13 @@ class AuthenticationTestCase(ModoAPITestCase): def test_2fa(self, match_mock): url = reverse("v2:token_obtain_pair") me_url = reverse("v2:account-me") - data = { - "username": "admin", - "password": "password" - } + data = {"username": "admin", "password": "password"} resp = self.client.post(url, data, format="json") self.assertEqual(resp.status_code, 200) self.client.credentials( - HTTP_AUTHORIZATION="Bearer {}".format(resp.json()["access"])) + HTTP_AUTHORIZATION="Bearer {}".format(resp.json()["access"]) + ) resp = self.client.get(me_url) self.assertEqual(resp.status_code, 200) @@ -540,7 +567,8 @@ class AuthenticationTestCase(ModoAPITestCase): self.assertEqual(resp.status_code, 200) self.client.credentials( - HTTP_AUTHORIZATION="Bearer {}".format(resp.json()["access"])) + HTTP_AUTHORIZATION="Bearer {}".format(resp.json()["access"]) + ) resp = self.client.get(me_url) self.assertEqual(resp.status_code, 200) diff --git a/modoboa/core/api/v2/urls.py b/modoboa/core/api/v2/urls.py index da5784b20..9fc5ef6b6 100644 --- a/modoboa/core/api/v2/urls.py +++ b/modoboa/core/api/v2/urls.py @@ -18,16 +18,22 @@ router.register(r"logs/audit-trail", viewsets.LogViewSet) urlpatterns = router.urls urlpatterns += [ - path('token/', views.TokenObtainPairView.as_view(), - name="token_obtain_pair"), - path('token/refresh/', TokenRefreshView.as_view(), - name="token_refresh"), - path('password_reset/', views.DefaultPasswordResetView.as_view(), - name="password_reset"), - path('reset_confirm/', views.PasswordResetConfirmView.as_view(), - name="password_reset_confirm_v2"), - path('sms_totp/', views.PasswordResetSmsTOTP.as_view(), - name="sms_totp"), - path('admin/components/', views.ComponentsInformationAPIView.as_view(), - name="components_information"), + path("token/", views.TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), + path( + "password_reset/", + views.DefaultPasswordResetView.as_view(), + name="password_reset", + ), + path( + "reset_confirm/", + views.PasswordResetConfirmView.as_view(), + name="password_reset_confirm_v2", + ), + path("sms_totp/", views.PasswordResetSmsTOTP.as_view(), name="sms_totp"), + path( + "admin/components/", + views.ComponentsInformationAPIView.as_view(), + name="components_information", + ), ] diff --git a/modoboa/core/api/v2/views.py b/modoboa/core/api/v2/views.py index 4f69225dc..3e3f32973 100644 --- a/modoboa/core/api/v2/views.py +++ b/modoboa/core/api/v2/views.py @@ -19,8 +19,11 @@ from modoboa.core.password_hashers import get_password_hasher from modoboa.core.utils import check_for_updates from modoboa.lib.permissions import IsSuperUser from modoboa.lib.throttle import ( - UserLesserDdosUser, LoginThrottle, PasswordResetApplyThrottle, - PasswordResetRequestThrottle, PasswordResetTotpThrottle + UserLesserDdosUser, + LoginThrottle, + PasswordResetApplyThrottle, + PasswordResetRequestThrottle, + PasswordResetTotpThrottle, ) from modoboa.parameters import tools as param_tools @@ -53,7 +56,7 @@ class TokenObtainPairView(jwt_views.TokenObtainPairView): logger.warning( _("Failed connection attempt from '%s' as user '%s'"), request.META["REMOTE_ADDR"], - escape(serializer.initial_data["username"]) + escape(serializer.initial_data["username"]), ) raise InvalidToken(e.args[0]) @@ -63,19 +66,16 @@ class TokenObtainPairView(jwt_views.TokenObtainPairView): # Reset login throttle delete_cache_key(LoginThrottle, self.get_throttles(), request) - logger.info( - _("User '%s' successfully logged in"), user.username - ) + logger.info(_("User '%s' successfully logged in"), user.username) if user and user.is_active: - condition = ( - user.is_local and - param_tools.get_global_parameter( - "update_scheme", raise_exception=False) + condition = user.is_local and param_tools.get_global_parameter( + "update_scheme", raise_exception=False ) if condition: # check if password scheme is correct scheme = param_tools.get_global_parameter( - "password_scheme", raise_exception=False) + "password_scheme", raise_exception=False + ) # use SHA512CRYPT as default fallback if scheme is None: pwhash = get_password_hasher("sha512crypt")() @@ -84,21 +84,19 @@ class TokenObtainPairView(jwt_views.TokenObtainPairView): if not user.password.startswith(pwhash.scheme): logger.info( _("Password scheme mismatch. Updating %s password"), - user.username + user.username, ) user.set_password(request.data["password"]) user.save() if pwhash.needs_rehash(user.password): logger.info( - _("Password hash parameter missmatch. " - "Updating %s password"), - user.username + _("Password hash parameter missmatch. " "Updating %s password"), + user.username, ) user.set_password(serializer.data["password"]) user.save() - return response.Response( - serializer.validated_data, status=status.HTTP_200_OK) + return response.Response(serializer.validated_data, status=status.HTTP_200_OK) class EmailPasswordResetView(APIView): @@ -110,15 +108,19 @@ class EmailPasswordResetView(APIView): def post(self, request, *args, **kwargs): serializer = serializers.PasswordRecoveryEmailSerializer( - data=request.data, context={'request': request}) + data=request.data, context={"request": request} + ) serializer.is_valid(raise_exception=True) try: serializer.save() except SMTPException: - return response.Response({ - "type": "email", - "reason": "Error while sending the email. Please contact an administrator." - }, 503) + return response.Response( + { + "type": "email", + "reason": "Error while sending the email. Please contact an administrator.", + }, + 503, + ) # Email response return response.Response({"type": "email"}, 200) @@ -133,7 +135,8 @@ class DefaultPasswordResetView(EmailPasswordResetView): def post(self, request, *args, **kwargs): """Recover password.""" serializer = serializers.PasswordRecoverySmsSerializer( - data=request.data, context={'request': request}) + data=request.data, context={"request": request} + ) try: serializer.is_valid(raise_exception=True) except serializers.NoSMSAvailable: @@ -144,7 +147,7 @@ class DefaultPasswordResetView(EmailPasswordResetView): class PasswordResetSmsTOTP(APIView): - """ Check SMS Totp code. """ + """Check SMS Totp code.""" throttle_classes = [PasswordResetTotpThrottle] @@ -154,7 +157,7 @@ class PasswordResetSmsTOTP(APIView): klass = serializers.PasswordRecoverySmsConfirmSerializer elif request.data["type"] == "resend": klass = serializers.PasswordRecoverySmsResendSerializer - serializer = klass(data=request.data, context={'request': request}) + serializer = klass(data=request.data, context={"request": request}) except (MultiValueDictKeyError, KeyError): return response.Response({"reason": "No type provided."}, 400) serializer.is_valid(raise_exception=True) @@ -162,23 +165,24 @@ class PasswordResetSmsTOTP(APIView): payload = {"type": "resend"} if request.data["type"] == "confirm": serializer_response = serializer.context["response"] - payload.update({ - "token": serializer_response[0], - "id": serializer_response[1], - "type": "confirm" - }) + payload.update( + { + "token": serializer_response[0], + "id": serializer_response[1], + "type": "confirm", + } + ) delete_cache_key(PasswordResetTotpThrottle, self.get_throttles(), request) return response.Response(payload, 200) class PasswordResetConfirmView(APIView): - """ Get and set new user password. """ + """Get and set new user password.""" throttle_classes = [PasswordResetApplyThrottle] def post(self, request, *args, **kwargs): - serializer = serializers.PasswordRecoveryConfirmSerializer( - data=request.data) + serializer = serializers.PasswordRecoveryConfirmSerializer(data=request.data) try: serializer.is_valid(raise_exception=True) except serializers.PasswordRequirementsFailure as e: @@ -202,7 +206,5 @@ class ComponentsInformationAPIView(APIView): @extend_schema(responses=serializers.ModoboaComponentSerializer(many=True)) def get(self, request, *args, **kwargs): status, extensions = check_for_updates() - serializer = serializers.ModoboaComponentSerializer( - extensions, many=True - ) + serializer = serializers.ModoboaComponentSerializer(extensions, many=True) return response.Response(serializer.data) diff --git a/modoboa/core/api/v2/viewsets.py b/modoboa/core/api/v2/viewsets.py index 7cba4fdf6..2dd4dd17c 100644 --- a/modoboa/core/api/v2/viewsets.py +++ b/modoboa/core/api/v2/viewsets.py @@ -30,32 +30,30 @@ class AccountViewSet(core_v1_viewsets.AccountViewSet): return response.Response(serializer.data) @action( - methods=['post'], + methods=["post"], detail=False, - url_path='me/password', - serializer_class=serializers.CheckPasswordSerializer + url_path="me/password", + serializer_class=serializers.CheckPasswordSerializer, ) def check_me_password(self, request): """Check current user password.""" serializer = serializers.CheckPasswordSerializer( - data=request.data, context={"user": request.user}) + data=request.data, context={"user": request.user} + ) serializer.is_valid(raise_exception=True) return response.Response() @extend_schema( description="Get current API token", responses={200: serializers.UserAPITokenSerializer}, - methods=["GET"] + methods=["GET"], ) @extend_schema( description="Generate new API token", responses={201: serializers.UserAPITokenSerializer}, - methods=["POST"] - ) - @extend_schema( - description="Delete current API token", - methods=["DELETE"] + methods=["POST"], ) + @extend_schema(description="Delete current API token", methods=["DELETE"]) @action( methods=["get", "post", "delete"], detail=False, @@ -64,9 +62,7 @@ class AccountViewSet(core_v1_viewsets.AccountViewSet): def manage_api_token(self, request): """Manage API token.""" if not request.user.is_superuser: - raise PermissionDenied( - "Only super administrators can have API tokens" - ) + raise PermissionDenied("Only super administrators can have API tokens") if request.method == "DELETE": Token.objects.filter(user=request.user).delete() return response.Response(status=204) @@ -87,15 +83,16 @@ class AccountViewSet(core_v1_viewsets.AccountViewSet): def tfa_verify_code(self, request): """Verify given code validity.""" serializer = serializers.VerifyTFACodeSerializer( - data=request.data, context={"user": request.user}) + data=request.data, context={"user": request.user} + ) serializer.is_valid(raise_exception=True) refresh = RefreshToken.for_user(request.user) - refresh[constants.TFA_DEVICE_TOKEN_KEY] = ( - serializer.validated_data["code"].persistent_id) - return response.Response({ - "refresh": str(refresh), - "access": str(refresh.access_token) - }) + refresh[constants.TFA_DEVICE_TOKEN_KEY] = serializer.validated_data[ + "code" + ].persistent_id + return response.Response( + {"refresh": str(refresh), "access": str(refresh.access_token)} + ) @action(methods=["get"], detail=False, url_path="tfa/setup/key") def tfa_setup_get_key(self, request): @@ -105,22 +102,22 @@ class AccountViewSet(core_v1_viewsets.AccountViewSet): device = request.user.totpdevice_set.first() if not device: return response.Response(status=404) - return response.Response({"key": device.key, "url":device.config_url}, - content_type="application/json") + return response.Response( + {"key": device.key, "url": device.config_url}, + content_type="application/json", + ) - @extend_schema( - request=core_v1_serializers.CheckTFASetupSerializer - ) + @extend_schema(request=core_v1_serializers.CheckTFASetupSerializer) @action(methods=["post"], detail=False, url_path="tfa/setup/check") def tfa_setup_check(self, request): """Check TFA setup.""" serializer = core_v1_serializers.CheckTFASetupSerializer( - data=request.data, context={"user": request.user}) + data=request.data, context={"user": request.user} + ) serializer.is_valid(raise_exception=True) # create static device for recovery purposes device = StaticDevice.objects.create( - user=request.user, - name="{} static device".format(request.user) + user=request.user, name="{} static device".format(request.user) ) tokens = [] for cpt in range(10): @@ -134,11 +131,13 @@ class AccountViewSet(core_v1_viewsets.AccountViewSet): device = request.user.totpdevice_set.first() refresh = RefreshToken.for_user(request.user) refresh[constants.TFA_DEVICE_TOKEN_KEY] = device.persistent_id - return response.Response({ - "tokens": tokens, - "refresh": str(refresh), - "access": str(refresh.access_token) - }) + return response.Response( + { + "tokens": tokens, + "refresh": str(refresh), + "access": str(refresh.access_token), + } + ) class LogViewSet(GetThrottleViewsetMixin, viewsets.ReadOnlyModelViewSet): @@ -160,9 +159,7 @@ class LogViewSet(GetThrottleViewsetMixin, viewsets.ReadOnlyModelViewSet): class LanguageViewSet(GetThrottleViewsetMixin, viewsets.ViewSet): """Language viewset.""" - permission_classes = ( - permissions.IsAuthenticated, - ) + permission_classes = (permissions.IsAuthenticated,) def list(self, request, *args, **kwargs): languages = [ diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 17a41bd68..5fdb42c7f 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -10,9 +10,7 @@ from django.utils.translation import gettext as _, gettext_lazy from modoboa.core.password_hashers.utils import cache_available_password_hasher from modoboa.lib import fields as lib_fields -from modoboa.lib.form_utils import ( - HorizontalRadioSelect, SeparatorField, YesNoField -) +from modoboa.lib.form_utils import HorizontalRadioSelect, SeparatorField, YesNoField from modoboa.parameters import forms as param_forms, tools as param_tools from . import constants @@ -47,11 +45,10 @@ class GeneralParametersForm(param_forms.AdminParametersForm): authentication_type = forms.ChoiceField( label=gettext_lazy("Authentication type"), - choices=[("local", gettext_lazy("Local")), - ("ldap", "LDAP")], + choices=[("local", gettext_lazy("Local")), ("ldap", "LDAP")], initial="local", help_text=gettext_lazy("The backend used for authentication"), - widget=HorizontalRadioSelect() + widget=HorizontalRadioSelect(), ) password_scheme = forms.ChoiceField( @@ -75,22 +72,20 @@ class GeneralParametersForm(param_forms.AdminParametersForm): initial=True, help_text=gettext_lazy( "Update user password at login to use the default password scheme" - ) + ), ) default_password = forms.CharField( label=gettext_lazy("Default password"), initial="ChangeMe1!", - help_text=gettext_lazy( - "Default password for automatically created accounts.") + help_text=gettext_lazy("Default password for automatically created accounts."), ) random_password_length = forms.IntegerField( label=gettext_lazy("Random password length"), min_value=8, initial=8, - help_text=gettext_lazy( - "Length of randomly generated passwords.") + help_text=gettext_lazy("Length of randomly generated passwords."), ) update_password_url = forms.URLField( @@ -102,7 +97,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): " to update their password. It applies only to non local" " users, ie. those automatically created after a successful" " external authentication (LDAP, SMTP)." - ) + ), ) password_recovery_msg = forms.CharField( @@ -111,27 +106,23 @@ class GeneralParametersForm(param_forms.AdminParametersForm): required=False, widget=forms.widgets.Textarea(), help_text=gettext_lazy( - "A temporary message that will be displayed on the " - "reset password page." - ) + "A temporary message that will be displayed on the " "reset password page." + ), ) sms_password_recovery = YesNoField( label=gettext_lazy("Enable password recovery by SMS"), initial=False, help_text=gettext_lazy( - "Enable password recovery by SMS for users who filled " - "a phone number." - ) + "Enable password recovery by SMS for users who filled " "a phone number." + ), ) sms_provider = forms.ChoiceField( label=gettext_lazy("SMS provider"), choices=constants.SMS_BACKENDS, - help_text=gettext_lazy( - "Choose a provider to send password recovery SMS" - ), - required=False + help_text=gettext_lazy("Choose a provider to send password recovery SMS"), + required=False, ) # LDAP specific settings @@ -140,8 +131,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): ldap_server_address = forms.CharField( label=gettext_lazy("Server address"), initial="localhost", - help_text=gettext_lazy( - "The IP address or the DNS name of the LDAP server"), + help_text=gettext_lazy("The IP address or the DNS name of the LDAP server"), ) ldap_server_port = forms.IntegerField( @@ -156,21 +146,21 @@ class GeneralParametersForm(param_forms.AdminParametersForm): help_text=gettext_lazy( "Enable a secondary LDAP server which will be used " "if the primary one fails" - ) + ), ) ldap_secondary_server_address = forms.CharField( label=gettext_lazy("Secondary server address"), initial="localhost", help_text=gettext_lazy( - "The IP address or the DNS name of the seondary LDAP server"), + "The IP address or the DNS name of the seondary LDAP server" + ), ) ldap_secondary_server_port = forms.IntegerField( label=gettext_lazy("Secondary server port"), initial=389, - help_text=gettext_lazy( - "The TCP port number used by the LDAP secondary server"), + help_text=gettext_lazy("The TCP port number used by the LDAP secondary server"), ) ldap_secured = forms.ChoiceField( @@ -178,14 +168,14 @@ class GeneralParametersForm(param_forms.AdminParametersForm): choices=constants.LDAP_SECURE_MODES, initial="none", help_text=gettext_lazy( - "Use an SSL/STARTTLS connection to access the LDAP server") + "Use an SSL/STARTTLS connection to access the LDAP server" + ), ) ldap_is_active_directory = YesNoField( label=gettext_lazy("Active Directory"), initial=False, - help_text=gettext_lazy( - "Tell if the LDAP server is an Active Directory one") + help_text=gettext_lazy("Tell if the LDAP server is an Active Directory one"), ) ldap_admin_groups = forms.CharField( @@ -195,16 +185,14 @@ class GeneralParametersForm(param_forms.AdminParametersForm): "Members of those LDAP Posix groups will be created as domain " "administrators. Use ';' characters to separate groups." ), - required=False + required=False, ) ldap_group_type = forms.ChoiceField( label=gettext_lazy("Group type"), initial="posixgroup", choices=constants.LDAP_GROUP_TYPES, - help_text=gettext_lazy( - "The LDAP group type to use with your directory." - ) + help_text=gettext_lazy("The LDAP group type to use with your directory."), ) ldap_groups_search_base = forms.CharField( @@ -213,7 +201,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): help_text=gettext_lazy( "The distinguished name of the search base used to find groups" ), - required=False + required=False, ) ldap_password_attribute = forms.CharField( @@ -223,13 +211,14 @@ class GeneralParametersForm(param_forms.AdminParametersForm): ) # LDAP authentication settings - ldap_auth_sep = SeparatorField( - label=gettext_lazy("LDAP authentication settings")) + ldap_auth_sep = SeparatorField(label=gettext_lazy("LDAP authentication settings")) ldap_auth_method = forms.ChoiceField( label=gettext_lazy("Authentication method"), - choices=[("searchbind", gettext_lazy("Search and bind")), - ("directbind", gettext_lazy("Direct bind"))], + choices=[ + ("searchbind", gettext_lazy("Search and bind")), + ("directbind", gettext_lazy("Direct bind")), + ], initial="searchbind", help_text=gettext_lazy("Choose the authentication method to use"), ) @@ -248,11 +237,10 @@ class GeneralParametersForm(param_forms.AdminParametersForm): label=gettext_lazy("Bind password"), initial="", help_text=gettext_lazy( - "The password to use when binding to the LDAP server " - "(with 'Bind DN')" + "The password to use when binding to the LDAP server " "(with 'Bind DN')" ), widget=forms.PasswordInput(render_value=True), - required=False + required=False, ) ldap_search_base = forms.CharField( @@ -285,8 +273,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): ) # LDAP sync. settings - ldap_sync_sep = SeparatorField( - label=gettext_lazy("LDAP synchronization settings")) + ldap_sync_sep = SeparatorField(label=gettext_lazy("LDAP synchronization settings")) ldap_sync_bind_dn = forms.CharField( label=gettext_lazy("Bind DN"), @@ -302,11 +289,10 @@ class GeneralParametersForm(param_forms.AdminParametersForm): label=gettext_lazy("Bind password"), initial="", help_text=gettext_lazy( - "The password to use when binding to the LDAP server " - "(with 'Bind DN')" + "The password to use when binding to the LDAP server " "(with 'Bind DN')" ), widget=forms.PasswordInput(render_value=True), - required=False + required=False, ) ldap_enable_sync = YesNoField( @@ -314,18 +300,17 @@ class GeneralParametersForm(param_forms.AdminParametersForm): initial=False, help_text=gettext_lazy( "Enable automatic synchronization between local database and " - "LDAP directory") + "LDAP directory" + ), ) ldap_sync_delete_remote_account = YesNoField( - label=gettext_lazy( - "Delete remote LDAP account when local account is deleted" - ), + label=gettext_lazy("Delete remote LDAP account when local account is deleted"), initial=False, help_text=gettext_lazy( "Delete remote LDAP account when local account is deleted, " "otherwise it will be disabled." - ) + ), ) ldap_sync_account_dn_template = forms.CharField( @@ -335,16 +320,15 @@ class GeneralParametersForm(param_forms.AdminParametersForm): "The template used to construct an account's DN. It should contain " "one placeholder (ie. %(user)s)" ), - required=False + required=False, ) ldap_enable_import = YesNoField( label=gettext_lazy("Enable import from LDAP"), initial=False, help_text=gettext_lazy( - "Enable account synchronization from LDAP directory to local " - "database" - ) + "Enable account synchronization from LDAP directory to local " "database" + ), ) ldap_import_search_base = forms.CharField( @@ -378,9 +362,8 @@ class GeneralParametersForm(param_forms.AdminParametersForm): label=gettext_lazy("Enable Dovecot LDAP sync"), initial=False, help_text=gettext_lazy( - "LDAP authentication settings will be applied to Dovecot " - "configuration." - ) + "LDAP authentication settings will be applied to Dovecot " "configuration." + ), ) ldap_dovecot_conf_file = forms.CharField( @@ -390,7 +373,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): help_text=gettext_lazy( "Location of the configuration file which contains " "Dovecot LDAP settings." - ) + ), ) dash_sep = SeparatorField(label=gettext_lazy("Dashboard")) @@ -400,7 +383,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): required=False, help_text=gettext_lazy( "Display custom RSS feed to resellers and domain administrators" - ) + ), ) hide_features_widget = YesNoField( @@ -408,7 +391,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): initial=False, help_text=gettext_lazy( "Hide features widget for resellers and domain administrators" - ) + ), ) notif_sep = SeparatorField(label=gettext_lazy("Notifications")) @@ -416,9 +399,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): sender_address = lib_fields.UTF8EmailField( label=_("Sender address"), initial="noreply@yourdomain.test", - help_text=_( - "Email address used to send notifications." - ) + help_text=_("Email address used to send notifications."), ) api_sep = SeparatorField(label=gettext_lazy("Public API")) @@ -426,38 +407,32 @@ class GeneralParametersForm(param_forms.AdminParametersForm): enable_api_communication = YesNoField( label=gettext_lazy("Enable communication"), initial=True, - help_text=gettext_lazy( - "Enable communication with Modoboa public API") + help_text=gettext_lazy("Enable communication with Modoboa public API"), ) check_new_versions = YesNoField( label=gettext_lazy("Check new versions"), initial=True, - help_text=gettext_lazy( - "Automatically checks if a newer version is available") + help_text=gettext_lazy("Automatically checks if a newer version is available"), ) send_new_versions_email = YesNoField( label=gettext_lazy("Send an email when new versions are found"), initial=False, - help_text=gettext_lazy( - "Send an email to notify admins about new versions" - ) + help_text=gettext_lazy("Send an email to notify admins about new versions"), ) new_versions_email_rcpt = lib_fields.UTF8EmailField( label=_("Recipient"), initial="postmaster@yourdomain.test", - help_text=_( - "Recipient of new versions notification emails." - ) + help_text=_("Recipient of new versions notification emails."), ) send_statistics = YesNoField( label=gettext_lazy("Send statistics"), initial=True, help_text=gettext_lazy( - "Send statistics to Modoboa public API " - "(counters and used extensions)") + "Send statistics to Modoboa public API " "(counters and used extensions)" + ), ) sep3 = SeparatorField(label=gettext_lazy("Miscellaneous")) @@ -465,10 +440,12 @@ class GeneralParametersForm(param_forms.AdminParametersForm): enable_inactive_accounts = YesNoField( label=_("Enable inactive account tracking"), initial=True, - help_text=_("Allow the administrator to set a threshold (in days) " - "beyond which an account is considered inactive " - "if the user hasn't logged in") - ) + help_text=_( + "Allow the administrator to set a threshold (in days) " + "beyond which an account is considered inactive " + "if the user hasn't logged in" + ), + ) inactive_account_threshold = forms.IntegerField( label=_("Inactive account threshold"), @@ -482,9 +459,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): top_notifications_check_interval = forms.IntegerField( label=_("Top notifications check interval"), initial=30, - help_text=_( - "Interval between two top notification checks (in seconds)" - ), + help_text=_("Interval between two top notification checks (in seconds)"), ) log_maximum_age = forms.IntegerField( @@ -533,15 +508,13 @@ class GeneralParametersForm(param_forms.AdminParametersForm): "send_new_versions_email": "check_new_versions=True", "new_versions_email_rcpt": "check_new_versions=True", "sms_provider": "sms_password_recovery=True", - "inactive_account_threshold": "enable_inactive_accounts=True" + "inactive_account_threshold": "enable_inactive_accounts=True", } def __init__(self, *args, **kwargs): super(GeneralParametersForm, self).__init__(*args, **kwargs) self.fields["default_top_redirection"].choices = enabled_applications() - self._add_visibilty_rules( - sms_backends.get_all_backend_visibility_rules() - ) + self._add_visibilty_rules(sms_backends.get_all_backend_visibility_rules()) self.fields["password_scheme"].choices = get_password_scheme() def _add_dynamic_fields(self): @@ -551,8 +524,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): if field == "sms_provider": sms_backend_fields = sms_backends.get_all_backend_settings() for field, definition in sms_backend_fields.items(): - new_fields[field] = definition["type"]( - **definition["attrs"]) + new_fields[field] = definition["type"](**definition["attrs"]) self.fields = new_fields def clean_ldap_user_dn_template(self): @@ -619,7 +591,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): required_fields = ["ldap_user_dn_template"] for f in required_fields: - if f not in cleaned_data or cleaned_data[f] == u'': + if f not in cleaned_data or cleaned_data[f] == "": self.add_error(f, _("This field is required")) return cleaned_data @@ -628,20 +600,22 @@ class GeneralParametersForm(param_forms.AdminParametersForm): """Apply configuration for given backend.""" import ldap from django_auth_ldap.config import ( - LDAPSearch, PosixGroupType, GroupOfNamesType, - ActiveDirectoryGroupType + LDAPSearch, + PosixGroupType, + GroupOfNamesType, + ActiveDirectoryGroupType, ) if not hasattr(settings, backend.setting_fullname("USER_ATTR_MAP")): - setattr(settings, backend.setting_fullname("USER_ATTR_MAP"), { - "first_name": "givenName", - "email": "mail", - "last_name": "sn" - }) + setattr( + settings, + backend.setting_fullname("USER_ATTR_MAP"), + {"first_name": "givenName", "email": "mail", "last_name": "sn"}, + ) ldap_uri = "ldaps://" if values["ldap_secured"] == "ssl" else "ldap://" ldap_uri += "%s:%s" % ( values[backend.srv_address_setting_name], - values[backend.srv_port_setting_name] + values[backend.srv_port_setting_name], ) setattr(settings, backend.setting_fullname("SERVER_URI"), ldap_uri) if values["ldap_secured"] == "starttls": @@ -649,48 +623,54 @@ class GeneralParametersForm(param_forms.AdminParametersForm): if values["ldap_is_active_directory"]: setattr( - settings, backend.setting_fullname("GROUP_TYPE"), - ActiveDirectoryGroupType() + settings, + backend.setting_fullname("GROUP_TYPE"), + ActiveDirectoryGroupType(), ) searchfilter = "(objectClass=group)" elif values["ldap_group_type"] == "groupofnames": - setattr(settings, backend.setting_fullname("GROUP_TYPE"), - GroupOfNamesType()) + setattr( + settings, backend.setting_fullname("GROUP_TYPE"), GroupOfNamesType() + ) searchfilter = "(objectClass=groupOfNames)" else: - setattr(settings, backend.setting_fullname("GROUP_TYPE"), - PosixGroupType()) + setattr(settings, backend.setting_fullname("GROUP_TYPE"), PosixGroupType()) searchfilter = "(objectClass=posixGroup)" - setattr(settings, backend.setting_fullname("GROUP_SEARCH"), LDAPSearch( - values["ldap_groups_search_base"], ldap.SCOPE_SUBTREE, - searchfilter - )) + setattr( + settings, + backend.setting_fullname("GROUP_SEARCH"), + LDAPSearch( + values["ldap_groups_search_base"], ldap.SCOPE_SUBTREE, searchfilter + ), + ) if values["ldap_auth_method"] == "searchbind": - setattr(settings, backend.setting_fullname("BIND_DN"), - values["ldap_bind_dn"]) setattr( - settings, backend.setting_fullname("BIND_PASSWORD"), - values["ldap_bind_password"] + settings, backend.setting_fullname("BIND_DN"), values["ldap_bind_dn"] + ) + setattr( + settings, + backend.setting_fullname("BIND_PASSWORD"), + values["ldap_bind_password"], ) search = LDAPSearch( - values["ldap_search_base"], ldap.SCOPE_SUBTREE, - values["ldap_search_filter"] + values["ldap_search_base"], + ldap.SCOPE_SUBTREE, + values["ldap_search_filter"], ) setattr(settings, backend.setting_fullname("USER_SEARCH"), search) else: setattr( - settings, backend.setting_fullname("USER_DN_TEMPLATE"), - values["ldap_user_dn_template"] + settings, + backend.setting_fullname("USER_DN_TEMPLATE"), + values["ldap_user_dn_template"], ) setattr( - settings, - backend.setting_fullname("BIND_AS_AUTHENTICATING_USER"), True) + settings, backend.setting_fullname("BIND_AS_AUTHENTICATING_USER"), True + ) if values["ldap_is_active_directory"]: setting = backend.setting_fullname("GLOBAL_OPTIONS") if not hasattr(settings, setting): - setattr(settings, setting, { - ldap.OPT_REFERRALS: False - }) + setattr(settings, setting, {ldap.OPT_REFERRALS: False}) else: getattr(settings, setting)[ldap.OPT_REFERRALS] = False @@ -701,6 +681,7 @@ class GeneralParametersForm(param_forms.AdminParametersForm): """ try: import ldap + ldap_available = True except ImportError: ldap_available = False @@ -710,12 +691,14 @@ class GeneralParametersForm(param_forms.AdminParametersForm): return from modoboa.lib.authbackends import LDAPBackend + self._apply_ldap_settings(values, LDAPBackend) if not values["ldap_enable_secondary_server"]: return from modoboa.lib.authbackends import LDAPSecondaryBackend + self._apply_ldap_settings(values, LDAPSecondaryBackend) def save(self): @@ -725,400 +708,622 @@ class GeneralParametersForm(param_forms.AdminParametersForm): self.localconfig.save(update_fields=["need_dovecot_update"]) -GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([ - ("authentication", { - "label": gettext_lazy("Authentication"), - "params": collections.OrderedDict([ - ("authentication_type", { - "label": gettext_lazy("Authentication type"), - "help_text": gettext_lazy( - "The backend used for authentication"), - }), - ("password_scheme", { - "label": gettext_lazy("Default password scheme"), - "help_text": gettext_lazy( - "Scheme used to crypt mailbox passwords"), - }), - ("rounds_number", { - "label": gettext_lazy("Rounds"), - "help_text": gettext_lazy( - "Number of rounds to use (only used by sha256crypt and " - "sha512crypt). Must be between 1000 and 999999999, " - "inclusive." - ) - }), - ("update_scheme", { - "label": gettext_lazy("Update password scheme at login"), - "help_text": gettext_lazy( - "Update user password at login to use the default " - "password scheme" - ) - }), - ("default_password", { - "label": gettext_lazy("Default password"), - "help_text": gettext_lazy( - "Default password for automatically created accounts.") - }), - ("random_password_length", { - "label": gettext_lazy("Random password length"), - "help_text": gettext_lazy( - "Length of randomly generated passwords.") - }), - ("update_password_url", { - "label": gettext_lazy("Update password service URL"), - "help_text": gettext_lazy( - "The URL of an external page where users will be able" - " to update their password. It applies only to non local" - " users, ie. those automatically created after a successful" - " external authentication (LDAP, SMTP)." - ) - }), - ("password_recovery_msg", { - "label": gettext_lazy("Password recovery announcement"), - "help_text": gettext_lazy( - "A temporary message that will be displayed on the " - "reset password page." - ) - }), - ("sms_password_recovery", { - "label": gettext_lazy("Enable password recovery by SMS"), - "help_text": gettext_lazy( - "Enable password recovery by SMS for users who filled " - "a phone number." - ) - }), - ("sms_provider", { - "label": gettext_lazy("SMS provider"), - "display": "sms_password_recovery=true", - "help_text": gettext_lazy( - "Choose a provider to send password recovery SMS" - ) - }), - *sms_backends.get_all_backend_structures() - ]) - }), - ("ldap", { - "label": _("LDAP"), - "params": collections.OrderedDict([ - ("ldap_server_address", { - "label": gettext_lazy("Server address"), - "help_text": gettext_lazy( - "The IP address or the DNS name of the LDAP server"), - }), - ("ldap_server_port", { - "label": gettext_lazy("Server port"), - "help_text": gettext_lazy( - "The TCP port number used by the LDAP server") - }), - ("ldap_enable_secondary_server", { - "label": gettext_lazy("Enable secondary server (fallback)"), - "help_text": gettext_lazy( - "Enable a secondary LDAP server which will be used " - "if the primary one fails" - ) - }), - ("ldap_secondary_server_address", { - "label": gettext_lazy("Secondary server address"), - "display": "ldap_enable_secondary_server=true", - "help_text": gettext_lazy( - "The IP address or the DNS name of the seondary LDAP server" - ) - }), - ("ldap_secondary_server_port", { - "label": gettext_lazy("Secondary server port"), - "display": "ldap_enable_secondary_server=true", - "help_text": gettext_lazy( - "The TCP port number used by the LDAP secondary server" - ) - }), - ("ldap_secured", { - "label": gettext_lazy("Use a secured connection"), - "help_text": gettext_lazy( - "Use an SSL/STARTTLS connection to access the LDAP server") - }), - ("ldap_is_active_directory", { - "label": gettext_lazy("Active Directory"), - "help_text": gettext_lazy( - "Tell if the LDAP server is an Active Directory one") - }), - ("ldap_admin_groups", { - "label": gettext_lazy("Administrator groups"), - "help_text": gettext_lazy( - "Members of those LDAP Posix groups will be created as " - "domain administrators. Use ';' characters to separate " - "groups." - ) - }), - ("ldap_group_type", { - "label": gettext_lazy("Group type"), - "help_text": gettext_lazy( - "The LDAP group type to use with your directory." - ) - }), - ("ldap_groups_search_base", { - "label": gettext_lazy("Groups search base"), - "help_text": gettext_lazy( - "The distinguished name of the search base used to find " - "groups" - ) - }), - ("ldap_password_attribute", { - "label": gettext_lazy("Password attribute"), - "help_text": gettext_lazy( - "The attribute used to store user passwords"), - }), - ("ldap_auth_sep", { - "label": gettext_lazy("LDAP authentication settings"), - "display": "authentication_type=ldap", - "separator": True - }), - ("ldap_auth_method", { - "label": gettext_lazy("Authentication method"), - "display": "authentication_type=ldap", - "help_text": gettext_lazy( - "Choose the authentication method to use"), - }), - ("ldap_bind_dn", { - "label": gettext_lazy("Bind DN"), - "help_text": gettext_lazy( - "The distinguished name to use when binding to the LDAP " - "server. Leave empty for an anonymous bind" +GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict( + [ + ( + "authentication", + { + "label": gettext_lazy("Authentication"), + "params": collections.OrderedDict( + [ + ( + "authentication_type", + { + "label": gettext_lazy("Authentication type"), + "help_text": gettext_lazy( + "The backend used for authentication" + ), + }, + ), + ( + "password_scheme", + { + "label": gettext_lazy("Default password scheme"), + "help_text": gettext_lazy( + "Scheme used to crypt mailbox passwords" + ), + }, + ), + ( + "rounds_number", + { + "label": gettext_lazy("Rounds"), + "help_text": gettext_lazy( + "Number of rounds to use (only used by sha256crypt and " + "sha512crypt). Must be between 1000 and 999999999, " + "inclusive." + ), + }, + ), + ( + "update_scheme", + { + "label": gettext_lazy( + "Update password scheme at login" + ), + "help_text": gettext_lazy( + "Update user password at login to use the default " + "password scheme" + ), + }, + ), + ( + "default_password", + { + "label": gettext_lazy("Default password"), + "help_text": gettext_lazy( + "Default password for automatically created accounts." + ), + }, + ), + ( + "random_password_length", + { + "label": gettext_lazy("Random password length"), + "help_text": gettext_lazy( + "Length of randomly generated passwords." + ), + }, + ), + ( + "update_password_url", + { + "label": gettext_lazy("Update password service URL"), + "help_text": gettext_lazy( + "The URL of an external page where users will be able" + " to update their password. It applies only to non local" + " users, ie. those automatically created after a successful" + " external authentication (LDAP, SMTP)." + ), + }, + ), + ( + "password_recovery_msg", + { + "label": gettext_lazy("Password recovery announcement"), + "help_text": gettext_lazy( + "A temporary message that will be displayed on the " + "reset password page." + ), + }, + ), + ( + "sms_password_recovery", + { + "label": gettext_lazy( + "Enable password recovery by SMS" + ), + "help_text": gettext_lazy( + "Enable password recovery by SMS for users who filled " + "a phone number." + ), + }, + ), + ( + "sms_provider", + { + "label": gettext_lazy("SMS provider"), + "display": "sms_password_recovery=true", + "help_text": gettext_lazy( + "Choose a provider to send password recovery SMS" + ), + }, + ), + *sms_backends.get_all_backend_structures(), + ] ), - "display": "authentication_type=ldap&ldap_auth_method=searchbind" - }), - ("ldap_bind_password", { - "label": gettext_lazy("Bind password"), - "help_text": gettext_lazy( - "The password to use when binding to the LDAP server " - "(with 'Bind DN')" + }, + ), + ( + "ldap", + { + "label": _("LDAP"), + "params": collections.OrderedDict( + [ + ( + "ldap_server_address", + { + "label": gettext_lazy("Server address"), + "help_text": gettext_lazy( + "The IP address or the DNS name of the LDAP server" + ), + }, + ), + ( + "ldap_server_port", + { + "label": gettext_lazy("Server port"), + "help_text": gettext_lazy( + "The TCP port number used by the LDAP server" + ), + }, + ), + ( + "ldap_enable_secondary_server", + { + "label": gettext_lazy( + "Enable secondary server (fallback)" + ), + "help_text": gettext_lazy( + "Enable a secondary LDAP server which will be used " + "if the primary one fails" + ), + }, + ), + ( + "ldap_secondary_server_address", + { + "label": gettext_lazy("Secondary server address"), + "display": "ldap_enable_secondary_server=true", + "help_text": gettext_lazy( + "The IP address or the DNS name of the seondary LDAP server" + ), + }, + ), + ( + "ldap_secondary_server_port", + { + "label": gettext_lazy("Secondary server port"), + "display": "ldap_enable_secondary_server=true", + "help_text": gettext_lazy( + "The TCP port number used by the LDAP secondary server" + ), + }, + ), + ( + "ldap_secured", + { + "label": gettext_lazy("Use a secured connection"), + "help_text": gettext_lazy( + "Use an SSL/STARTTLS connection to access the LDAP server" + ), + }, + ), + ( + "ldap_is_active_directory", + { + "label": gettext_lazy("Active Directory"), + "help_text": gettext_lazy( + "Tell if the LDAP server is an Active Directory one" + ), + }, + ), + ( + "ldap_admin_groups", + { + "label": gettext_lazy("Administrator groups"), + "help_text": gettext_lazy( + "Members of those LDAP Posix groups will be created as " + "domain administrators. Use ';' characters to separate " + "groups." + ), + }, + ), + ( + "ldap_group_type", + { + "label": gettext_lazy("Group type"), + "help_text": gettext_lazy( + "The LDAP group type to use with your directory." + ), + }, + ), + ( + "ldap_groups_search_base", + { + "label": gettext_lazy("Groups search base"), + "help_text": gettext_lazy( + "The distinguished name of the search base used to find " + "groups" + ), + }, + ), + ( + "ldap_password_attribute", + { + "label": gettext_lazy("Password attribute"), + "help_text": gettext_lazy( + "The attribute used to store user passwords" + ), + }, + ), + ( + "ldap_auth_sep", + { + "label": gettext_lazy("LDAP authentication settings"), + "display": "authentication_type=ldap", + "separator": True, + }, + ), + ( + "ldap_auth_method", + { + "label": gettext_lazy("Authentication method"), + "display": "authentication_type=ldap", + "help_text": gettext_lazy( + "Choose the authentication method to use" + ), + }, + ), + ( + "ldap_bind_dn", + { + "label": gettext_lazy("Bind DN"), + "help_text": gettext_lazy( + "The distinguished name to use when binding to the LDAP " + "server. Leave empty for an anonymous bind" + ), + "display": "authentication_type=ldap&ldap_auth_method=searchbind", + }, + ), + ( + "ldap_bind_password", + { + "label": gettext_lazy("Bind password"), + "help_text": gettext_lazy( + "The password to use when binding to the LDAP server " + "(with 'Bind DN')" + ), + "display": "authentication_type=ldap&ldap_auth_method=searchbind", + "password": True, + }, + ), + ( + "ldap_search_base", + { + "label": gettext_lazy("Users search base"), + "help_text": gettext_lazy( + "The distinguished name of the search base used to find " + "users" + ), + "display": "authentication_type=ldap&ldap_auth_method=searchbind", + }, + ), + ( + "ldap_search_filter", + { + "label": gettext_lazy("Search filter"), + "help_text": gettext_lazy( + "An optional filter string (e.g. '(objectClass=person)'). " + "In order to be valid, it must be enclosed in parentheses." + ), + "display": "authentication_type=ldap&ldap_auth_method=searchbind", + }, + ), + ( + "ldap_user_dn_template", + { + "label": gettext_lazy("User DN template"), + "help_text": gettext_lazy( + "The template used to construct a user's DN. It should " + "contain one placeholder (ie. %(user)s)" + ), + "display": "authentication_type=ldap&ldap_auth_method=directbind", + }, + ), + ( + "ldap_sync_sep", + { + "label": gettext_lazy("LDAP synchronization settings"), + "separator": True, + }, + ), + ( + "ldap_sync_bind_dn", + { + "label": gettext_lazy("Bind DN"), + "help_text": gettext_lazy( + "The distinguished name to use when binding to the LDAP server. " + "Leave empty for an anonymous bind" + ), + }, + ), + ( + "ldap_sync_bind_password", + { + "label": gettext_lazy("Bind password"), + "help_text": gettext_lazy( + "The password to use when binding to the LDAP server " + "(with 'Bind DN')" + ), + "password": True, + }, + ), + ( + "ldap_enable_sync", + { + "label": gettext_lazy("Enable export to LDAP"), + "help_text": gettext_lazy( + "Enable automatic synchronization between local database " + "and LDAP directory" + ), + }, + ), + ( + "ldap_sync_delete_remote_account", + { + "label": gettext_lazy( + "Delete remote LDAP account when local account is deleted" + ), + "help_text": gettext_lazy( + "Delete remote LDAP account when local account is deleted, " + "otherwise it will be disabled." + ), + "display": "ldap_enable_sync=true", + }, + ), + ( + "ldap_sync_account_dn_template", + { + "label": gettext_lazy("Account DN template"), + "help_text": gettext_lazy( + "The template used to construct an account's DN. It should " + "contain one placeholder (ie. %(user)s)" + ), + "display": "ldap_enable_sync=true", + }, + ), + ( + "ldap_enable_import", + { + "label": gettext_lazy("Enable import from LDAP"), + "help_text": gettext_lazy( + "Enable account synchronization from LDAP directory to " + "local database" + ), + }, + ), + ( + "ldap_import_search_base", + { + "label": gettext_lazy("Users search base"), + "help_text": gettext_lazy( + "The distinguished name of the search base used to find " + "users" + ), + "display": "ldap_enable_import=true", + }, + ), + ( + "ldap_import_search_filter", + { + "label": gettext_lazy("Search filter"), + "help_text": gettext_lazy( + "An optional filter string (e.g. '(objectClass=person)'). " + "In order to be valid, it must be enclosed in parentheses." + ), + "display": "ldap_enable_import=true", + }, + ), + ( + "ldap_import_username_attr", + { + "label": gettext_lazy("Username attribute"), + "help_text": gettext_lazy( + "The name of the LDAP attribute where the username can be " + "found." + ), + "display": "ldap_enable_import=true", + }, + ), + ( + "ldap_dovecot_sync", + { + "label": gettext_lazy("Enable Dovecot LDAP sync"), + "help_text": gettext_lazy( + "LDAP authentication settings will be applied to Dovecot " + "configuration." + ), + }, + ), + ( + "ldap_dovecot_conf_file", + { + "label": gettext_lazy("Dovecot LDAP config file"), + "help_text": gettext_lazy( + "Location of the configuration file which contains " + "Dovecot LDAP settings." + ), + "display": "ldap_dovecot_sync=true", + }, + ), + ] ), - "display": "authentication_type=ldap&ldap_auth_method=searchbind", - "password": True - }), - ("ldap_search_base", { - "label": gettext_lazy("Users search base"), - "help_text": gettext_lazy( - "The distinguished name of the search base used to find " - "users" + }, + ), + ( + "dashboard", + { + "label": gettext_lazy("Dashboard"), + "params": collections.OrderedDict( + [ + ( + "rss_feed_url", + { + "label": gettext_lazy("Custom RSS feed"), + "help_text": gettext_lazy( + "Display custom RSS feed to resellers and domain " + "administrators" + ), + }, + ), + ( + "hide_features_widget", + { + "label": gettext_lazy("Hide features widget"), + "help_text": gettext_lazy( + "Hide features widget for resellers and domain " + "administrators" + ), + }, + ), + ] ), - "display": "authentication_type=ldap&ldap_auth_method=searchbind" - }), - ("ldap_search_filter", { - "label": gettext_lazy("Search filter"), - "help_text": gettext_lazy( - "An optional filter string (e.g. '(objectClass=person)'). " - "In order to be valid, it must be enclosed in parentheses." + }, + ), + ( + "notifications", + { + "label": gettext_lazy("Notifications"), + "params": collections.OrderedDict( + [ + ( + "sender_address", + { + "label": gettext_lazy("Sender address"), + "help_text": gettext_lazy( + "Email address used to send notifications." + ), + }, + ) + ] ), - "display": "authentication_type=ldap&ldap_auth_method=searchbind" - }), - ("ldap_user_dn_template", { - "label": gettext_lazy("User DN template"), - "help_text": gettext_lazy( - "The template used to construct a user's DN. It should " - "contain one placeholder (ie. %(user)s)" + }, + ), + ( + "api", + { + "label": gettext_lazy("Public API"), + "params": collections.OrderedDict( + [ + ( + "enable_api_communication", + { + "label": gettext_lazy("Enable communication"), + "help_text": gettext_lazy( + "Automatically checks if a newer version is available" + ), + }, + ), + ( + "check_new_versions", + { + "label": gettext_lazy("Check new versions"), + "display": "enable_api_communication=true", + "help_text": gettext_lazy( + "Automatically checks if a newer version is available" + ), + }, + ), + ( + "send_new_versions_email", + { + "label": gettext_lazy( + "Send an email when new versions are found" + ), + "display": "check_new_versions=true", + "help_text": gettext_lazy( + "Send an email to notify admins about new versions" + ), + }, + ), + ( + "new_versions_email_rcpt", + { + "label": gettext_lazy("Recipient"), + "display": "check_new_versions=true", + "help_text": gettext_lazy( + "Recipient of new versions notification emails." + ), + }, + ), + ( + "send_statistics", + { + "label": gettext_lazy("Send statistics"), + "display": "enable_api_communication=true", + "help_text": gettext_lazy( + "Send statistics to Modoboa public API " + "(counters and used extensions)" + ), + }, + ), + ] ), - "display": "authentication_type=ldap&ldap_auth_method=directbind" - }), - ("ldap_sync_sep", { - "label": gettext_lazy("LDAP synchronization settings"), - "separator": True - }), - ("ldap_sync_bind_dn", { - "label": gettext_lazy("Bind DN"), - "help_text": gettext_lazy( - "The distinguished name to use when binding to the LDAP server. " - "Leave empty for an anonymous bind" - ) - }), - ("ldap_sync_bind_password", { - "label": gettext_lazy("Bind password"), - "help_text": gettext_lazy( - "The password to use when binding to the LDAP server " - "(with 'Bind DN')" + }, + ), + ( + "misc", + { + "label": gettext_lazy("Miscellaneous"), + "params": collections.OrderedDict( + [ + ( + "enable_inactive_accounts", + { + "label": gettext_lazy( + "Enable inactive account tracking" + ), + "help_text": gettext_lazy( + "Allow the administrator to set a threshold (in days) " + "beyond which an account is considered inactive " + "if the user hasn't logged in" + ), + }, + ), + ( + "inactive_account_threshold", + { + "label": gettext_lazy("Inactive account threshold"), + "display": "enable_inactive_accounts=true", + "help_text": gettext_lazy( + "An account with a last login date greater than this " + "threshold (in days) will be considered as inactive" + ), + }, + ), + ( + "top_notifications_check_interval", + { + "label": gettext_lazy( + "Top notifications check interval" + ), + "help_text": gettext_lazy( + "Interval between two top notification checks (in seconds)" + ), + }, + ), + ( + "log_maximum_age", + { + "label": gettext_lazy("Maximum log record age"), + "help_text": gettext_lazy( + "The maximum age in days of a log record" + ), + }, + ), + ( + "items_per_page", + { + "label": gettext_lazy("Items per page"), + "help_text": gettext_lazy( + "Number of displayed items per page" + ), + }, + ), + ( + "default_top_redirection", + { + "label": gettext_lazy("Default top redirection"), + "help_text": gettext_lazy( + "The default redirection used when no application is " + "specified" + ), + }, + ), + ] ), - "password": True - }), - ("ldap_enable_sync", { - "label": gettext_lazy("Enable export to LDAP"), - "help_text": gettext_lazy( - "Enable automatic synchronization between local database " - "and LDAP directory" - ) - }), - ("ldap_sync_delete_remote_account", { - "label": gettext_lazy( - "Delete remote LDAP account when local account is deleted"), - "help_text": gettext_lazy( - "Delete remote LDAP account when local account is deleted, " - "otherwise it will be disabled." - ), - "display": "ldap_enable_sync=true" - }), - ("ldap_sync_account_dn_template", { - "label": gettext_lazy("Account DN template"), - "help_text": gettext_lazy( - "The template used to construct an account's DN. It should " - "contain one placeholder (ie. %(user)s)" - ), - "display": "ldap_enable_sync=true" - }), - ("ldap_enable_import", { - "label": gettext_lazy("Enable import from LDAP"), - "help_text": gettext_lazy( - "Enable account synchronization from LDAP directory to " - "local database" - ) - }), - ("ldap_import_search_base", { - "label": gettext_lazy("Users search base"), - "help_text": gettext_lazy( - "The distinguished name of the search base used to find " - "users" - ), - "display": "ldap_enable_import=true" - }), - ("ldap_import_search_filter", { - "label": gettext_lazy("Search filter"), - "help_text": gettext_lazy( - "An optional filter string (e.g. '(objectClass=person)'). " - "In order to be valid, it must be enclosed in parentheses." - ), - "display": "ldap_enable_import=true" - }), - ("ldap_import_username_attr", { - "label": gettext_lazy("Username attribute"), - "help_text": gettext_lazy( - "The name of the LDAP attribute where the username can be " - "found." - ), - "display": "ldap_enable_import=true" - }), - ("ldap_dovecot_sync", { - "label": gettext_lazy("Enable Dovecot LDAP sync"), - "help_text": gettext_lazy( - "LDAP authentication settings will be applied to Dovecot " - "configuration." - ) - }), - ("ldap_dovecot_conf_file", { - "label": gettext_lazy("Dovecot LDAP config file"), - "help_text": gettext_lazy( - "Location of the configuration file which contains " - "Dovecot LDAP settings." - ), - "display": "ldap_dovecot_sync=true" - }), - - ]) - }), - ("dashboard", { - "label": gettext_lazy("Dashboard"), - "params": collections.OrderedDict([ - ("rss_feed_url", { - "label": gettext_lazy("Custom RSS feed"), - "help_text": gettext_lazy( - "Display custom RSS feed to resellers and domain " - "administrators" - ) - }), - ("hide_features_widget", { - "label": gettext_lazy("Hide features widget"), - "help_text": gettext_lazy( - "Hide features widget for resellers and domain " - "administrators" - ) - }), - ]) - }), - ("notifications", { - "label": gettext_lazy("Notifications"), - "params": collections.OrderedDict([ - ("sender_address", { - "label": gettext_lazy("Sender address"), - "help_text": gettext_lazy( - "Email address used to send notifications.") - }) - ]) - }), - ("api", { - "label": gettext_lazy("Public API"), - "params": collections.OrderedDict([ - ("enable_api_communication", { - "label": gettext_lazy("Enable communication"), - "help_text": gettext_lazy( - "Automatically checks if a newer version is available") - }), - ("check_new_versions", { - "label": gettext_lazy("Check new versions"), - "display": "enable_api_communication=true", - "help_text": gettext_lazy( - "Automatically checks if a newer version is available") - }), - ("send_new_versions_email", { - "label": gettext_lazy( - "Send an email when new versions are found"), - "display": "check_new_versions=true", - "help_text": gettext_lazy( - "Send an email to notify admins about new versions" - ) - }), - ("new_versions_email_rcpt", { - "label": gettext_lazy("Recipient"), - "display": "check_new_versions=true", - "help_text": gettext_lazy( - "Recipient of new versions notification emails." - ) - }), - ("send_statistics", { - "label": gettext_lazy("Send statistics"), - "display": "enable_api_communication=true", - "help_text": gettext_lazy( - "Send statistics to Modoboa public API " - "(counters and used extensions)") - }) - ]) - }), - ("misc", { - "label": gettext_lazy("Miscellaneous"), - "params": collections.OrderedDict([ - ("enable_inactive_accounts", { - "label": gettext_lazy("Enable inactive account tracking"), - "help_text": gettext_lazy( - "Allow the administrator to set a threshold (in days) " - "beyond which an account is considered inactive " - "if the user hasn't logged in" - ), - }), - ("inactive_account_threshold", { - "label": gettext_lazy("Inactive account threshold"), - "display": "enable_inactive_accounts=true", - "help_text": gettext_lazy( - "An account with a last login date greater than this " - "threshold (in days) will be considered as inactive" - ) - }), - ("top_notifications_check_interval", { - "label": gettext_lazy("Top notifications check interval"), - "help_text": gettext_lazy( - "Interval between two top notification checks (in seconds)" - ), - }), - ("log_maximum_age", { - "label": gettext_lazy("Maximum log record age"), - "help_text": gettext_lazy( - "The maximum age in days of a log record"), - }), - ("items_per_page", { - "label": gettext_lazy("Items per page"), - "help_text": gettext_lazy("Number of displayed items per page") - }), - ("default_top_redirection", { - "label": gettext_lazy("Default top redirection"), - "help_text": gettext_lazy( - "The default redirection used when no application is " - "specified" - ), - }) - ]) - }) -]) + }, + ), + ] +) diff --git a/modoboa/core/apps.py b/modoboa/core/apps.py index 311f497de..fb3e30eb6 100644 --- a/modoboa/core/apps.py +++ b/modoboa/core/apps.py @@ -16,11 +16,15 @@ def load_core_settings(): from .api.v2 import serializers param_tools.registry.add( - "global", app_settings.GeneralParametersForm, gettext_lazy("General")) + "global", app_settings.GeneralParametersForm, gettext_lazy("General") + ) param_tools.registry.add2( - "global", "core", gettext_lazy("General"), + "global", + "core", + gettext_lazy("General"), app_settings.GLOBAL_PARAMETERS_STRUCT, - serializers.CoreGlobalParametersSerializer) + serializers.CoreGlobalParametersSerializer, + ) class CoreConfig(AppConfig): diff --git a/modoboa/core/checks/settings_checks.py b/modoboa/core/checks/settings_checks.py index d5858e2e1..f72969226 100644 --- a/modoboa/core/checks/settings_checks.py +++ b/modoboa/core/checks/settings_checks.py @@ -3,9 +3,11 @@ from django.core.checks import Warning, register from django.utils.translation import gettext as _ W001 = Warning( - _("You have USE_TZ set to False, this may result in issues during " - "transitions between summer/winter time (ie the same local time occuring " - "twice due to clock change)."), + _( + "You have USE_TZ set to False, this may result in issues during " + "transitions between summer/winter time (ie the same local time occuring " + "twice due to clock change)." + ), hint=_("Set `USE_TZ = True` in settings.py"), id="modoboa.W001", ) diff --git a/modoboa/core/commands/__init__.py b/modoboa/core/commands/__init__.py index 2f73d5ae6..b935d655f 100644 --- a/modoboa/core/commands/__init__.py +++ b/modoboa/core/commands/__init__.py @@ -21,10 +21,9 @@ class Command(object): self._verbose = verbose if not settings.configured: settings.configure( - TEMPLATES=[{ - "BACKEND": ( - "django.template.backends.django.DjangoTemplates") - }] + TEMPLATES=[ + {"BACKEND": ("django.template.backends.django.DjangoTemplates")} + ] ) self._templates_dir = "%s/templates" % os.path.dirname(__file__) @@ -62,12 +61,12 @@ def scan_for_commands(dirname=""): if os.path.isfile(f): continue cmdname = f.replace(".py", "") - cmdmod = __import__("modoboa.core.commands", globals(), locals(), - [smart_str(cmdname)]) + cmdmod = __import__( + "modoboa.core.commands", globals(), locals(), [smart_str(cmdname)] + ) cmdmod = getattr(cmdmod, cmdname) if "_" in cmdname: - cmdclassname = "".join( - [s.capitalize() for s in cmdname.split("_")]) + cmdclassname = "".join([s.capitalize() for s in cmdname.split("_")]) else: cmdclassname = cmdname.capitalize() try: @@ -85,11 +84,13 @@ def handle_command_line(): description="A set of utilities to ease the installation of Modoboa.", epilog="""Available commands: %s -""" % "\n".join(["\t%s" % c for c in sorted(commands)])) - parser.add_argument("--verbose", action="store_true", - help="Activate verbose output") - parser.add_argument("command", type=str, - help="A valid command name") +""" + % "\n".join(["\t%s" % c for c in sorted(commands)]), + ) + parser.add_argument( + "--verbose", action="store_true", help="Activate verbose output" + ) + parser.add_argument("command", type=str, help="A valid command name") (args, remaining) = parser.parse_known_args() if args.command not in commands: diff --git a/modoboa/core/commands/deploy.py b/modoboa/core/commands/deploy.py index db80ee3c4..014fac790 100644 --- a/modoboa/core/commands/deploy.py +++ b/modoboa/core/commands/deploy.py @@ -37,7 +37,6 @@ DBCONN_TPL = """ class DeployCommand(Command): - """The ``deploy`` command.""" help = ( # NOQA:A003 @@ -47,45 +46,59 @@ class DeployCommand(Command): def __init__(self, *args, **kwargs): super(DeployCommand, self).__init__(*args, **kwargs) - self._parser.add_argument("name", type=str, - help="The name of your Modoboa instance") self._parser.add_argument( - "--collectstatic", action="store_true", default=False, - help="Run django collectstatic command" + "name", type=str, help="The name of your Modoboa instance" ) self._parser.add_argument( - "--dburl", type=str, nargs="+", default=None, - help="A database-url with a name") - self._parser.add_argument( - "--domain", type=str, default=None, - help="The domain under which you want to deploy modoboa") - self._parser.add_argument( - "--lang", type=str, default="en", - help="Set the default language" + "--collectstatic", + action="store_true", + default=False, + help="Run django collectstatic command", ) self._parser.add_argument( - "--timezone", type=str, default="UTC", - help="Set the local timezone" + "--dburl", + type=str, + nargs="+", + default=None, + help="A database-url with a name", ) self._parser.add_argument( - "--devel", action="store_true", default=False, - help="Create a development instance" + "--domain", + type=str, + default=None, + help="The domain under which you want to deploy modoboa", ) self._parser.add_argument( - "--extensions", type=str, nargs="*", - help="The list of extension to deploy" + "--lang", type=str, default="en", help="Set the default language" ) self._parser.add_argument( - "--dont-install-extensions", action="store_true", default=False, - help="Do not install extensions using pip" + "--timezone", type=str, default="UTC", help="Set the local timezone" ) self._parser.add_argument( - "--admin-username", default="admin", - help="Username of the initial super administrator" + "--devel", + action="store_true", + default=False, + help="Create a development instance", ) self._parser.add_argument( - "--secret-key-length", default=60, type=int, - help="Specify the length of the secret key" + "--extensions", type=str, nargs="*", help="The list of extension to deploy" + ) + self._parser.add_argument( + "--dont-install-extensions", + action="store_true", + default=False, + help="Do not install extensions using pip", + ) + self._parser.add_argument( + "--admin-username", + default="admin", + help="Username of the initial super administrator", + ) + self._parser.add_argument( + "--secret-key-length", + default=60, + type=int, + help="Specify the length of the secret key", ) def _exec_django_command(self, name, cwd, *args): @@ -109,7 +122,7 @@ class DeployCommand(Command): if output: print( "\n".join([l.decode() for l in output if l is not None]), - file=sys.stderr + file=sys.stderr, ) print("%s failed, check your configuration" % cmd, file=sys.stderr) @@ -124,8 +137,7 @@ class DeployCommand(Command): print("Configuring database connection: %s" % name) info = { "conn_name": name, - "ENGINE": input( - "Database type (mysql, postgres or sqlite3): ") + "ENGINE": input("Database type (mysql, postgres or sqlite3): "), } if info["ENGINE"] not in ["mysql", "postgres", "sqlite3"]: raise RuntimeError("Unsupported database engine") @@ -141,8 +153,7 @@ class DeployCommand(Command): info["ENGINE"] = "django.db.backends.mysql" default_port = 3306 info["HOST"] = input("Database host (default: 'localhost'): ") - info["PORT"] = input( - "Database port (default: '%s'): " % default_port) + info["PORT"] = input("Database port (default: '%s'): " % default_port) # leave port setting empty, if default value is supplied and # leave it to django if info["PORT"] == default_port: @@ -167,7 +178,7 @@ class DeployCommand(Command): is marked as insecure. """ c = string.ascii_letters + string.digits + string.punctuation - secret_key = ''.join(secrets.choice(c) for i in range(secret_key_length)) + secret_key = "".join(secrets.choice(c) for i in range(secret_key_length)) return secret_key def find_extra_settings(self, extensions): @@ -188,9 +199,7 @@ class DeployCommand(Command): def handle(self, parsed_args): django.setup() - management.call_command( - "startproject", parsed_args.name, verbosity=False - ) + management.call_command("startproject", parsed_args.name, verbosity=False) path = "%(name)s/%(name)s" % {"name": parsed_args.name} sys.path.append(parsed_args.name) @@ -210,14 +219,12 @@ class DeployCommand(Command): info["conn_name"] = conn_name connections[conn_name] = conn_tpl.render(Context(info)) else: - connections["default"] = conn_tpl.render( - Context(self.ask_db_info())) + connections["default"] = conn_tpl.render(Context(self.ask_db_info())) if parsed_args.domain: allowed_host = parsed_args.domain else: - allowed_host = input( - "What will be the hostname used to access Modoboa? ") + allowed_host = input("What will be the hostname used to access Modoboa? ") if not allowed_host: allowed_host = "localhost" extra_settings = [] @@ -225,13 +232,14 @@ class DeployCommand(Command): if extensions: if "all" in extensions: extensions = self._get_extension_list() - extensions = [(extension, extension.replace("-", "_")) - for extension in extensions] + extensions = [ + (extension, extension.replace("-", "_")) for extension in extensions + ] if not parsed_args.dont_install_extensions: cmd = ( - sys.executable + - " -m pip install " + - " ".join([extension[0] for extension in extensions]) + sys.executable + + " -m pip install " + + " ".join([extension[0] for extension in extensions]) ) exec_cmd(cmd, capture_output=False) extra_settings = self.find_extra_settings(extensions) @@ -241,11 +249,10 @@ class DeployCommand(Command): os.path.join(os.path.dirname(__file__), "../../bower_components") ) - mod = __import__( - parsed_args.name, globals(), locals(), [smart_str("settings")] - ) + mod = __import__(parsed_args.name, globals(), locals(), [smart_str("settings")]) tpl = self._render_template( - "%s/settings.py.tpl" % self._templates_dir, { + "%s/settings.py.tpl" % self._templates_dir, + { "db_connections": connections, "secret_key": self._generate_secret_key(parsed_args.secret_key_length), "name": parsed_args.name, @@ -255,42 +262,40 @@ class DeployCommand(Command): "bower_components_dir": bower_components_dir, "devmode": parsed_args.devel, "extensions": extensions, - "extra_settings": extra_settings - } + "extra_settings": extra_settings, + }, ) with open("%s/settings.py" % path, "w") as fp: fp.write(tpl) - shutil.copyfile( - "%s/urls.py.tpl" % self._templates_dir, "%s/urls.py" % path - ) + shutil.copyfile("%s/urls.py.tpl" % self._templates_dir, "%s/urls.py" % path) os.mkdir("%s/media" % parsed_args.name) if isfile("%s/settings.pyc" % path): os.unlink("%s/settings.pyc" % path) + self._exec_django_command("migrate", parsed_args.name, "--noinput") self._exec_django_command( - "migrate", parsed_args.name, "--noinput" - ) - self._exec_django_command( - "load_initial_data", parsed_args.name, - "--admin-username", parsed_args.admin_username + "load_initial_data", + parsed_args.name, + "--admin-username", + parsed_args.admin_username, ) if parsed_args.collectstatic: - self._exec_django_command( - "collectstatic", parsed_args.name, "--noinput" - ) - self._exec_django_command( - "set_default_site", parsed_args.name, allowed_host - ) + self._exec_django_command("collectstatic", parsed_args.name, "--noinput") + self._exec_django_command("set_default_site", parsed_args.name, allowed_host) base_frontend_dir = os.path.join( - os.path.dirname(__file__), "../../frontend_dist/") + os.path.dirname(__file__), "../../frontend_dist/" + ) if not os.path.exists(base_frontend_dir): return frontend_target_dir = "{}/frontend".format(parsed_args.name) shutil.copytree(base_frontend_dir, frontend_target_dir) with open("{}/config.json".format(frontend_target_dir), "w") as fp: - fp.write("""{ + fp.write( + """{ "API_BASE_URL": "https://%s/api/v2" } -""" % allowed_host) +""" + % allowed_host + ) diff --git a/modoboa/core/commands/help.py b/modoboa/core/commands/help.py index bbfea1916..2c7049b8f 100644 --- a/modoboa/core/commands/help.py +++ b/modoboa/core/commands/help.py @@ -9,8 +9,7 @@ class HelpCommand(Command): def __init__(self, *args, **kwargs): super(HelpCommand, self).__init__(*args, **kwargs) - self._parser.add_argument("name", type=str, - help="A command name") + self._parser.add_argument("name", type=str, help="A command name") def handle(self, parsed_args): if parsed_args.name not in self._commands: diff --git a/modoboa/core/constants.py b/modoboa/core/constants.py index 20e7a2150..e407a4ed7 100644 --- a/modoboa/core/constants.py +++ b/modoboa/core/constants.py @@ -22,25 +22,25 @@ ADMIN_GROUPS = [ ] LANGUAGES = ( - ("br", u"breton"), - ("cs", u"čeština"), - ("de", u"deutsch"), - ("en", u"english"), - ("el", u"ελληνικά"), - ("es", u"español"), + ("br", "breton"), + ("cs", "čeština"), + ("de", "deutsch"), + ("en", "english"), + ("el", "ελληνικά"), + ("es", "español"), ("fi", "Suomalainen"), - ("fr", u"français"), - ("it", u"italiano"), - ("ja", u"日本の"), - ("nl", u"nederlands"), - ("pt", u"português"), - ("pt-br", u"português (BR)"), - ("pl", u"polski"), - ("ro", u"Română"), - ("ru", u"русский"), - ("sv", u"svenska"), - ("tr", u"türk"), - ("zh-hant", u"中文(台灣)"), + ("fr", "français"), + ("it", "italiano"), + ("ja", "日本の"), + ("nl", "nederlands"), + ("pt", "português"), + ("pt-br", "português (BR)"), + ("pl", "polski"), + ("ro", "Română"), + ("ru", "русский"), + ("sv", "svenska"), + ("tr", "türk"), + ("zh-hant", "中文(台灣)"), ) @@ -52,12 +52,12 @@ LDAP_GROUP_TYPES = ( LDAP_SECURE_MODES = [ ("none", gettext_lazy("No")), ("starttls", "STARTTLS"), - ("ssl", "SSL/TLS") + ("ssl", "SSL/TLS"), ] LDAP_AUTH_METHODS = [ ("searchbind", gettext_lazy("Search and bind")), - ("directbind", gettext_lazy("Direct bind")) + ("directbind", gettext_lazy("Direct bind")), ] PERMISSIONS = { @@ -101,7 +101,7 @@ PERMISSIONS = { ["admin", "domainalias", "add_domainalias"], ["admin", "domainalias", "change_domainalias"], ["admin", "domainalias", "delete_domainalias"], - ] + ], } SMS_BACKENDS = [ diff --git a/modoboa/core/context_processors.py b/modoboa/core/context_processors.py index 6b967b758..c8ce1a41c 100644 --- a/modoboa/core/context_processors.py +++ b/modoboa/core/context_processors.py @@ -11,19 +11,18 @@ def top_notifications(request): if request.user.is_anonymous: return {} interval = request.localconfig.parameters.get_value( - "top_notifications_check_interval") + "top_notifications_check_interval" + ) notifications = signals.get_top_notifications.send( - sender="top_notifications", include_all=False) - notifications = reduce( - lambda a, b: a + b, [notif[1] for notif in notifications]) + sender="top_notifications", include_all=False + ) + notifications = reduce(lambda a, b: a + b, [notif[1] for notif in notifications]) return { "notifications_check_interval": interval * 1000, - "top_notifications": notifications + "top_notifications": notifications, } def new_admin_url(*args): """A context processor to include new admin url.""" - return { - "new_admin": getattr(settings, "NEW_ADMIN_URL", "new-admin") - } + return {"new_admin": getattr(settings, "NEW_ADMIN_URL", "new-admin")} diff --git a/modoboa/core/dev_settings.py b/modoboa/core/dev_settings.py index 8f8b2df1c..21da259aa 100644 --- a/modoboa/core/dev_settings.py +++ b/modoboa/core/dev_settings.py @@ -2,9 +2,7 @@ import os -BOWER_COMPONENTS_ROOT = os.path.join( - os.path.dirname(__file__), ".." -) +BOWER_COMPONENTS_ROOT = os.path.join(os.path.dirname(__file__), "..") BOWER_INSTALLED_APPS = ( "jquery#1.9", diff --git a/modoboa/core/drf_authentication.py b/modoboa/core/drf_authentication.py index 9427fca5a..561c88fee 100644 --- a/modoboa/core/drf_authentication.py +++ b/modoboa/core/drf_authentication.py @@ -14,8 +14,8 @@ from . import constants class NotVerified(APIException): status_code = status.HTTP_418_IM_A_TEAPOT - default_detail = _('Missing 2FA verification.') - default_code = 'not_verified' + default_detail = _("Missing 2FA verification.") + default_code = "not_verified" class JWTAuthenticationWith2FA(JWTAuthentication): @@ -25,9 +25,7 @@ class JWTAuthenticationWith2FA(JWTAuthentication): header = self.get_header(request) raw_token = self.get_raw_token(header) validated_token = self.get_validated_token(raw_token) - url_exceptions = ( - reverse("v2:account-tfa-verify-code"), - ) + url_exceptions = (reverse("v2:account-tfa-verify-code"),) if request.path in url_exceptions: return if constants.TFA_DEVICE_TOKEN_KEY in validated_token: @@ -48,5 +46,5 @@ class JWTAuthenticationWith2FA(JWTAuthentication): class SimpleJWTWith2FAScheme(SimpleJWTScheme): - target_class = 'modoboa.core.drf_authentication.JWTAuthenticationWith2FA' - name = 'jwt2FAAuth' + target_class = "modoboa.core.drf_authentication.JWTAuthenticationWith2FA" + name = "jwt2FAAuth" diff --git a/modoboa/core/extensions.py b/modoboa/core/extensions.py index 0aab96f56..1b1c86b41 100644 --- a/modoboa/core/extensions.py +++ b/modoboa/core/extensions.py @@ -32,10 +32,13 @@ class ModoExtension(object): def infos(self): """Information about this extension.""" return { - "name": self.name, "label": self.label, "version": self.version, - "description": self.description, "url": self.get_url(), + "name": self.name, + "label": self.label, + "version": self.version, + "description": self.description, + "url": self.get_url(), "topredirection_url": self.topredirection_url, - "always_active": self.always_active + "always_active": self.always_active, } def load_initial_data(self): @@ -111,9 +114,7 @@ class ExtensionsPool(object): root = r"^{}/".format(ext.get_url()) pattern = "{}.urls" try: - result.append( - re_path(root, include(pattern.format(ext_name))) - ) + result.append(re_path(root, include(pattern.format(ext_name)))) except ImportError: # No urls for this extension pass diff --git a/modoboa/core/factories.py b/modoboa/core/factories.py index 838b9ffd3..92a2e697f 100644 --- a/modoboa/core/factories.py +++ b/modoboa/core/factories.py @@ -8,7 +8,6 @@ from . import models class PermissionFactory(factory.django.DjangoModelFactory): - """A base factory to handle permissions.""" class Meta(object): @@ -22,7 +21,6 @@ class PermissionFactory(factory.django.DjangoModelFactory): class GroupFactory(factory.django.DjangoModelFactory): - """A factory to create Group instances.""" class Meta(object): @@ -32,12 +30,11 @@ class GroupFactory(factory.django.DjangoModelFactory): class UserFactory(PermissionFactory): - """A factory to create User instances.""" class Meta(object): model = models.User - django_get_or_create = ("username", ) + django_get_or_create = ("username",) email = factory.LazyAttribute(lambda a: a.username) password = "{PLAIN}toto" diff --git a/modoboa/core/forms.py b/modoboa/core/forms.py index 031a78cdc..e5bda6f0c 100644 --- a/modoboa/core/forms.py +++ b/modoboa/core/forms.py @@ -3,9 +3,7 @@ import oath from django import forms -from django.contrib.auth import ( - forms as auth_forms, get_user_model, password_validation -) +from django.contrib.auth import forms as auth_forms, get_user_model, password_validation from django.db.models import Q from django.utils.translation import gettext as _, gettext_lazy @@ -21,41 +19,46 @@ class LoginForm(forms.Form): username = forms.CharField( label=gettext_lazy("Username"), - widget=forms.TextInput(attrs={"class": "form-control"}) + widget=forms.TextInput(attrs={"class": "form-control"}), ) password = forms.CharField( label=gettext_lazy("Password"), - widget=forms.PasswordInput(attrs={"class": "form-control"}) - ) - rememberme = forms.BooleanField( - initial=False, - required=False + widget=forms.PasswordInput(attrs={"class": "form-control"}), ) + rememberme = forms.BooleanField(initial=False, required=False) class ProfileForm(forms.ModelForm): """Form to update User profile.""" oldpassword = forms.CharField( - label=gettext_lazy("Old password"), required=False, - widget=forms.PasswordInput(attrs={"class": "form-control"}) + label=gettext_lazy("Old password"), + required=False, + widget=forms.PasswordInput(attrs={"class": "form-control"}), ) newpassword = forms.CharField( - label=gettext_lazy("New password"), required=False, - widget=forms.PasswordInput(attrs={"class": "form-control"}) + label=gettext_lazy("New password"), + required=False, + widget=forms.PasswordInput(attrs={"class": "form-control"}), ) confirmation = forms.CharField( - label=gettext_lazy("Confirmation"), required=False, - widget=forms.PasswordInput(attrs={"class": "form-control"}) + label=gettext_lazy("Confirmation"), + required=False, + widget=forms.PasswordInput(attrs={"class": "form-control"}), ) class Meta: model = User - fields = ("first_name", "last_name", "language", - "phone_number", "secondary_email") + fields = ( + "first_name", + "last_name", + "language", + "phone_number", + "secondary_email", + ) widgets = { "first_name": forms.TextInput(attrs={"class": "form-control"}), - "last_name": forms.TextInput(attrs={"class": "form-control"}) + "last_name": forms.TextInput(attrs={"class": "form-control"}), } def __init__(self, update_password, *args, **kwargs): @@ -90,7 +93,8 @@ class ProfileForm(forms.ModelForm): else: try: password_validation.validate_password( - confirmation, self.instance) + confirmation, self.instance + ) except forms.ValidationError as err: self.add_error("confirmation", err) else: @@ -107,8 +111,7 @@ class ProfileForm(forms.ModelForm): if commit: if self.cleaned_data.get("confirmation", "") != "": user.set_password( - self.cleaned_data["confirmation"], - self.cleaned_data["oldpassword"] + self.cleaned_data["confirmation"], self.cleaned_data["oldpassword"] ) user.save() return user @@ -118,7 +121,8 @@ class APIAccessForm(forms.Form): """Form to control API access.""" enable_api_access = forms.BooleanField( - label=gettext_lazy("Enable API access"), required=False) + label=gettext_lazy("Enable API access"), required=False + ) def __init__(self, *args, **kwargs): """Initialize form.""" @@ -133,19 +137,30 @@ class PasswordResetForm(auth_forms.PasswordResetForm): def get_users(self, email): """Return matching user(s) who should receive a reset.""" return ( - get_user_model()._default_manager.filter( - email__iexact=email, is_active=True) + get_user_model() + ._default_manager.filter(email__iexact=email, is_active=True) .exclude(Q(secondary_email__isnull=True) | Q(secondary_email="")) ) - def send_mail(self, subject_template_name, email_template_name, - context, from_email, to_email, - html_email_template_name=None): + def send_mail( + self, + subject_template_name, + email_template_name, + context, + from_email, + to_email, + html_email_template_name=None, + ): """Send message to secondary email instead.""" to_email = context["user"].secondary_email super(PasswordResetForm, self).send_mail( - subject_template_name, email_template_name, - context, from_email, to_email, html_email_template_name) + subject_template_name, + email_template_name, + context, + from_email, + to_email, + html_email_template_name, + ) class VerifySMSCodeForm(forms.Form): @@ -153,7 +168,7 @@ class VerifySMSCodeForm(forms.Form): code = forms.CharField( label=gettext_lazy("Verification code"), - widget=forms.widgets.TextInput(attrs={"class": "form-control"}) + widget=forms.widgets.TextInput(attrs={"class": "form-control"}), ) def __init__(self, *args, **kwargs): diff --git a/modoboa/core/handlers.py b/modoboa/core/handlers.py index 0934cb3d0..7c4fe6584 100644 --- a/modoboa/core/handlers.py +++ b/modoboa/core/handlers.py @@ -40,8 +40,9 @@ def post_revision_commit(sender, **kwargs): level = "warning" message = _("%(object)s '%(name)s' %(action)s by user %(user)s") % { "object": smart_str(version.content_type).capitalize(), - "name": version.object_repr, "action": action, - "user": current_user + "name": version.object_repr, + "action": action, + "user": current_user, } getattr(logger, level)(message) @@ -62,7 +63,8 @@ def log_object_removal(sender, instance, **kwargs): logger = logging.getLogger("modoboa.admin") msg = _("%(object)s '%(name)s' %(action)s by ") % { "object": smart_str(version.content_type).capitalize(), - "name": version.object_repr, "action": _("deleted") + "name": version.object_repr, + "action": _("deleted"), } request = get_request() if request: @@ -79,8 +81,7 @@ def create_local_config(sender, **kwargs): return if models.LocalConfig.objects.using(kwargs["using"]).exists(): return - models.LocalConfig.objects.create( - site=sites_models.Site.objects.get_current()) + models.LocalConfig.objects.create(site=sites_models.Site.objects.get_current()) @receiver(signals.pre_delete, sender=models.User) @@ -91,16 +92,13 @@ def update_permissions(sender, instance, **kwargs): if request: from_user = request.user if from_user == instance: - raise exceptions.PermDeniedException( - _("You can't delete your own account") - ) + raise exceptions.PermDeniedException(_("You can't delete your own account")) if not from_user.can_access(instance): raise exceptions.PermDeniedException # We send an additional signal before permissions are removed - core_signals.account_deleted.send( - sender="update_permissions", user=instance) + core_signals.account_deleted.send(sender="update_permissions", user=instance) owner = permissions.get_object_owner(instance) if owner == instance: # The default admin is being removed... @@ -108,10 +106,8 @@ def update_permissions(sender, instance, **kwargs): # Change ownership of existing objects for ooentry in instance.objectaccess_set.filter(is_owner=True): if ooentry.content_object is not None: - permissions.grant_access_to_object( - owner, ooentry.content_object, True) - permissions.ungrant_access_to_object( - ooentry.content_object, instance) + permissions.grant_access_to_object(owner, ooentry.content_object, True) + permissions.ungrant_access_to_object(ooentry.content_object, instance) # Remove existing permissions on this user permissions.ungrant_access_to_object(instance) @@ -125,9 +121,11 @@ def check_for_new_versions(sender, include_all, **kwargs): status, extensions = utils.check_for_updates() if not status: return [{"id": "newversionavailable"}] if include_all else [] - return [{ - "id": "newversionavailable", - "url": reverse("core:index") + "#info/", - "text": _("One or more updates are available"), - "level": "info", - }] + return [ + { + "id": "newversionavailable", + "url": reverse("core:index") + "#info/", + "text": _("One or more updates are available"), + "level": "info", + } + ] diff --git a/modoboa/core/loggers.py b/modoboa/core/loggers.py index 35efe0397..cf52213e4 100644 --- a/modoboa/core/loggers.py +++ b/modoboa/core/loggers.py @@ -7,6 +7,5 @@ class SQLHandler(logging.Handler): from .models import Log Log.objects.create( - message=record.getMessage(), level=record.levelname, - logger=record.name + message=record.getMessage(), level=record.levelname, logger=record.name ) diff --git a/modoboa/core/management/commands/clean_inactive_accounts.py b/modoboa/core/management/commands/clean_inactive_accounts.py index 2c48e4754..fbfed94b9 100644 --- a/modoboa/core/management/commands/clean_inactive_accounts.py +++ b/modoboa/core/management/commands/clean_inactive_accounts.py @@ -17,44 +17,53 @@ class Command(BaseCommand): def add_arguments(self, parser): """Add extra arguments to command line.""" parser.add_argument( - "--delete", action="store_true", default=False, - help="Delete inactive accounts (default is to disable)") + "--delete", + action="store_true", + default=False, + help="Delete inactive accounts (default is to disable)", + ) parser.add_argument( - "--dry-run", action="store_true", default=False, - help="Only look for inactive accounts, no action") + "--dry-run", + action="store_true", + default=False, + help="Only look for inactive accounts, no action", + ) parser.add_argument( - "--silent", action="store_true", default=False, - help="Enable silent mode (no question)") + "--silent", + action="store_true", + default=False, + help="Enable silent mode (no question)", + ) parser.add_argument( - "--verbose", action="store_true", default=False, - help="Enable verbose mode") + "--verbose", action="store_true", default=False, help="Enable verbose mode" + ) def _log_inactive_accounts(self, qset): """Log inactive accounts found.""" - print( - "The following inactive accounts have been found:", - file=self.stdout) + print("The following inactive accounts have been found:", file=self.stdout) for account in qset.values("username", "last_login"): print( "> {}\n (last login: {})".format( - account["username"], account["last_login"]), - file=self.stdout + account["username"], account["last_login"] + ), + file=self.stdout, ) def handle(self, *args, **options): """Command entry point.""" - if not param_tools.get_global_parameter( - "enable_inactive_accounts"): + if not param_tools.get_global_parameter("enable_inactive_accounts"): if options["verbose"]: - print("Inactive accounts detection is disabled.", - file=self.stdout) + print("Inactive accounts detection is disabled.", file=self.stdout) return inactive_account_threshold = param_tools.get_global_parameter( - "inactive_account_threshold") + "inactive_account_threshold" + ) qset = models.User.objects.filter( - is_active=True, is_superuser=False, - last_login__lt=timezone.now() - - relativedelta(days=inactive_account_threshold)) + is_active=True, + is_superuser=False, + last_login__lt=timezone.now() + - relativedelta(days=inactive_account_threshold), + ) if not qset.exists(): return if options["verbose"]: @@ -63,8 +72,7 @@ class Command(BaseCommand): return action = "delete" if options["delete"] else "disable" if not options["silent"]: - answer = input( - "Do you want to {} those accounts? (y/N) ".format(action)) + answer = input("Do you want to {} those accounts? (y/N) ".format(action)) if not answer.lower().startswith("y"): return if action == "disable": diff --git a/modoboa/core/management/commands/cleanlogs.py b/modoboa/core/management/commands/cleanlogs.py index 179aa0262..b7332a6e5 100644 --- a/modoboa/core/management/commands/cleanlogs.py +++ b/modoboa/core/management/commands/cleanlogs.py @@ -16,11 +16,14 @@ class Command(BaseCommand): def add_arguments(self, parser): """Add extra arguments to command line.""" parser.add_argument( - "--debug", action="store_true", default=False, - help="Activate debug output") + "--debug", action="store_true", default=False, help="Activate debug output" + ) parser.add_argument( - "--verbose", action="store_true", default=False, - help="Display informational messages") + "--verbose", + action="store_true", + default=False, + help="Display informational messages", + ) def __vprint(self, msg): if not self.verbose: diff --git a/modoboa/core/management/commands/communicate_with_public_api.py b/modoboa/core/management/commands/communicate_with_public_api.py index d5d850621..a891f8801 100644 --- a/modoboa/core/management/commands/communicate_with_public_api.py +++ b/modoboa/core/management/commands/communicate_with_public_api.py @@ -31,30 +31,29 @@ class Command(BaseCommand): updates[extension["name"]] = extension["last_version"] to_create.append( models.ExtensionUpdateHistory( - extension=extension["name"], - version=extension["last_version"] + extension=extension["name"], version=extension["last_version"] ) ) if not updates: return content = render_to_string( - "core/notifications/update_available.html", { - "updates": updates - }) + "core/notifications/update_available.html", {"updates": updates} + ) subject = _("[modoboa] Update(s) available") sender = local_config.parameters.get_value("sender_address") - recipient = local_config.parameters.get_value( - "new_versions_email_rcpt") + recipient = local_config.parameters.get_value("new_versions_email_rcpt") msg = EmailMessage( - subject, content.strip(), sender, [recipient], + subject, + content.strip(), + sender, + [recipient], ) msg.send() models.ExtensionUpdateHistory.objects.bulk_create(to_create) def handle(self, *args, **options): """Command entry point.""" - local_config = ( - models.LocalConfig.objects.select_related("site").first()) + local_config = models.LocalConfig.objects.select_related("site").first() if not local_config.parameters.get_value("enable_api_communication"): return self.client = api_client.ModoAPIClient() @@ -87,7 +86,8 @@ class Command(BaseCommand): "domain_alias_counter": admin_models.DomainAlias.objects.count(), "mailbox_counter": admin_models.Mailbox.objects.count(), "alias_counter": ( - admin_models.Alias.objects.filter(internal=False).count()), - "extensions": extensions + admin_models.Alias.objects.filter(internal=False).count() + ), + "extensions": extensions, } self.client.update_instance(local_config.api_pk, data) diff --git a/modoboa/core/management/commands/generate_postfix_maps.py b/modoboa/core/management/commands/generate_postfix_maps.py index 92cb87b18..5677d2700 100644 --- a/modoboa/core/management/commands/generate_postfix_maps.py +++ b/modoboa/core/management/commands/generate_postfix_maps.py @@ -28,19 +28,20 @@ class Command(BaseCommand): def add_arguments(self, parser): """Add extra arguments.""" + parser.add_argument("--dburl", help="Custom database url") parser.add_argument( - "--dburl", help="Custom database url") + "--destdir", default=".", help="Directory where files will be created" + ) parser.add_argument( - "--destdir", default=".", - help="Directory where files will be created") - parser.add_argument( - "--force-overwrite", action="store_true", default=False, - help="Force overwrite of existing map files") + "--force-overwrite", + action="store_true", + default=False, + help="Force overwrite of existing map files", + ) def __load_checksums(self, destdir): """Load existing checksums if possible.""" - self.__checksums_file = os.path.join( - destdir, "modoboa-postfix-maps.chk") + self.__checksums_file = os.path.join(destdir, "modoboa-postfix-maps.chk") self.__checksums = {} if not os.path.exists(self.__checksums_file): return @@ -48,7 +49,8 @@ class Command(BaseCommand): for line in fp: fname, dbtype, checksum = line.split(":") self.__checksums[fname.strip()] = { - "dbtype": dbtype, "checksum": checksum.strip() + "dbtype": dbtype, + "checksum": checksum.strip(), } def __register_map_files(self): @@ -62,9 +64,7 @@ class Command(BaseCommand): def __check_file(self, path): """Check if map file has been modified.""" fname = os.path.basename(path) - condition = ( - not self.__checksums or - fname not in self.__checksums) + condition = not self.__checksums or fname not in self.__checksums if condition: return True with open(path, mode="rb") as fp: @@ -92,7 +92,9 @@ query = {{ query|safe }} dburl = options.get("dburl") db_settings = ( dj_database_url.config(default=dburl) - if dburl else settings.DATABASES["default"]) + if dburl + else settings.DATABASES["default"] + ) if "sqlite" in db_settings["ENGINE"]: dbtype = "sqlite" elif "postgresql" in db_settings["ENGINE"]: @@ -102,10 +104,11 @@ query = {{ query|safe }} dbhost = db_settings.get("HOST", "127.0.0.1") dbport = db_settings.get("PORT", "") if len(dbport): - dbhost += ':' + dbport + dbhost += ":" + dbport commandline = "{} {}".format( - os.path.basename(sys.argv[0]), " ".join(sys.argv[1:])) + os.path.basename(sys.argv[0]), " ".join(sys.argv[1:]) + ) context = { "date": timezone.now(), "commandline": commandline, @@ -113,19 +116,20 @@ query = {{ query|safe }} "dbuser": db_settings["USER"], "dbpass": db_settings["PASSWORD"], "dbname": db_settings["NAME"], - "dbhost": dbhost + "dbhost": dbhost, } return context - def __render_map_file( - self, mapobject, destdir, context, force_overwrite=False): + def __render_map_file(self, mapobject, destdir, context, force_overwrite=False): """Render a map file.""" fullpath = os.path.join(destdir, mapobject.filename) if os.path.exists(fullpath) and not force_overwrite: if not self.__check_file(fullpath): print( - "Cannot upgrade '{}' map because it has been modified." - .format(mapobject.filename)) + "Cannot upgrade '{}' map because it has been modified.".format( + mapobject.filename + ) + ) return self.__checksums[mapobject.filename] mapcontent = utils.parse_map_file(fullpath) context = copy.deepcopy(context) @@ -139,8 +143,7 @@ query = {{ query|safe }} context["dbhost"] = mapcontent["hosts"] content = self.get_template(context["dbtype"]).render( Context( - dict(list(context.items()), - query=getattr(mapobject, context["dbtype"])) + dict(list(context.items()), query=getattr(mapobject, context["dbtype"])) ) ) fullpath = os.path.join(destdir, mapobject.filename) @@ -161,10 +164,9 @@ query = {{ query|safe }} checksums = {} for mapobject in mapfiles: checksum = self.__render_map_file( - mapobject, destdir, context, - force_overwrite=options["force_overwrite"]) + mapobject, destdir, context, force_overwrite=options["force_overwrite"] + ) checksums[mapobject.filename] = checksum with open(self.__checksums_file, "w") as fp: for fname, checksum in list(checksums.items()): - fp.write("{}:{}:{}\n".format( - fname, context["dbtype"], checksum)) + fp.write("{}:{}:{}\n".format(fname, context["dbtype"], checksum)) diff --git a/modoboa/core/management/commands/load_initial_data.py b/modoboa/core/management/commands/load_initial_data.py index 446dcd78f..3a19086f6 100644 --- a/modoboa/core/management/commands/load_initial_data.py +++ b/modoboa/core/management/commands/load_initial_data.py @@ -22,12 +22,15 @@ class Command(BaseCommand): def add_arguments(self, parser): """Add extra arguments to command.""" parser.add_argument( - "--admin-username", default="admin", - help="Username of the initial super administrator." + "--admin-username", + default="admin", + help="Username of the initial super administrator.", ) parser.add_argument( - "--extra-fixtures", action="store_true", default=False, - help="Also load some fixtures from the admin application." + "--extra-fixtures", + action="store_true", + default=False, + help="Also load some fixtures from the admin application.", ) def handle(self, *args, **options): @@ -35,22 +38,24 @@ class Command(BaseCommand): extensions.exts_pool.load_all() if not models.User.objects.filter(is_superuser=True).count(): - admin = models.User( - username=options["admin_username"], is_superuser=True) + admin = models.User(username=options["admin_username"], is_superuser=True) admin.set_password("password") admin.save() models.ObjectAccess.objects.create( - user=admin, content_object=admin, is_owner=True) + user=admin, content_object=admin, is_owner=True + ) groups = list(constants.PERMISSIONS.keys()) for groupname in groups: group, created = Group.objects.get_or_create(name=groupname) results = signals.extra_role_permissions.send( - sender=self.__class__, role=groupname) + sender=self.__class__, role=groupname + ) permissions = constants.PERMISSIONS.get(groupname, []) if results: permissions += reduce( - lambda a, b: a + b, [result[1] for result in results]) + lambda a, b: a + b, [result[1] for result in results] + ) if not permissions: continue add_permissions_to_group(group, permissions) @@ -61,15 +66,12 @@ class Command(BaseCommand): extension.load_initial_data() except Exception as e: self.stderr.write( - "Unable to load initial data for '{}' ({}).".format( - extname, str(e) - ) + "Unable to load initial data for '{}' ({}).".format(extname, str(e)) ) else: - signals.initial_data_loaded.send( - sender=self.__class__, extname=extname - ) + signals.initial_data_loaded.send(sender=self.__class__, extname=extname) if options["extra_fixtures"]: from modoboa.admin import factories + factories.populate_database() diff --git a/modoboa/core/management/commands/set_default_site.py b/modoboa/core/management/commands/set_default_site.py index 3c77cf2f9..799826f83 100644 --- a/modoboa/core/management/commands/set_default_site.py +++ b/modoboa/core/management/commands/set_default_site.py @@ -9,7 +9,6 @@ from django.core.management.base import BaseCommand, CommandError class Command(BaseCommand): - """Management command to set the default site.""" help = "Set default site (see django.contrib.sites)" # NOQA:A003 diff --git a/modoboa/core/middleware.py b/modoboa/core/middleware.py index 159f1acb9..b9aaef4aa 100644 --- a/modoboa/core/middleware.py +++ b/modoboa/core/middleware.py @@ -24,17 +24,14 @@ class TwoFAMiddleware: def __call__(self, request): user = getattr(request, "user", None) redirect_url = reverse("core:2fa_verify") - url_exceptions = ( - redirect_url, - "/jsi18n/" - ) + url_exceptions = (redirect_url, "/jsi18n/") condition = ( - user and - not user.is_anonymous and - user.tfa_enabled and - not request.path.startswith('/api/') and - request.path not in url_exceptions and - not user.is_verified() + user + and not user.is_anonymous + and user.tfa_enabled + and not request.path.startswith("/api/") + and request.path not in url_exceptions + and not user.is_verified() ) if condition: return http.HttpResponseRedirect(reverse("core:2fa_verify")) diff --git a/modoboa/core/migrations/0001_initial.py b/modoboa/core/migrations/0001_initial.py index b8a7ea543..d0ba5d42c 100644 --- a/modoboa/core/migrations/0001_initial.py +++ b/modoboa/core/migrations/0001_initial.py @@ -6,73 +6,154 @@ from django.conf import settings class Migration(migrations.Migration): dependencies = [ - ('auth', '0001_initial'), - ('contenttypes', '0001_initial'), + ("auth", "0001_initial"), + ("contenttypes", "0001_initial"), ] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(unique=True, max_length=254)), - ('first_name', models.CharField(max_length=30, blank=True)), - ('last_name', models.CharField(max_length=30, blank=True)), - ('email', models.EmailField(max_length=254, blank=True)), - ('is_staff', models.BooleanField(default=False)), - ('is_active', models.BooleanField(default=True)), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now)), - ('is_local', models.BooleanField(default=True)), - ('password', models.CharField(max_length=256, verbose_name='password')), - ('last_login', models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login')), - ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of his/her group.', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ("username", models.CharField(unique=True, max_length=254)), + ("first_name", models.CharField(max_length=30, blank=True)), + ("last_name", models.CharField(max_length=30, blank=True)), + ("email", models.EmailField(max_length=254, blank=True)), + ("is_staff", models.BooleanField(default=False)), + ("is_active", models.BooleanField(default=True)), + ( + "date_joined", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("is_local", models.BooleanField(default=True)), + ("password", models.CharField(max_length=256, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="last login" + ), + ), + ( + "groups", + models.ManyToManyField( + related_query_name="user", + related_name="user_set", + to="auth.Group", + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of his/her group.", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + related_query_name="user", + related_name="user_set", + to="auth.Permission", + blank=True, + help_text="Specific permissions for this user.", + verbose_name="user permissions", + ), + ), ], options={ - 'ordering': ['username'], + "ordering": ["username"], }, bases=(models.Model,), ), migrations.CreateModel( - name='Extension', + name="Extension", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=150)), - ('enabled', models.BooleanField(default=False, help_text='Check to enable this extension', verbose_name='enabled')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=150)), + ( + "enabled", + models.BooleanField( + default=False, + help_text="Check to enable this extension", + verbose_name="enabled", + ), + ), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.CreateModel( - name='Log', + name="Log", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ('message', models.CharField(max_length=255)), - ('level', models.CharField(max_length=15)), - ('logger', models.CharField(max_length=30)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("date_created", models.DateTimeField(auto_now_add=True)), + ("message", models.CharField(max_length=255)), + ("level", models.CharField(max_length=15)), + ("logger", models.CharField(max_length=30)), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.CreateModel( - name='ObjectAccess', + name="ObjectAccess", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('object_id', models.PositiveIntegerField()), - ('is_owner', models.BooleanField(default=False)), - ('content_type', models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE)), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("object_id", models.PositiveIntegerField()), + ("is_owner", models.BooleanField(default=False)), + ( + "content_type", + models.ForeignKey( + to="contenttypes.ContentType", on_delete=models.CASCADE + ), + ), + ( + "user", + models.ForeignKey( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), + ), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.AlterUniqueTogether( - name='objectaccess', - unique_together=set([('user', 'content_type', 'object_id')]), + name="objectaccess", + unique_together=set([("user", "content_type", "object_id")]), ), ] diff --git a/modoboa/core/migrations/0002_delete_extension.py b/modoboa/core/migrations/0002_delete_extension.py index 06ea4b5f1..c43cf011f 100644 --- a/modoboa/core/migrations/0002_delete_extension.py +++ b/modoboa/core/migrations/0002_delete_extension.py @@ -4,11 +4,11 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('core', '0001_initial'), + ("core", "0001_initial"), ] operations = [ migrations.DeleteModel( - name='Extension', + name="Extension", ), ] diff --git a/modoboa/core/migrations/0003_user_master_user.py b/modoboa/core/migrations/0003_user_master_user.py index b31578867..2cac2befc 100644 --- a/modoboa/core/migrations/0003_user_master_user.py +++ b/modoboa/core/migrations/0003_user_master_user.py @@ -4,14 +4,16 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('core', '0002_delete_extension'), + ("core", "0002_delete_extension"), ] operations = [ migrations.AddField( - model_name='user', - name='master_user', - field=models.BooleanField(default=False, verbose_name='Allow mailboxes access'), + model_name="user", + name="master_user", + field=models.BooleanField( + default=False, verbose_name="Allow mailboxes access" + ), preserve_default=True, ), ] diff --git a/modoboa/core/migrations/0004_auto_20150728_1228.py b/modoboa/core/migrations/0004_auto_20150728_1228.py index ffa20cf95..a7fa76773 100644 --- a/modoboa/core/migrations/0004_auto_20150728_1228.py +++ b/modoboa/core/migrations/0004_auto_20150728_1228.py @@ -4,42 +4,46 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('core', '0003_user_master_user'), + ("core", "0003_user_master_user"), ] operations = [ migrations.AlterField( - model_name='user', - name='email', + model_name="user", + name="email", field=models.EmailField(db_index=True, max_length=254, blank=True), preserve_default=True, ), migrations.AlterField( - model_name='user', - name='is_active', + model_name="user", + name="is_active", field=models.BooleanField(default=True, db_index=True), preserve_default=True, ), migrations.AlterField( - model_name='user', - name='is_local', + model_name="user", + name="is_local", field=models.BooleanField(default=True, db_index=True), preserve_default=True, ), migrations.AlterField( - model_name='user', - name='is_staff', + model_name="user", + name="is_staff", field=models.BooleanField(default=False, db_index=True), preserve_default=True, ), migrations.AlterField( - model_name='user', - name='master_user', - field=models.BooleanField(default=False, help_text='Allow this administrator to access user mailboxes', verbose_name='Allow mailboxes access'), + model_name="user", + name="master_user", + field=models.BooleanField( + default=False, + help_text="Allow this administrator to access user mailboxes", + verbose_name="Allow mailboxes access", + ), preserve_default=True, ), migrations.AlterIndexTogether( - name='user', - index_together=set([('email', 'is_active')]), + name="user", + index_together=set([("email", "is_active")]), ), ] diff --git a/modoboa/core/migrations/0005_user_language.py b/modoboa/core/migrations/0005_user_language.py index 849940b58..f4af48272 100644 --- a/modoboa/core/migrations/0005_user_language.py +++ b/modoboa/core/migrations/0005_user_language.py @@ -4,14 +4,31 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('core', '0004_auto_20150728_1228'), + ("core", "0004_auto_20150728_1228"), ] operations = [ migrations.AddField( - model_name='user', - name='language', - field=models.CharField(default=b'en', help_text='Prefered language to display pages.', max_length=10, choices=[(b'cs', '\u010de\u0161tina'), (b'de', 'deutsch'), (b'en', 'english'), (b'es', 'espa\xf1ol'), (b'fr', 'fran\xe7ais'), (b'it', 'italiano'), (b'nl', 'nederlands'), (b'pt_PT', 'portugu\xeas'), (b'pt_BR', 'portugu\xeas (BR)'), (b'ru', '\u0440\u0443\u0441\u0441\u043a\u0438\u0439'), (b'sv', 'svenska')]), + model_name="user", + name="language", + field=models.CharField( + default=b"en", + help_text="Prefered language to display pages.", + max_length=10, + choices=[ + (b"cs", "\u010de\u0161tina"), + (b"de", "deutsch"), + (b"en", "english"), + (b"es", "espa\xf1ol"), + (b"fr", "fran\xe7ais"), + (b"it", "italiano"), + (b"nl", "nederlands"), + (b"pt_PT", "portugu\xeas"), + (b"pt_BR", "portugu\xeas (BR)"), + (b"ru", "\u0440\u0443\u0441\u0441\u043a\u0438\u0439"), + (b"sv", "svenska"), + ], + ), preserve_default=True, ), ] diff --git a/modoboa/core/migrations/0006_auto_20151114_1518.py b/modoboa/core/migrations/0006_auto_20151114_1518.py index 40c125697..8e242c08e 100644 --- a/modoboa/core/migrations/0006_auto_20151114_1518.py +++ b/modoboa/core/migrations/0006_auto_20151114_1518.py @@ -4,20 +4,28 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('core', '0005_user_language'), + ("core", "0005_user_language"), ] operations = [ migrations.AddField( - model_name='user', - name='phone_number', - field=models.CharField(max_length=128, null=True, verbose_name='Phone number', blank=True), + model_name="user", + name="phone_number", + field=models.CharField( + max_length=128, null=True, verbose_name="Phone number", blank=True + ), preserve_default=True, ), migrations.AddField( - model_name='user', - name='secondary_email', - field=models.EmailField(help_text='An alternative e-mail address, can be used for recovery needs.', max_length=254, null=True, verbose_name='Secondary email', blank=True), + model_name="user", + name="secondary_email", + field=models.EmailField( + help_text="An alternative e-mail address, can be used for recovery needs.", + max_length=254, + null=True, + verbose_name="Secondary email", + blank=True, + ), preserve_default=True, ), ] diff --git a/modoboa/core/migrations/0007_auto_20151116_1349.py b/modoboa/core/migrations/0007_auto_20151116_1349.py index e36ddaf1e..13186b62c 100644 --- a/modoboa/core/migrations/0007_auto_20151116_1349.py +++ b/modoboa/core/migrations/0007_auto_20151116_1349.py @@ -5,24 +5,33 @@ import django.contrib.auth.models class Migration(migrations.Migration): dependencies = [ - ('core', '0006_auto_20151114_1518'), + ("core", "0006_auto_20151114_1518"), ] operations = [ migrations.AlterModelManagers( - name='user', + name="user", managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), ], ), migrations.AlterField( - model_name='user', - name='groups', - field=models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups'), + model_name="user", + name="groups", + field=models.ManyToManyField( + related_query_name="user", + related_name="user_set", + to="auth.Group", + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + verbose_name="groups", + ), ), migrations.AlterField( - model_name='user', - name='last_login', - field=models.DateTimeField(null=True, verbose_name='last login', blank=True), + model_name="user", + name="last_login", + field=models.DateTimeField( + null=True, verbose_name="last login", blank=True + ), ), ] diff --git a/modoboa/core/migrations/0008_localconfig.py b/modoboa/core/migrations/0008_localconfig.py index a9df6ff8d..5b2b671ab 100644 --- a/modoboa/core/migrations/0008_localconfig.py +++ b/modoboa/core/migrations/0008_localconfig.py @@ -6,18 +6,31 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('sites', '0002_alter_domain_unique'), - ('core', '0007_auto_20151116_1349'), + ("sites", "0002_alter_domain_unique"), + ("core", "0007_auto_20151116_1349"), ] operations = [ migrations.CreateModel( - name='LocalConfig', + name="LocalConfig", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('api_pk', models.PositiveIntegerField(null=True)), - ('api_versions', models.JSONField()), - ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("api_pk", models.PositiveIntegerField(null=True)), + ("api_versions", models.JSONField()), + ( + "site", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="sites.Site" + ), + ), ], ), ] diff --git a/modoboa/core/migrations/0009_auto_20161026_1003.py b/modoboa/core/migrations/0009_auto_20161026_1003.py index c65124efe..722067d4b 100644 --- a/modoboa/core/migrations/0009_auto_20161026_1003.py +++ b/modoboa/core/migrations/0009_auto_20161026_1003.py @@ -5,23 +5,44 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('core', '0008_localconfig'), + ("core", "0008_localconfig"), ] operations = [ migrations.AlterField( - model_name='user', - name='first_name', - field=models.CharField(blank=True, max_length=30, verbose_name='First name'), + model_name="user", + name="first_name", + field=models.CharField( + blank=True, max_length=30, verbose_name="First name" + ), ), migrations.AlterField( - model_name='user', - name='language', - field=models.CharField(choices=[(b'cs', '\u010de\u0161tina'), (b'de', 'deutsch'), (b'en', 'english'), (b'es', 'espa\xf1ol'), (b'fr', 'fran\xe7ais'), (b'it', 'italiano'), (b'ja_JP', '\u65e5\u672c\u306e'), (b'nl', 'nederlands'), (b'pt_PT', 'portugu\xeas'), (b'pt_BR', 'portugu\xeas (BR)'), (b'ru', '\u0440\u0443\u0441\u0441\u043a\u0438\u0439'), (b'sv', 'svenska')], default=b'en', help_text='Prefered language to display pages.', max_length=10, verbose_name='language'), + model_name="user", + name="language", + field=models.CharField( + choices=[ + (b"cs", "\u010de\u0161tina"), + (b"de", "deutsch"), + (b"en", "english"), + (b"es", "espa\xf1ol"), + (b"fr", "fran\xe7ais"), + (b"it", "italiano"), + (b"ja_JP", "\u65e5\u672c\u306e"), + (b"nl", "nederlands"), + (b"pt_PT", "portugu\xeas"), + (b"pt_BR", "portugu\xeas (BR)"), + (b"ru", "\u0440\u0443\u0441\u0441\u043a\u0438\u0439"), + (b"sv", "svenska"), + ], + default=b"en", + help_text="Prefered language to display pages.", + max_length=10, + verbose_name="language", + ), ), migrations.AlterField( - model_name='user', - name='last_name', - field=models.CharField(blank=True, max_length=30, verbose_name='Last name'), + model_name="user", + name="last_name", + field=models.CharField(blank=True, max_length=30, verbose_name="Last name"), ), ] diff --git a/modoboa/core/migrations/0010_auto_20161026_1011.py b/modoboa/core/migrations/0010_auto_20161026_1011.py index 930f252a9..1fb4c3566 100644 --- a/modoboa/core/migrations/0010_auto_20161026_1011.py +++ b/modoboa/core/migrations/0010_auto_20161026_1011.py @@ -42,22 +42,22 @@ def clear_parameters(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('core', '0009_auto_20161026_1003'), - ('lib', '0005_auto_20160416_1449'), + ("core", "0009_auto_20161026_1003"), + ("lib", "0005_auto_20160416_1449"), ] operations = [ migrations.AddField( - model_name='localconfig', - name='_parameters', + model_name="localconfig", + name="_parameters", field=models.JSONField(default=dict), preserve_default=False, ), migrations.AddField( - model_name='user', - name='_parameters', + model_name="user", + name="_parameters", field=models.JSONField(default=dict), preserve_default=False, ), - migrations.RunPython(move_parameters, clear_parameters) + migrations.RunPython(move_parameters, clear_parameters), ] diff --git a/modoboa/core/migrations/0011_auto_20161104_2217.py b/modoboa/core/migrations/0011_auto_20161104_2217.py index f8b165155..fb06b7de7 100644 --- a/modoboa/core/migrations/0011_auto_20161104_2217.py +++ b/modoboa/core/migrations/0011_auto_20161104_2217.py @@ -5,18 +5,18 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('core', '0010_auto_20161026_1011'), + ("core", "0010_auto_20161026_1011"), ] operations = [ migrations.AlterField( - model_name='localconfig', - name='_parameters', + model_name="localconfig", + name="_parameters", field=models.JSONField(default=dict), ), migrations.AlterField( - model_name='user', - name='_parameters', + model_name="user", + name="_parameters", field=models.JSONField(default=dict), ), ] diff --git a/modoboa/core/migrations/0012_auto_20170215_1034.py b/modoboa/core/migrations/0012_auto_20170215_1034.py index 0adfd9a97..e21975f51 100644 --- a/modoboa/core/migrations/0012_auto_20170215_1034.py +++ b/modoboa/core/migrations/0012_auto_20170215_1034.py @@ -7,13 +7,14 @@ def rename_settings(apps, schema_editor): LocalConfig = apps.get_model("core", "LocalConfig") lc = LocalConfig.objects.first() condition = ( - lc and - "admin" in lc._parameters and - "default_domain_quota" in lc._parameters["admin"] + lc + and "admin" in lc._parameters + and "default_domain_quota" in lc._parameters["admin"] ) if condition: - lc._parameters["admin"]["default_mailbox_quota"] = ( - lc._parameters["admin"]["default_domain_quota"]) + lc._parameters["admin"]["default_mailbox_quota"] = lc._parameters["admin"][ + "default_domain_quota" + ] del lc._parameters["admin"]["default_domain_quota"] lc.save(update_fields=["_parameters"]) @@ -21,9 +22,7 @@ def rename_settings(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('core', '0011_auto_20161104_2217'), + ("core", "0011_auto_20161104_2217"), ] - operations = [ - migrations.RunPython(rename_settings) - ] + operations = [migrations.RunPython(rename_settings)] diff --git a/modoboa/core/migrations/0013_auto_20170707_1058.py b/modoboa/core/migrations/0013_auto_20170707_1058.py index c6ecee2dc..28335393f 100644 --- a/modoboa/core/migrations/0013_auto_20170707_1058.py +++ b/modoboa/core/migrations/0013_auto_20170707_1058.py @@ -6,23 +6,27 @@ import django.utils.timezone class Migration(migrations.Migration): dependencies = [ - ('core', '0012_auto_20170215_1034'), + ("core", "0012_auto_20170215_1034"), ] operations = [ migrations.AlterField( - model_name='user', - name='date_joined', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined'), + model_name="user", + name="date_joined", + field=models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), ), migrations.AlterField( - model_name='user', - name='first_name', - field=models.CharField(blank=True, max_length=30, verbose_name='first name'), + model_name="user", + name="first_name", + field=models.CharField( + blank=True, max_length=30, verbose_name="first name" + ), ), migrations.AlterField( - model_name='user', - name='last_name', - field=models.CharField(blank=True, max_length=30, verbose_name='last name'), + model_name="user", + name="last_name", + field=models.CharField(blank=True, max_length=30, verbose_name="last name"), ), ] diff --git a/modoboa/core/migrations/0014_auto_20171010_1746.py b/modoboa/core/migrations/0014_auto_20171010_1746.py index 5d7fd85af..6f162b0fa 100644 --- a/modoboa/core/migrations/0014_auto_20171010_1746.py +++ b/modoboa/core/migrations/0014_auto_20171010_1746.py @@ -23,7 +23,7 @@ def forward(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('core', '0013_auto_20170707_1058'), + ("core", "0013_auto_20170707_1058"), ] operations = [ diff --git a/modoboa/core/migrations/0015_auto_20180112_1642.py b/modoboa/core/migrations/0015_auto_20180112_1642.py index a3aaea97b..ce9dd3f50 100644 --- a/modoboa/core/migrations/0015_auto_20180112_1642.py +++ b/modoboa/core/migrations/0015_auto_20180112_1642.py @@ -5,13 +5,34 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('core', '0014_auto_20171010_1746'), + ("core", "0014_auto_20171010_1746"), ] operations = [ migrations.AlterField( - model_name='user', - name='language', - field=models.CharField(choices=[('cs', '\u010de\u0161tina'), ('de', 'deutsch'), ('en', 'english'), ('el_GR', '\u03b5\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac'), ('es', 'espa\xf1ol'), ('fr', 'fran\xe7ais'), ('it', 'italiano'), ('ja_JP', '\u65e5\u672c\u306e'), ('nl', 'nederlands'), ('pt_PT', 'portugu\xeas'), ('pt_BR', 'portugu\xeas (BR)'), ('pl_PL', 'polski'), ('ru', '\u0440\u0443\u0441\u0441\u043a\u0438\u0439'), ('sv', 'svenska')], default='en', help_text='Prefered language to display pages.', max_length=10, verbose_name='language'), + model_name="user", + name="language", + field=models.CharField( + choices=[ + ("cs", "\u010de\u0161tina"), + ("de", "deutsch"), + ("en", "english"), + ("el_GR", "\u03b5\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac"), + ("es", "espa\xf1ol"), + ("fr", "fran\xe7ais"), + ("it", "italiano"), + ("ja_JP", "\u65e5\u672c\u306e"), + ("nl", "nederlands"), + ("pt_PT", "portugu\xeas"), + ("pt_BR", "portugu\xeas (BR)"), + ("pl_PL", "polski"), + ("ru", "\u0440\u0443\u0441\u0441\u043a\u0438\u0439"), + ("sv", "svenska"), + ], + default="en", + help_text="Prefered language to display pages.", + max_length=10, + verbose_name="language", + ), ), ] diff --git a/modoboa/core/migrations/0016_auto_20180713_1014.py b/modoboa/core/migrations/0016_auto_20180713_1014.py index 9a98017a0..683632868 100644 --- a/modoboa/core/migrations/0016_auto_20180713_1014.py +++ b/modoboa/core/migrations/0016_auto_20180713_1014.py @@ -5,13 +5,36 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('core', '0015_auto_20180112_1642'), + ("core", "0015_auto_20180112_1642"), ] operations = [ migrations.AlterField( - model_name='user', - name='language', - field=models.CharField(choices=[('cs', '\u010de\u0161tina'), ('de', 'deutsch'), ('en', 'english'), ('el_GR', '\u03b5\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac'), ('es', 'espa\xf1ol'), ('fr', 'fran\xe7ais'), ('it', 'italiano'), ('ja_JP', '\u65e5\u672c\u306e'), ('nl', 'nederlands'), ('pt_PT', 'portugu\xeas'), ('pt_BR', 'portugu\xeas (BR)'), ('pl_PL', 'polski'), ('ro_RO', 'Rom\xe2n\u0103'), ('ru', '\u0440\u0443\u0441\u0441\u043a\u0438\u0439'), ('sv', 'svenska'), ('zh_TW', '\u4e2d\u6587\uff08\u53f0\u7063\uff09')], default='en', help_text='Prefered language to display pages.', max_length=10, verbose_name='language'), + model_name="user", + name="language", + field=models.CharField( + choices=[ + ("cs", "\u010de\u0161tina"), + ("de", "deutsch"), + ("en", "english"), + ("el_GR", "\u03b5\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac"), + ("es", "espa\xf1ol"), + ("fr", "fran\xe7ais"), + ("it", "italiano"), + ("ja_JP", "\u65e5\u672c\u306e"), + ("nl", "nederlands"), + ("pt_PT", "portugu\xeas"), + ("pt_BR", "portugu\xeas (BR)"), + ("pl_PL", "polski"), + ("ro_RO", "Rom\xe2n\u0103"), + ("ru", "\u0440\u0443\u0441\u0441\u043a\u0438\u0439"), + ("sv", "svenska"), + ("zh_TW", "\u4e2d\u6587\uff08\u53f0\u7063\uff09"), + ], + default="en", + help_text="Prefered language to display pages.", + max_length=10, + verbose_name="language", + ), ), ] diff --git a/modoboa/core/migrations/0017_auto_20190705_1045.py b/modoboa/core/migrations/0017_auto_20190705_1045.py index 530abdd41..676789195 100644 --- a/modoboa/core/migrations/0017_auto_20190705_1045.py +++ b/modoboa/core/migrations/0017_auto_20190705_1045.py @@ -5,18 +5,43 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('core', '0016_auto_20180713_1014'), + ("core", "0016_auto_20180713_1014"), ] operations = [ migrations.AlterField( - model_name='log', - name='message', + model_name="log", + name="message", field=models.TextField(), ), migrations.AlterField( - model_name='user', - name='language', - field=models.CharField(choices=[('br', 'breton'), ('cs', 'čeština'), ('de', 'deutsch'), ('en', 'english'), ('el_GR', 'ελληνικά'), ('es', 'español'), ('fr', 'français'), ('it', 'italiano'), ('ja_JP', '日本の'), ('nl', 'nederlands'), ('pt_PT', 'português'), ('pt_BR', 'português (BR)'), ('pl_PL', 'polski'), ('ro_RO', 'Română'), ('ru', 'русский'), ('sv', 'svenska'), ('tr_TR', 'türk'), ('zh_TW', '中文(台灣)')], default='en', help_text='Prefered language to display pages.', max_length=10, verbose_name='language'), + model_name="user", + name="language", + field=models.CharField( + choices=[ + ("br", "breton"), + ("cs", "čeština"), + ("de", "deutsch"), + ("en", "english"), + ("el_GR", "ελληνικά"), + ("es", "español"), + ("fr", "français"), + ("it", "italiano"), + ("ja_JP", "日本の"), + ("nl", "nederlands"), + ("pt_PT", "português"), + ("pt_BR", "português (BR)"), + ("pl_PL", "polski"), + ("ro_RO", "Română"), + ("ru", "русский"), + ("sv", "svenska"), + ("tr_TR", "türk"), + ("zh_TW", "中文(台灣)"), + ], + default="en", + help_text="Prefered language to display pages.", + max_length=10, + verbose_name="language", + ), ), ] diff --git a/modoboa/core/migrations/0018_auto_20200104_1714.py b/modoboa/core/migrations/0018_auto_20200104_1714.py index c2789e28f..5ced6ee8e 100644 --- a/modoboa/core/migrations/0018_auto_20200104_1714.py +++ b/modoboa/core/migrations/0018_auto_20200104_1714.py @@ -6,13 +6,15 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('core', '0017_auto_20190705_1045'), + ("core", "0017_auto_20190705_1045"), ] operations = [ migrations.AlterField( - model_name='user', - name='last_name', - field=models.CharField(blank=True, max_length=150, verbose_name='last name'), + model_name="user", + name="last_name", + field=models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), ), ] diff --git a/modoboa/core/migrations/0019_auto_20200121_1659.py b/modoboa/core/migrations/0019_auto_20200121_1659.py index 131a4b231..43d364469 100644 --- a/modoboa/core/migrations/0019_auto_20200121_1659.py +++ b/modoboa/core/migrations/0019_auto_20200121_1659.py @@ -8,20 +8,28 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('core', '0018_auto_20200104_1714'), + ("core", "0018_auto_20200104_1714"), ] operations = [ migrations.CreateModel( - name='ExtensionUpdateHistory', + name="ExtensionUpdateHistory", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('extension', models.CharField(max_length=100)), - ('version', models.CharField(max_length=30)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("extension", models.CharField(max_length=100)), + ("version", models.CharField(max_length=30)), ], ), migrations.AlterUniqueTogether( - name='extensionupdatehistory', - unique_together=set([('extension', 'version')]), + name="extensionupdatehistory", + unique_together=set([("extension", "version")]), ), ] diff --git a/modoboa/core/migrations/0020_auto_20200421_0851.py b/modoboa/core/migrations/0020_auto_20200421_0851.py index 1acbe11be..4def6021c 100644 --- a/modoboa/core/migrations/0020_auto_20200421_0851.py +++ b/modoboa/core/migrations/0020_auto_20200421_0851.py @@ -7,13 +7,19 @@ import phonenumber_field.modelfields class Migration(migrations.Migration): dependencies = [ - ('core', '0019_auto_20200121_1659'), + ("core", "0019_auto_20200121_1659"), ] operations = [ migrations.AlterField( - model_name='user', - name='phone_number', - field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None, verbose_name='Phone number'), + model_name="user", + name="phone_number", + field=phonenumber_field.modelfields.PhoneNumberField( + blank=True, + max_length=128, + null=True, + region=None, + verbose_name="Phone number", + ), ), ] diff --git a/modoboa/core/migrations/0021_localconfig_need_dovecot_update.py b/modoboa/core/migrations/0021_localconfig_need_dovecot_update.py index 152e07e71..e43629aa0 100644 --- a/modoboa/core/migrations/0021_localconfig_need_dovecot_update.py +++ b/modoboa/core/migrations/0021_localconfig_need_dovecot_update.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('core', '0020_auto_20200421_0851'), + ("core", "0020_auto_20200421_0851"), ] operations = [ migrations.AddField( - model_name='localconfig', - name='need_dovecot_update', + model_name="localconfig", + name="need_dovecot_update", field=models.BooleanField(default=False), ), ] diff --git a/modoboa/core/migrations/0022_user_tfa_enabled.py b/modoboa/core/migrations/0022_user_tfa_enabled.py index c97496498..484f73cc4 100644 --- a/modoboa/core/migrations/0022_user_tfa_enabled.py +++ b/modoboa/core/migrations/0022_user_tfa_enabled.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('core', '0021_localconfig_need_dovecot_update'), + ("core", "0021_localconfig_need_dovecot_update"), ] operations = [ migrations.AddField( - model_name='user', - name='tfa_enabled', + model_name="user", + name="tfa_enabled", field=models.BooleanField(default=False), ), ] diff --git a/modoboa/core/migrations/0023_auto_20221012_0911.py b/modoboa/core/migrations/0023_auto_20221012_0911.py index 65a4d6699..bd92c1f80 100644 --- a/modoboa/core/migrations/0023_auto_20221012_0911.py +++ b/modoboa/core/migrations/0023_auto_20221012_0911.py @@ -6,18 +6,45 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('core', '0022_user_tfa_enabled'), + ("core", "0022_user_tfa_enabled"), ] operations = [ migrations.AlterField( - model_name='user', - name='first_name', - field=models.CharField(blank=True, max_length=150, verbose_name='first name'), + model_name="user", + name="first_name", + field=models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), ), migrations.AlterField( - model_name='user', - name='language', - field=models.CharField(choices=[('br', 'breton'), ('cs', 'čeština'), ('de', 'deutsch'), ('en', 'english'), ('el', 'ελληνικά'), ('es', 'español'), ('fr', 'français'), ('it', 'italiano'), ('ja', '日本の'), ('nl', 'nederlands'), ('pt', 'português'), ('pt-br', 'português (BR)'), ('pl', 'polski'), ('ro', 'Română'), ('ru', 'русский'), ('sv', 'svenska'), ('tr', 'türk'), ('zh-hant', '中文(台灣)')], default='en', help_text='Prefered language to display pages.', max_length=10, verbose_name='language'), + model_name="user", + name="language", + field=models.CharField( + choices=[ + ("br", "breton"), + ("cs", "čeština"), + ("de", "deutsch"), + ("en", "english"), + ("el", "ελληνικά"), + ("es", "español"), + ("fr", "français"), + ("it", "italiano"), + ("ja", "日本の"), + ("nl", "nederlands"), + ("pt", "português"), + ("pt-br", "português (BR)"), + ("pl", "polski"), + ("ro", "Română"), + ("ru", "русский"), + ("sv", "svenska"), + ("tr", "türk"), + ("zh-hant", "中文(台灣)"), + ], + default="en", + help_text="Prefered language to display pages.", + max_length=10, + verbose_name="language", + ), ), ] diff --git a/modoboa/core/migrations/0024_alter_user_language.py b/modoboa/core/migrations/0024_alter_user_language.py index f4034db2d..a46a37daa 100644 --- a/modoboa/core/migrations/0024_alter_user_language.py +++ b/modoboa/core/migrations/0024_alter_user_language.py @@ -6,13 +6,39 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('core', '0023_auto_20221012_0911'), + ("core", "0023_auto_20221012_0911"), ] operations = [ migrations.AlterField( - model_name='user', - name='language', - field=models.CharField(choices=[('br', 'breton'), ('cs', 'čeština'), ('de', 'deutsch'), ('en', 'english'), ('el', 'ελληνικά'), ('es', 'español'), ('fi', 'Suomalainen'), ('fr', 'français'), ('it', 'italiano'), ('ja', '日本の'), ('nl', 'nederlands'), ('pt', 'português'), ('pt-br', 'português (BR)'), ('pl', 'polski'), ('ro', 'Română'), ('ru', 'русский'), ('sv', 'svenska'), ('tr', 'türk'), ('zh-hant', '中文(台灣)')], default='en', help_text='Prefered language to display pages.', max_length=10, verbose_name='language'), + model_name="user", + name="language", + field=models.CharField( + choices=[ + ("br", "breton"), + ("cs", "čeština"), + ("de", "deutsch"), + ("en", "english"), + ("el", "ελληνικά"), + ("es", "español"), + ("fi", "Suomalainen"), + ("fr", "français"), + ("it", "italiano"), + ("ja", "日本の"), + ("nl", "nederlands"), + ("pt", "português"), + ("pt-br", "português (BR)"), + ("pl", "polski"), + ("ro", "Română"), + ("ru", "русский"), + ("sv", "svenska"), + ("tr", "türk"), + ("zh-hant", "中文(台灣)"), + ], + default="en", + help_text="Prefered language to display pages.", + max_length=10, + verbose_name="language", + ), ), ] diff --git a/modoboa/core/migrations/0026_alter_localconfig__parameters_and_more.py b/modoboa/core/migrations/0026_alter_localconfig__parameters_and_more.py index f3ad50026..aed150f30 100644 --- a/modoboa/core/migrations/0026_alter_localconfig__parameters_and_more.py +++ b/modoboa/core/migrations/0026_alter_localconfig__parameters_and_more.py @@ -7,43 +7,49 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('core', '0025_rename_user_email_is_active_core_user_email_c0c03f_idx'), + ("core", "0025_rename_user_email_is_active_core_user_email_c0c03f_idx"), ] operations = [ migrations.AlterField( - model_name='localconfig', - name='_parameters', + model_name="localconfig", + name="_parameters", field=models.JSONField(default=dict), ), migrations.AlterField( - model_name='localconfig', - name='api_versions', + model_name="localconfig", + name="api_versions", field=models.JSONField(), ), migrations.AlterField( - model_name='user', - name='_parameters', + model_name="user", + name="_parameters", field=models.JSONField(default=dict), ), migrations.AlterField( - model_name='localconfig', - name='api_versions', + model_name="localconfig", + name="api_versions", field=models.JSONField(default=dict), ), migrations.AlterField( - model_name='localconfig', - name='_parameters', - field=models.JSONField(default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + model_name="localconfig", + name="_parameters", + field=models.JSONField( + default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder + ), ), migrations.AlterField( - model_name='localconfig', - name='api_versions', - field=models.JSONField(default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + model_name="localconfig", + name="api_versions", + field=models.JSONField( + default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder + ), ), migrations.AlterField( - model_name='user', - name='_parameters', - field=models.JSONField(default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + model_name="user", + name="_parameters", + field=models.JSONField( + default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder + ), ), ] diff --git a/modoboa/core/mocks.py b/modoboa/core/mocks.py index 6cc508470..eaf276d36 100644 --- a/modoboa/core/mocks.py +++ b/modoboa/core/mocks.py @@ -6,48 +6,42 @@ import httmock @httmock.urlmatch( - netloc=r"api\.modoboa\.org$", path=r"^/1/instances/search/", method="post") + netloc=r"api\.modoboa\.org$", path=r"^/1/instances/search/", method="post" +) def modo_api_instance_search(url, request): """Return empty response.""" return {"status_code": 404} -@httmock.urlmatch( - netloc=r"api\.modoboa\.org$", path=r"^/1/instances/", method="post") +@httmock.urlmatch(netloc=r"api\.modoboa\.org$", path=r"^/1/instances/", method="post") def modo_api_instance_create(url, request): """Simulate successful creation.""" - return { - "status_code": 201, - "content": {"pk": 100} - } + return {"status_code": 201, "content": {"pk": 100}} -@httmock.urlmatch( - netloc=r"api\.modoboa\.org$", path=r"^/1/instances/.+/", method="put") +@httmock.urlmatch(netloc=r"api\.modoboa\.org$", path=r"^/1/instances/.+/", method="put") def modo_api_instance_update(url, request): """Simulate successful update.""" return {"status_code": 200} -@httmock.urlmatch( - netloc=r"api\.modoboa\.org$", path=r"^/1/versions/", method="get") +@httmock.urlmatch(netloc=r"api\.modoboa\.org$", path=r"^/1/versions/", method="get") def modo_api_versions(url, request): """Simulate versions check.""" return { "status_code": 200, "content": [ {"name": "modoboa", "version": "9.0.0", "url": ""}, - ] + ], } -@httmock.urlmatch( - netloc=r"api\.modoboa\.org$", path=r"^/1/versions/", method="get") +@httmock.urlmatch(netloc=r"api\.modoboa\.org$", path=r"^/1/versions/", method="get") def modo_api_versions_no_update(url, request): """Simulate versions check.""" return { "status_code": 200, "content": [ {"name": "modoboa", "version": "0.0.0", "url": ""}, - ] + ], } diff --git a/modoboa/core/models.py b/modoboa/core/models.py index 5b86c2d6d..6aa8466f2 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -20,20 +20,23 @@ from reversion import revisions as reversion from modoboa.core.password_hashers import get_password_hasher from modoboa.lib.exceptions import ( - BadRequest, Conflict, InternalError, PermDeniedException + BadRequest, + Conflict, + InternalError, + PermDeniedException, ) from modoboa.parameters import tools as param_tools from . import constants, signals try: from modoboa.lib.ldap_utils import LDAPAuthBackend + ldap_available = True except ImportError: ldap_available = False class User(AbstractUser): - """Custom User model. It overloads the way passwords are stored into the database. The @@ -49,27 +52,28 @@ class User(AbstractUser): is_active = models.BooleanField(default=True, db_index=True) is_local = models.BooleanField(default=True, db_index=True) master_user = models.BooleanField( - gettext_lazy("Allow mailboxes access"), default=False, - help_text=gettext_lazy( - "Allow this administrator to access user mailboxes" - ) + gettext_lazy("Allow mailboxes access"), + default=False, + help_text=gettext_lazy("Allow this administrator to access user mailboxes"), ) password = models.CharField(gettext_lazy("password"), max_length=256) language = models.CharField( gettext_lazy("language"), - max_length=10, default="en", choices=constants.LANGUAGES, - help_text=gettext_lazy( - "Prefered language to display pages." - ) + max_length=10, + default="en", + choices=constants.LANGUAGES, + help_text=gettext_lazy("Prefered language to display pages."), ) - phone_number = PhoneNumberField( - gettext_lazy("Phone number"), blank=True, null=True) + phone_number = PhoneNumberField(gettext_lazy("Phone number"), blank=True, null=True) secondary_email = models.EmailField( - gettext_lazy("Secondary email"), max_length=254, - blank=True, null=True, + gettext_lazy("Secondary email"), + max_length=254, + blank=True, + null=True, help_text=gettext_lazy( - "An alternative e-mail address, can be used for recovery needs.") + "An alternative e-mail address, can be used for recovery needs." + ), ) tfa_enabled = models.BooleanField(default=False) @@ -80,7 +84,7 @@ class User(AbstractUser): ordering = ["username"] indexes = [models.Index(fields=["email", "is_active"])] - password_expr = re.compile(r'\{([\w\-]+)\}(.+)') + password_expr = re.compile(r"\{([\w\-]+)\}(.+)") def __init__(self, *args, **kwargs): """Load parameter manager.""" @@ -96,12 +100,15 @@ class User(AbstractUser): """ scheme = param_tools.get_global_parameter( - "password_scheme", raise_exception=False) + "password_scheme", raise_exception=False + ) if scheme is None: from modoboa.core.apps import load_core_settings + load_core_settings() scheme = param_tools.get_global_parameter( - "password_scheme", raise_exception=False) + "password_scheme", raise_exception=False + ) raw_value = smart_bytes(raw_value) return get_password_hasher(scheme.upper())().encrypt(raw_value) @@ -123,12 +130,13 @@ class User(AbstractUser): raise InternalError( _("Failed to update password: LDAP module not installed") ) - LDAPAuthBackend().update_user_password( - self.username, curvalue, raw_value - ) + LDAPAuthBackend().update_user_password(self.username, curvalue, raw_value) signals.account_password_updated.send( sender=self.__class__, - account=self, password=raw_value, created=self.pk is None) + account=self, + password=raw_value, + created=self.pk is None, + ) def check_password(self, raw_value): """Compare raw_value to current password.""" @@ -154,9 +162,10 @@ class User(AbstractUser): @property def tags(self): - return [{"name": "account", "label": _("account"), "type": "idt"}, - {"name": self.role, "label": self.role, - "type": "grp", "color": "info"}] + return [ + {"name": "account", "label": _("account"), "type": "idt"}, + {"name": self.role, "label": self.role, "type": "grp", "color": "info"}, + ] @property def fullname(self): @@ -187,7 +196,8 @@ class User(AbstractUser): def encoded_address(self): if self.first_name != "" or self.last_name != "": return '"{}" <{}>'.format( - Header(self.fullname, "utf8").encode(), self.email) + Header(self.fullname, "utf8").encode(), self.email + ) return self.email def is_owner(self, obj): @@ -198,8 +208,7 @@ class User(AbstractUser): """ ct = ContentType.objects.get_for_model(obj) try: - ooentry = self.objectaccess_set.get( - content_type=ct, object_id=obj.id) + ooentry = self.objectaccess_set.get(content_type=ct, object_id=obj.id) except ObjectAccess.DoesNotExist: return False return ooentry.is_owner @@ -220,8 +229,7 @@ class User(AbstractUser): ct = ContentType.objects.get_for_model(obj) try: - ooentry = self.objectaccess_set.get( - content_type=ct, object_id=obj.id) + ooentry = self.objectaccess_set.get(content_type=ct, object_id=obj.id) except ObjectAccess.DoesNotExist: pass else: @@ -258,7 +266,8 @@ class User(AbstractUser): if role is None or self.role == role: return signals.account_role_changed.send( - sender=self.__class__, account=self, role=role) + sender=self.__class__, account=self, role=role + ) self.groups.clear() if role == "SuperAdmins": self.is_superuser = True @@ -272,6 +281,7 @@ class User(AbstractUser): self.groups.add(Group.objects.get(name="SimpleUsers")) if role != "SimpleUsers" and not self.can_access(self): from modoboa.lib.permissions import grant_access_to_object + grant_access_to_object(self, self) self.save() self._role = role @@ -291,6 +301,7 @@ class User(AbstractUser): def post_create(self, creator): """Grant permission on this user to creator.""" from modoboa.lib.permissions import grant_access_to_object + grant_access_to_object(creator, self, is_owner=True) def save(self, *args, **kwargs): @@ -322,10 +333,9 @@ class User(AbstractUser): allowed_roles = get_account_roles(user) allowed_roles = [role[0] for role in allowed_roles] if desired_role not in allowed_roles: - raise PermDeniedException(_( - "You can't import an account with a role greater than " - "yours" - )) + raise PermDeniedException( + _("You can't import an account with a role greater than " "yours") + ) self.username = row[1].strip().lower() try: @@ -338,13 +348,17 @@ class User(AbstractUser): if desired_role == "SimpleUsers": if len(row) < 8 or not row[7].strip(): raise BadRequest( - _("The simple user '%s' must have a valid email address" - % self.username) + _( + "The simple user '%s' must have a valid email address" + % self.username + ) ) if self.username != row[7].strip(): raise BadRequest( - _("username and email fields must not differ for '%s'" - % self.username) + _( + "username and email fields must not differ for '%s'" + % self.username + ) ) if crypt_password: @@ -353,7 +367,7 @@ class User(AbstractUser): self.password = row[2].strip() self.first_name = row[3].strip() self.last_name = row[4].strip() - self.is_active = (row[5].strip().lower() in ["true", "1", "yes", "y"]) + self.is_active = row[5].strip().lower() in ["true", "1", "yes", "y"] self.language = settings.LANGUAGE_CODE self.save() self.role = desired_role @@ -361,7 +375,8 @@ class User(AbstractUser): if len(row) < 8: return signals.account_imported.send( - sender=self.__class__, user=user, account=self, row=row[7:]) + sender=self.__class__, user=user, account=self, row=row[7:] + ) def to_csv_row(self): """Return row that can be included in a CSV file.""" @@ -373,10 +388,9 @@ class User(AbstractUser): smart_str(self.last_name), smart_str(self.is_active), smart_str(self.role), - smart_str(self.email) + smart_str(self.email), ] - results = signals.account_exported.send( - sender=self.__class__, user=self) + results = signals.account_exported.send(sender=self.__class__, user=self) for result in results: row += result[1] return row @@ -411,8 +425,7 @@ def populate_callback(user, group="SimpleUsers"): user.post_create(sadmins[0]) for su in sadmins[1:]: grant_access_to_object(su, user) - signals.account_auto_created.send( - sender="populate_callback", user=user) + signals.account_auto_created.send(sender="populate_callback", user=user) class ObjectAccess(models.Model): @@ -426,9 +439,7 @@ class ObjectAccess(models.Model): unique_together = (("user", "content_type", "object_id"),) def __str__(self): - return "%s => %s (%s)" % ( - self.user, self.content_object, self.content_type - ) + return "%s => %s (%s)" % (self.user, self.content_object, self.content_type) class Log(models.Model): diff --git a/modoboa/core/password_hashers/__init__.py b/modoboa/core/password_hashers/__init__.py index a0e29dda8..5bc8bd086 100644 --- a/modoboa/core/password_hashers/__init__.py +++ b/modoboa/core/password_hashers/__init__.py @@ -3,11 +3,19 @@ Password hashers for Modoboa. """ from modoboa.core.password_hashers.advanced import ( # NOQA:F401 - BLFCRYPTHasher, MD5CRYPTHasher, SHA256CRYPTHasher, SHA512CRYPTHasher, - ARGON2IDHasher, SSHAHasher + BLFCRYPTHasher, + MD5CRYPTHasher, + SHA256CRYPTHasher, + SHA512CRYPTHasher, + ARGON2IDHasher, + SSHAHasher, ) from modoboa.core.password_hashers.base import ( # NOQA:F401 - CRYPTHasher, MD5Hasher, PLAINHasher, SHA256Hasher, PasswordHasher + CRYPTHasher, + MD5Hasher, + PLAINHasher, + SHA256Hasher, + PasswordHasher, ) diff --git a/modoboa/core/password_hashers/advanced.py b/modoboa/core/password_hashers/advanced.py index 721c54f6f..8b44d0367 100644 --- a/modoboa/core/password_hashers/advanced.py +++ b/modoboa/core/password_hashers/advanced.py @@ -52,6 +52,7 @@ class MD5CRYPTHasher(PasswordHasher): This scheme can't be considered as secure anymore. """ + _weak = True @property @@ -117,6 +118,7 @@ class SSHAHasher(PasswordHasher): """ Salted SHA1 password hasher. """ + _weak = True @property @@ -129,10 +131,11 @@ class SSHAHasher(PasswordHasher): def verify(self, clearvalue, hashed_value): # We have to add the scheme for the hashed_value to be validated - return ldap_salted_sha1.verify(clearvalue, f'{{SSHA}}{hashed_value}') + return ldap_salted_sha1.verify(clearvalue, f"{{SSHA}}{hashed_value}") if argon2_hasher is not None: + class ARGON2IDHasher(PasswordHasher): """ argon2 password hasher. @@ -142,7 +145,9 @@ if argon2_hasher is not None: but can only be used with dovecot >= 2.3 and libsodium >= 1.0.13 """ - def __init__(self,): + def __init__( + self, + ): super(ARGON2IDHasher, self).__init__() parameters = dict() @@ -175,9 +180,9 @@ if argon2_hasher is not None: return False def needs_rehash(self, hashed_value): - return self.hasher.check_needs_rehash( - hashed_value.strip(self.scheme) - ) + return self.hasher.check_needs_rehash(hashed_value.strip(self.scheme)) + else: + class ARGON2IDHasher: pass diff --git a/modoboa/core/password_hashers/base.py b/modoboa/core/password_hashers/base.py index 2b2bba139..0d355ac63 100644 --- a/modoboa/core/password_hashers/base.py +++ b/modoboa/core/password_hashers/base.py @@ -19,10 +19,11 @@ class MetaHasher(type): PasswordHasher Metaclass Allow classmethod to be properties """ + @property def name(cls): """Returns the name of the hasher""" - return cls.__name__.rstrip('Hasher').lower() + return cls.__name__.rstrip("Hasher").lower() @property def label(cls): @@ -34,6 +35,7 @@ class PasswordHasher(metaclass=MetaHasher): """ Base class of all hashers. """ + _weak = False def __init__(self, target="local"): @@ -76,8 +78,7 @@ class PasswordHasher(metaclass=MetaHasher): :return: True if passwords match, False otherwise """ return constant_time_compare( - self._b64encode(self._encrypt(clearvalue, hashed_value)), - hashed_value + self._b64encode(self._encrypt(clearvalue, hashed_value)), hashed_value ) def needs_rehash(self, hashed_value): @@ -100,6 +101,7 @@ class PLAINHasher(PasswordHasher): """ Plain (ie. clear) password hasher. """ + _weak = True @property @@ -116,6 +118,7 @@ class CRYPTHasher(PasswordHasher): Uses python `crypt` standard module. """ + _weak = True @property @@ -124,13 +127,7 @@ class CRYPTHasher(PasswordHasher): def _encrypt(self, clearvalue, salt=None): if salt is None: - salt = "".join( - Random().sample( - string.ascii_letters + - string.digits, - 2 - ) - ) + salt = "".join(Random().sample(string.ascii_letters + string.digits, 2)) return crypt.crypt(clearvalue, salt) @@ -140,6 +137,7 @@ class MD5Hasher(PasswordHasher): Uses python `hashlib` standard module. """ + _weak = True @property @@ -157,6 +155,7 @@ class SHA256Hasher(PasswordHasher): Uses python `hashlib` and `base64` standard modules. """ + _weak = True @property diff --git a/modoboa/core/password_hashers/utils.py b/modoboa/core/password_hashers/utils.py index 356c77823..25b1155b4 100644 --- a/modoboa/core/password_hashers/utils.py +++ b/modoboa/core/password_hashers/utils.py @@ -42,12 +42,10 @@ def get_dovecot_schemes(): status = 0 # Manage alarms if needed - doveadm_alarms = Alarm.objects.filter( - internal_name=DOVEADM_PASS_SCHEME_ALARM) + doveadm_alarms = Alarm.objects.filter(internal_name=DOVEADM_PASS_SCHEME_ALARM) doveadm_alarm = doveadm_alarms.first() if status == 0: - condition = (doveadm_alarm is not None and - doveadm_alarm.status == ALARM_OPENED) + condition = doveadm_alarm is not None and doveadm_alarm.status == ALARM_OPENED if condition: doveadm_alarm.close() elif status == 2: @@ -57,21 +55,19 @@ def get_dovecot_schemes(): else: Alarm.objects.create( title="Failed to retrieve available dovecot schemes", - internal_name=DOVEADM_PASS_SCHEME_ALARM) + internal_name=DOVEADM_PASS_SCHEME_ALARM, + ) - return ["{{{}}}".format(smart_str(scheme)) - for scheme in schemes.split()], status + return ["{{{}}}".format(smart_str(scheme)) for scheme in schemes.split()], status def cache_available_password_hasher(bypass_cache=False): available_schemes, status = get_dovecot_schemes() password_scheme_choice = [ - (hasher.name, hasher.label) - for hasher in PasswordHasher.get_password_hashers() - if hasher().scheme in available_schemes - ] + (hasher.name, hasher.label) + for hasher in PasswordHasher.get_password_hashers() + if hasher().scheme in available_schemes + ] if status == 0 and not bypass_cache: - cache.set("password_scheme_choice", - password_scheme_choice, - 2592000) + cache.set("password_scheme_choice", password_scheme_choice, 2592000) return password_scheme_choice diff --git a/modoboa/core/password_validation.py b/modoboa/core/password_validation.py index 47522897d..e21fc3f06 100644 --- a/modoboa/core/password_validation.py +++ b/modoboa/core/password_validation.py @@ -16,51 +16,56 @@ class ComplexityValidator(object): def validate(self, password, user=None): special_characters = "~!@#$%^&*()_+{}\":;,'[]" condition = ( - self.digits > 0 and - sum(1 for char in password if char.isdigit()) < self.digits) + self.digits > 0 + and sum(1 for char in password if char.isdigit()) < self.digits + ) if condition: raise ValidationError( ngettext( "Password must contain at least {} digit.", "Password must contain at least {} digits.", - self.digits - ).format(self.digits)) + self.digits, + ).format(self.digits) + ) condition = ( - self.lower > 0 and - sum(1 for char in password if char.islower()) < self.lower) + self.lower > 0 + and sum(1 for char in password if char.islower()) < self.lower + ) if condition: raise ValidationError( ngettext( "Password must contain at least {} lowercase letter.", "Password must contain at least {} lowercase letters.", - self.lower - ) - .format(self.lower)) + self.lower, + ).format(self.lower) + ) condition = ( - self.upper > 0 and - sum(1 for char in password if char.isupper()) < self.upper) + self.upper > 0 + and sum(1 for char in password if char.isupper()) < self.upper + ) if condition: raise ValidationError( ngettext( "Password must contain at least {} uppercase letter.", "Password must contain at least {} uppercase letters.", - self.upper - ) - .format(self.upper)) + self.upper, + ).format(self.upper) + ) condition = ( - self.specials > 0 and - sum(1 for char in password if char in special_characters) < - self.specials) + self.specials > 0 + and sum(1 for char in password if char in special_characters) + < self.specials + ) if condition: raise ValidationError( ngettext( "Password must contain at least {} special character.", "Password must contain at least {} special characters.", - self.specials - ) - .format(self.specials)) + self.specials, + ).format(self.specials) + ) def get_help_text(self): return _( - "Your password must contain a combination of different " - "character types.") + "Your password must contain a combination of different " "character types." + ) diff --git a/modoboa/core/signals.py b/modoboa/core/signals.py index 05107d396..ca5118f35 100644 --- a/modoboa/core/signals.py +++ b/modoboa/core/signals.py @@ -2,25 +2,27 @@ import django.dispatch -account_auto_created = django.dispatch.Signal() # Provides user -account_exported = django.dispatch.Signal() # Provides account -account_imported = django.dispatch.Signal() # Provides user, account, row -account_deleted = django.dispatch.Signal() # Provides user -account_role_changed = django.dispatch.Signal() # Provides account, role -account_password_updated = django.dispatch.Signal() # Provides account password created -allow_password_change = django.dispatch.Signal() # Provides user -can_create_object = django.dispatch.Signal() # Provides context, klass, object_type, count, instance -extra_account_actions = django.dispatch.Signal() # Provides account -extra_admin_dashboard_widgets = django.dispatch.Signal() # Provides user -extra_admin_menu_entries = django.dispatch.Signal() # Provides location, user -extra_role_permissions = django.dispatch.Signal() # Provides role -extra_static_content = django.dispatch.Signal() # Provides caller, st_type, user +account_auto_created = django.dispatch.Signal() # Provides user +account_exported = django.dispatch.Signal() # Provides account +account_imported = django.dispatch.Signal() # Provides user, account, row +account_deleted = django.dispatch.Signal() # Provides user +account_role_changed = django.dispatch.Signal() # Provides account, role +account_password_updated = django.dispatch.Signal() # Provides account password created +allow_password_change = django.dispatch.Signal() # Provides user +can_create_object = ( + django.dispatch.Signal() +) # Provides context, klass, object_type, count, instance +extra_account_actions = django.dispatch.Signal() # Provides account +extra_admin_dashboard_widgets = django.dispatch.Signal() # Provides user +extra_admin_menu_entries = django.dispatch.Signal() # Provides location, user +extra_role_permissions = django.dispatch.Signal() # Provides role +extra_static_content = django.dispatch.Signal() # Provides caller, st_type, user extra_uprefs_routes = django.dispatch.Signal() -extra_user_menu_entries = django.dispatch.Signal() # Provides location, user -get_announcements = django.dispatch.Signal() # Provides location -get_top_notifications = django.dispatch.Signal() # Provides include_all -initial_data_loaded = django.dispatch.Signal() # Provides extname +extra_user_menu_entries = django.dispatch.Signal() # Provides location, user +get_announcements = django.dispatch.Signal() # Provides location +get_top_notifications = django.dispatch.Signal() # Provides include_all +initial_data_loaded = django.dispatch.Signal() # Provides extname register_postfix_maps = django.dispatch.Signal() -user_can_set_role = django.dispatch.Signal() # Provides user, role, account -user_login = django.dispatch.Signal() # Provides username, password +user_can_set_role = django.dispatch.Signal() # Provides user, role, account +user_login = django.dispatch.Signal() # Provides username, password user_logout = django.dispatch.Signal() diff --git a/modoboa/core/sms_backends/__init__.py b/modoboa/core/sms_backends/__init__.py index 9ac1ee8a1..f3abe9648 100644 --- a/modoboa/core/sms_backends/__init__.py +++ b/modoboa/core/sms_backends/__init__.py @@ -24,11 +24,9 @@ class SMSBackend: def get_backend_class(name): """Return class for given backend.""" - backend_module = import_module( - "modoboa.core.sms_backends.{}".format(name)) + backend_module = import_module("modoboa.core.sms_backends.{}".format(name)) try: - backend_class = getattr( - backend_module, "{}Backend".format(name.upper())) + backend_class = getattr(backend_module, "{}Backend".format(name.upper())) except AttributeError: return None else: diff --git a/modoboa/core/sms_backends/ovh.py b/modoboa/core/sms_backends/ovh.py index ae86a9d7b..a1c260ebe 100644 --- a/modoboa/core/sms_backends/ovh.py +++ b/modoboa/core/sms_backends/ovh.py @@ -28,32 +28,29 @@ class OVHBackend(SMSBackend): ("soyoustart-ca", _("So you Start North America")), ("kimsufi-eu", _("Kimsufi Europe")), ("kimsufi-ca", _("Kimsufi North America")), - ) - } + ), + }, }, "sms_ovh_application_key": { "type": forms.CharField, - "attrs": { - "label": _("Application key"), - "required": False - } + "attrs": {"label": _("Application key"), "required": False}, }, "sms_ovh_application_secret": { "type": forms.CharField, "attrs": { "label": _("Application secret"), "widget": forms.widgets.PasswordInput(render_value=True), - "required": False - } + "required": False, + }, }, "sms_ovh_consumer_key": { "type": forms.CharField, "attrs": { "label": _("Consumer key"), "widget": forms.widgets.PasswordInput(render_value=True), - "required": False - } - } + "required": False, + }, + }, } serializer_settings = { @@ -69,8 +66,8 @@ class OVHBackend(SMSBackend): ("soyoustart-ca", _("So you Start North America")), ("kimsufi-eu", _("Kimsufi Europe")), ("kimsufi-ca", _("Kimsufi North America")), - ) - } + ), + }, }, "sms_ovh_application_key": { "type": serializers.CharField, @@ -78,7 +75,7 @@ class OVHBackend(SMSBackend): "required": False, "allow_blank": True, "allow_null": True, - } + }, }, "sms_ovh_application_secret": { "type": serializers.CharField, @@ -86,7 +83,7 @@ class OVHBackend(SMSBackend): "required": False, "allow_blank": True, "allow_null": True, - } + }, }, "sms_ovh_consumer_key": { "type": serializers.CharField, @@ -94,36 +91,48 @@ class OVHBackend(SMSBackend): "required": False, "allow_blank": True, "allow_null": True, - } - } + }, + }, } structure = [ - ("sms_ovh_endpoint", { - "label": _("API endpoint"), - "display": "sms_password_recovery=true&sms_provider=ovh", - }), - ("sms_ovh_application_key", { - "label": _("Application key"), - "display": "sms_password_recovery=true&sms_provider=ovh" - }), - ("sms_ovh_application_secret", { - "label": _("Appication secret"), - "display": "sms_password_recovery=true&sms_provider=ovh", - "password": True - }), - ("sms_ovh_consumer_key", { - "label": _("Consumer key"), - "display": "sms_password_recovery=true&sms_provider=ovh", - "password": True - }), + ( + "sms_ovh_endpoint", + { + "label": _("API endpoint"), + "display": "sms_password_recovery=true&sms_provider=ovh", + }, + ), + ( + "sms_ovh_application_key", + { + "label": _("Application key"), + "display": "sms_password_recovery=true&sms_provider=ovh", + }, + ), + ( + "sms_ovh_application_secret", + { + "label": _("Appication secret"), + "display": "sms_password_recovery=true&sms_provider=ovh", + "password": True, + }, + ), + ( + "sms_ovh_consumer_key", + { + "label": _("Consumer key"), + "display": "sms_password_recovery=true&sms_provider=ovh", + "password": True, + }, + ), ] visibility_rules = { "sms_ovh_endpoint": "sms_provider=ovh", "sms_ovh_application_key": "sms_provider=ovh", "sms_ovh_application_secret": "sms_provider=ovh", - "sms_ovh_consumer_key": "sms_provider=ovh" + "sms_ovh_consumer_key": "sms_provider=ovh", } @cached_property @@ -132,7 +141,7 @@ class OVHBackend(SMSBackend): endpoint=self._params.get_value("sms_ovh_endpoint"), application_key=self._params.get_value("sms_ovh_application_key"), application_secret=self._params.get_value("sms_ovh_application_secret"), - consumer_key=self._params.get_value("sms_ovh_consumer_key") + consumer_key=self._params.get_value("sms_ovh_consumer_key"), ) def send(self, text, recipients): @@ -144,7 +153,7 @@ class OVHBackend(SMSBackend): receivers=recipients, priority="high", noStopClause=True, - senderForResponse=True + senderForResponse=True, ) if result["totalCreditsRemoved"] != len(recipients): return False diff --git a/modoboa/core/templatetags/core_tags.py b/modoboa/core/templatetags/core_tags.py index f8820e6ca..859996d0d 100644 --- a/modoboa/core/templatetags/core_tags.py +++ b/modoboa/core/templatetags/core_tags.py @@ -26,126 +26,149 @@ register = template.Library() def core_menu(selection, user): """Build the top level menu.""" entries = signals.extra_admin_menu_entries.send( - sender="core_menu", location="top_menu", user=user) + sender="core_menu", location="top_menu", user=user + ) entries = reduce(lambda a, b: a + b, [entry[1] for entry in entries]) if user.is_superuser: entries += [ - {"name": "settings", - "label": _("Modoboa"), - "url": reverse("core:index")} + {"name": "settings", "label": _("Modoboa"), "url": reverse("core:index")} ] if not len(entries): return "" - return render_to_string("common/menulist.html", { - "entries": entries, - "selection": selection, - "user": user} + return render_to_string( + "common/menulist.html", + {"entries": entries, "selection": selection, "user": user}, ) @register.simple_tag def extensions_menu(selection, user): menu = signals.extra_user_menu_entries.send( - sender="core_menu", location="top_menu", user=user) + sender="core_menu", location="top_menu", user=user + ) menu = reduce(lambda a, b: a + b, [entry[1] for entry in menu]) - return render_to_string("common/menulist.html", { - "selection": selection, "entries": menu, "user": user - }) + return render_to_string( + "common/menulist.html", {"selection": selection, "entries": menu, "user": user} + ) @register.simple_tag def admin_menu(selection, user): entries = [ - {"name": "info", - "class": "ajaxnav", - "url": "info/", - "label": _("Information")}, - {"name": "logs", - "class": "ajaxnav", - "url": "logs/?sort_order=-date_created", - "label": _("Logs")}, - {"name": "parameters", - "class": "ajaxnav", - "url": "parameters/", - "img": "", - "label": _("Parameters")}, + {"name": "info", "class": "ajaxnav", "url": "info/", "label": _("Information")}, + { + "name": "logs", + "class": "ajaxnav", + "url": "logs/?sort_order=-date_created", + "label": _("Logs"), + }, + { + "name": "parameters", + "class": "ajaxnav", + "url": "parameters/", + "img": "", + "label": _("Parameters"), + }, ] - return render_to_string("common/menu.html", { - "entries": entries, - "css": "nav nav-sidebar", - "selection": selection, - "user": user - }) + return render_to_string( + "common/menu.html", + { + "entries": entries, + "css": "nav nav-sidebar", + "selection": selection, + "user": user, + }, + ) @register.simple_tag def user_menu(request, selection): entries = [ - {"name": "user", - "img": "fa fa-user", - "label": request.user.fullname, - "menu": [ - {"name": "settings", - "img": "fa fa-list", - "label": _("Settings"), - "url": reverse("core:user_index")} - ]} + { + "name": "user", + "img": "fa fa-user", + "label": request.user.fullname, + "menu": [ + { + "name": "settings", + "img": "fa fa-list", + "label": _("Settings"), + "url": reverse("core:user_index"), + } + ], + } ] extra_entries = signals.extra_user_menu_entries.send( - sender="user_menu", location="options_menu", user=request.user) - extra_entries = reduce( - lambda a, b: a + b, [entry[1] for entry in extra_entries]) - entries[0]["menu"] += ( - extra_entries + [{ + sender="user_menu", location="options_menu", user=request.user + ) + extra_entries = reduce(lambda a, b: a + b, [entry[1] for entry in extra_entries]) + entries[0]["menu"] += extra_entries + [ + { "name": "logout", "url": reverse("core:logout"), "label": _("Logout"), "img": "fa fa-sign-out", - "method": "post" - }] + "method": "post", + } + ] + return render_to_string( + "common/menulist.html", + { + "request": request, + "selection": selection, + "entries": entries, + "user": request.user, + }, ) - return render_to_string("common/menulist.html", { - "request": request, "selection": selection, - "entries": entries, "user": request.user - }) @register.simple_tag def uprefs_menu(selection, user): entries = [ - {"name": "profile", - "class": "ajaxnav", - "url": "profile/", - "label": _("Profile")}, - {"name": "preferences", - "class": "ajaxnav", - "url": "preferences/", - "label": _("Preferences")}, - {"name": "security", - "class": "ajaxnav", - "url": "security/", - "label": _("Security")}, + { + "name": "profile", + "class": "ajaxnav", + "url": "profile/", + "label": _("Profile"), + }, + { + "name": "preferences", + "class": "ajaxnav", + "url": "preferences/", + "label": _("Preferences"), + }, + { + "name": "security", + "class": "ajaxnav", + "url": "security/", + "label": _("Security"), + }, ] if user.is_superuser: - entries.append({ - "name": "api", - "class": "ajaxnav", - "url": "api/", - "label": _("API"), - }) + entries.append( + { + "name": "api", + "class": "ajaxnav", + "url": "api/", + "label": _("API"), + } + ) extra_entries = signals.extra_user_menu_entries.send( - sender="user_menu", location="uprefs_menu", user=user) - extra_entries = reduce( - lambda a, b: a + b, [entry[1] for entry in extra_entries]) + sender="user_menu", location="uprefs_menu", user=user + ) + extra_entries = reduce(lambda a, b: a + b, [entry[1] for entry in extra_entries]) entries += extra_entries entries = sorted(entries, key=lambda e: e["label"]) - return render_to_string("common/menu.html", { - "entries": entries, - "css": "nav nav-sidebar", - "selection": selection, - "user": user - }) + return render_to_string( + "common/menu.html", + { + "entries": entries, + "css": "nav nav-sidebar", + "selection": selection, + "user": user, + }, + ) @register.filter @@ -154,7 +177,7 @@ def colorize_level(level): classes = { "INFO": "text-info", "WARNING": "text-warning", - "CRITICAL": "text-danger" + "CRITICAL": "text-danger", } if level not in classes: return level @@ -169,14 +192,18 @@ def tohtml(message): @register.simple_tag def visirule(field): - if not hasattr(field, "form") or \ - not hasattr(field.form, "visirules") or \ - field.html_name not in field.form.visirules: + if ( + not hasattr(field, "form") + or not hasattr(field.form, "visirules") + or field.html_name not in field.form.visirules + ): return "" rule = field.form.visirules[field.html_name] return mark_safe( - " data-visibility-field='{}' data-visibility-value='{}' " - .format(rule["field"], rule["value"])) + " data-visibility-field='{}' data-visibility-value='{}' ".format( + rule["field"], rule["value"] + ) + ) @register.simple_tag @@ -200,8 +227,7 @@ class ConnectedUsers(template.Node): uid_list.append(uid) # Query all logged in users based on id list - context[self.varname] = ( - models.User.objects.filter(pk__in=uid_list).distinct()) + context[self.varname] = models.User.objects.filter(pk__in=uid_list).distinct() return "" @@ -230,12 +256,11 @@ def get_modoboa_logo(): @register.simple_tag def load_optionalmenu(user): menu = signals.extra_user_menu_entries.send( - sender="user_menu", location="top_menu_middle", user=user) - menu = reduce( - lambda a, b: a + b, [entry[1] for entry in menu]) + sender="user_menu", location="top_menu_middle", user=user + ) + menu = reduce(lambda a, b: a + b, [entry[1] for entry in menu]) return template.loader.render_to_string( - "common/menulist.html", - {"entries": menu, "user": user} + "common/menulist.html", {"entries": menu, "user": user} ) @@ -253,13 +278,16 @@ def display_messages(msgs): else: timeout = "undefined" - return mark_safe(""" + return mark_safe( + """ -""" % (level, text, timeout)) +""" + % (level, text, timeout) + ) @register.filter @@ -267,5 +295,5 @@ def currencyfmt(amount): """Simple temp. filter to replace babel.""" lang = get_language() if lang == "fr": - return u"{} €".format(amount) - return u"€{}".format(amount) + return "{} €".format(amount) + return "€{}".format(amount) diff --git a/modoboa/core/tests/stupid_extension_2/urls.py b/modoboa/core/tests/stupid_extension_2/urls.py index a4098f372..9a604dfdf 100644 --- a/modoboa/core/tests/stupid_extension_2/urls.py +++ b/modoboa/core/tests/stupid_extension_2/urls.py @@ -4,6 +4,4 @@ from django.urls import re_path from . import views -urlpatterns = [ - re_path("^$", views.test_view, name="index") -] +urlpatterns = [re_path("^$", views.test_view, name="index")] diff --git a/modoboa/core/tests/test_authentication.py b/modoboa/core/tests/test_authentication.py index 425b3e11e..80d0ba416 100644 --- a/modoboa/core/tests/test_authentication.py +++ b/modoboa/core/tests/test_authentication.py @@ -95,7 +95,7 @@ class AuthenticationTestCase(ModoTestCase): password = "toto" data = {"username": username, "password": password} user = models.User.objects.get(username=username) - pw_hash = get_password_hasher('fallback_scheme')() + pw_hash = get_password_hasher("fallback_scheme")() self.client.logout() self.set_global_parameter("password_scheme", "sha512crypt") @@ -154,9 +154,7 @@ class AuthenticationTestCase(ModoTestCase): self.client.post(reverse("core:login"), data) user.refresh_from_db() self.assertTrue(user.password.startswith("{ARGON2ID}")) - parameters = argon2.extract_parameters( - user.password.lstrip("{ARGON2ID}") - ) + parameters = argon2.extract_parameters(user.password.lstrip("{ARGON2ID}")) self.assertEqual(parameters.time_cost, 4) self.assertEqual(parameters.memory_cost, 10000) self.assertEqual(parameters.parallelism, 4) @@ -170,8 +168,7 @@ class AuthenticationTestCase(ModoTestCase): self.client.post(reverse("core:login"), data) user.refresh_from_db() self.assertTrue(user.password.startswith("{ARGON2ID}")) - parameters = argon2.extract_parameters( - user.password.lstrip("{ARGON2ID}")) + parameters = argon2.extract_parameters(user.password.lstrip("{ARGON2ID}")) self.assertEqual(parameters.time_cost, 3) self.assertEqual(parameters.memory_cost, 1000) self.assertEqual(parameters.parallelism, 2) @@ -184,41 +181,45 @@ class AuthenticationTestCase(ModoTestCase): "MD5 MD5-CRYPT SHA SHA1 SSHA 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" + "PBKDF2 CRYPT SHA256-CRYPT SHA512-CRYPT", ) supported_schemes = get_dovecot_schemes()[0] - self.assertEqual(supported_schemes, - ["{MD5}", - "{MD5-CRYPT}", - "{SHA}", - "{SHA1}", - "{SSHA}", - "{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}"]) + self.assertEqual( + supported_schemes, + [ + "{MD5}", + "{MD5-CRYPT}", + "{SHA}", + "{SHA1}", + "{SSHA}", + "{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}", + ], + ) @override_settings(DOVECOT_SUPPORTED_SCHEMES="SHA1 SHA512-CRYPT") def test_dovecot_supported_schemes_from_settings(self): @@ -240,9 +241,10 @@ class PasswordResetTestCase(ModoTestCase): """Create test data.""" super(PasswordResetTestCase, cls).setUpTestData() cls.account_ok = factories.UserFactory( - username="user@test.com", secondary_email="test@ext.com", + username="user@test.com", + secondary_email="test@ext.com", phone_number="+33612345678", - groups=("SimpleUsers",) + groups=("SimpleUsers",), ) cls.account_ko = factories.UserFactory( username="user2@test.com", groups=("SimpleUsers",) @@ -255,8 +257,8 @@ class PasswordResetTestCase(ModoTestCase): data = {"email": self.account_ok.email} response = self.client.post(url, data, follow=True) self.assertContains( - response, - "We've emailed you instructions for setting your password") + response, "We've emailed you instructions for setting your password" + ) self.assertEqual(len(mail.outbox), 1) def test_reset_password_no_secondary_email(self): @@ -266,8 +268,8 @@ class PasswordResetTestCase(ModoTestCase): data = {"email": self.account_ko.email} response = self.client.post(url, data, follow=True) self.assertContains( - response, - "We've emailed you instructions for setting your password") + response, "We've emailed you instructions for setting your password" + ) self.assertEqual(len(mail.outbox), 0) def test_reset_password_announcement(self): @@ -286,13 +288,15 @@ class PasswordResetTestCase(ModoTestCase): """Test reset password by SMS.""" client_get.return_value = ["service"] client_post.return_value = {"totalCreditsRemoved": 1} - self.set_global_parameters({ - "sms_password_recovery": True, - "sms_provider": "ovh", - "sms_ovh_application_key": "key", - "sms_ovh_application_secret": "secret", - "sms_ovh_consumer_key": "consumer" - }) + self.set_global_parameters( + { + "sms_password_recovery": True, + "sms_provider": "ovh", + "sms_ovh_application_key": "key", + "sms_ovh_application_secret": "secret", + "sms_ovh_consumer_key": "consumer", + } + ) self.client.logout() url = reverse("password_reset") data = {"email": self.account_ok.email} @@ -318,13 +322,16 @@ class PasswordResetTestCase(ModoTestCase): response = self.client.get(url) self.assertEqual(response.status_code, 404) - self.set_global_parameters({ - "sms_password_recovery": True, - "sms_provider": "ovh", - "sms_ovh_application_key": "key", - "sms_ovh_application_secret": "secret", - "sms_ovh_consumer_key": "consumer" - }, app="core") + self.set_global_parameters( + { + "sms_password_recovery": True, + "sms_provider": "ovh", + "sms_ovh_application_key": "key", + "sms_ovh_application_secret": "secret", + "sms_ovh_consumer_key": "consumer", + }, + app="core", + ) # No user pk in session response = self.client.get(url) self.assertEqual(response.status_code, 404) @@ -340,10 +347,12 @@ class PasswordResetTestCase(ModoTestCase): @skipIf(NO_SMTP, "No SMTP server available") -@override_settings(AUTHENTICATION_BACKENDS=( - "modoboa.lib.authbackends.SMTPBackend", - "django.contrib.auth.backends.ModelBackend" -)) +@override_settings( + AUTHENTICATION_BACKENDS=( + "modoboa.lib.authbackends.SMTPBackend", + "django.contrib.auth.backends.ModelBackend", + ) +) class SMTPAuthenticationTestCase(ModoTestCase): """Validate SMTP authentication scenarios.""" @@ -356,10 +365,8 @@ class SMTPAuthenticationTestCase(ModoTestCase): response = self.client.post(reverse("core:login"), data) self.assertEqual(response.status_code, 302) self.assertTrue(response.url.endswith(reverse("core:user_index"))) - mock_smtp.return_value.login.assert_called_once_with( - username, password) - self.assertTrue( - models.User.objects.filter(username=username).exists()) + mock_smtp.return_value.login.assert_called_once_with(username, password) + self.assertTrue(models.User.objects.filter(username=username).exists()) @mock.patch("smtplib.SMTP") def test_smtp_authentication(self, mock_smtp): @@ -383,14 +390,13 @@ class SMTPAuthenticationTestCase(ModoTestCase): """Check SMTP authentication failure.""" instance = mock_smtp.return_value instance.login.side_effect = smtplib.SMTPAuthenticationError( - 450, "User not found") + 450, "User not found" + ) self.client.logout() username = "user@unknown.test" password = "toto" data = {"username": username, "password": password} response = self.client.post(reverse("core:login"), data) self.assertEqual(response.status_code, 401) - mock_smtp.return_value.login.assert_called_once_with( - username, password) - self.assertFalse( - models.User.objects.filter(username=username).exists()) + mock_smtp.return_value.login.assert_called_once_with(username, password) + self.assertFalse(models.User.objects.filter(username=username).exists()) diff --git a/modoboa/core/tests/test_checks.py b/modoboa/core/tests/test_checks.py index 06ab8ad58..02bdfeb60 100644 --- a/modoboa/core/tests/test_checks.py +++ b/modoboa/core/tests/test_checks.py @@ -9,8 +9,7 @@ class CheckSessionCookieSecureTest(SimpleTestCase): def test_use_tz_false(self): """If USE_TZ is off provide one warning.""" self.assertEqual( - settings_checks.check_use_tz_enabled(None), - [settings_checks.W001] + settings_checks.check_use_tz_enabled(None), [settings_checks.W001] ) @override_settings(USE_TZ=True) diff --git a/modoboa/core/tests/test_core.py b/modoboa/core/tests/test_core.py index c722432da..d1adf8feb 100644 --- a/modoboa/core/tests/test_core.py +++ b/modoboa/core/tests/test_core.py @@ -16,7 +16,6 @@ from .. import factories, mocks, models class AuthenticationTestCase(ModoTestCase): - """Validate authentication scenarios.""" @classmethod @@ -49,10 +48,8 @@ class ManagementCommandsTestCase(SimpleModoTestCase): def test_change_default_admin(self): """Use dedicated option.""" - management.call_command( - "load_initial_data", "--admin-username", "modoadmin") - self.assertTrue( - self.client.login(username="modoadmin", password="password")) + management.call_command("load_initial_data", "--admin-username", "modoadmin") + self.assertTrue(self.client.login(username="modoadmin", password="password")) def test_clean_logs(self): """Run cleanlogs command.""" @@ -72,8 +69,7 @@ class ManagementCommandsTestCase(SimpleModoTestCase): last_login = timezone.now() - relativedelta(days=45) account = factories.UserFactory( - username="user1@domain.test", groups=("SimpleUsers", ), - last_login=last_login + username="user1@domain.test", groups=("SimpleUsers",), last_login=last_login ) management.call_command("clean_inactive_accounts", "--dry-run") account.refresh_from_db() @@ -81,16 +77,15 @@ class ManagementCommandsTestCase(SimpleModoTestCase): out = StringIO() management.call_command( - "clean_inactive_accounts", "--verbose", "--dry-run", stdout=out) + "clean_inactive_accounts", "--verbose", "--dry-run", stdout=out + ) self.assertIn("user1@domain.test", out.getvalue()) # Disable account account threshold self.set_global_parameter("enable_inactive_accounts", False) out = StringIO() - management.call_command( - "clean_inactive_accounts", "--verbose", stdout=out) - self.assertIn("Inactive accounts detection is disabled.", - out.getvalue()) + management.call_command("clean_inactive_accounts", "--verbose", stdout=out) + self.assertIn("Inactive accounts detection is disabled.", out.getvalue()) self.set_global_parameter("enable_inactive_accounts", True) management.call_command("clean_inactive_accounts", "--silent") @@ -100,8 +95,7 @@ class ManagementCommandsTestCase(SimpleModoTestCase): account.is_active = True account.save(update_fields=["is_active"]) - management.call_command( - "clean_inactive_accounts", "--silent", "--delete") + management.call_command("clean_inactive_accounts", "--silent", "--delete") with self.assertRaises(models.User.DoesNotExist): account.refresh_from_db() @@ -120,8 +114,10 @@ class ProfileTestCase(ModoTestCase): def test_update_profile(self): """Update profile without password.""" data = { - "first_name": "Homer", "last_name": "Simpson", - "phone_number": "+33612345678", "language": "en" + "first_name": "Homer", + "last_name": "Simpson", + "phone_number": "+33612345678", + "language": "en", } self.ajax_post(reverse("core:user_profile"), data) admin = models.User.objects.get(username="admin") @@ -134,47 +130,53 @@ class ProfileTestCase(ModoTestCase): * The default admin changes his password (no associated Mailbox) * A normal user changes his password """ - self.ajax_post(reverse("core:user_profile"), - {"language": "en", "oldpassword": "password", - "newpassword": "12345Toi", "confirmation": "12345Toi"}) + self.ajax_post( + reverse("core:user_profile"), + { + "language": "en", + "oldpassword": "password", + "newpassword": "12345Toi", + "confirmation": "12345Toi", + }, + ) self.client.logout() - self.assertEqual( - self.client.login(username="admin", password="12345Toi"), True - ) + self.assertEqual(self.client.login(username="admin", password="12345Toi"), True) self.assertEqual( self.client.login(username="user@test.com", password="toto"), True ) self.ajax_post( reverse("core:user_profile"), - {"language": "en", - "oldpassword": "toto", - "confirmation": "tutu"}, - status=400 + {"language": "en", "oldpassword": "toto", "confirmation": "tutu"}, + status=400, ) self.ajax_post( reverse("core:user_profile"), - {"language": "en", - "oldpassword": "toto", - "newpassword": "tutu"}, - status=400 + {"language": "en", "oldpassword": "toto", "newpassword": "tutu"}, + status=400, ) self.ajax_post( reverse("core:user_profile"), - {"language": "en", - "oldpassword": "toto", - "newpassword": "tutu", - "confirmation": "tutu"}, - status=400 + { + "language": "en", + "oldpassword": "toto", + "newpassword": "tutu", + "confirmation": "tutu", + }, + status=400, ) self.ajax_post( reverse("core:user_profile"), - {"language": "en", "oldpassword": "toto", - "newpassword": "Toto1234", "confirmation": "Toto1234"} + { + "language": "en", + "oldpassword": "toto", + "newpassword": "Toto1234", + "confirmation": "Toto1234", + }, ) self.client.logout() self.assertTrue( @@ -184,9 +186,13 @@ class ProfileTestCase(ModoTestCase): def test_update_password_restrictions(self): self.ajax_post( reverse("core:user_profile"), - {"language": "en", "oldpassword": "", - "newpassword": "12345Toi", "confirmation": "12345Toi"}, - status=400 + { + "language": "en", + "oldpassword": "", + "newpassword": "12345Toi", + "confirmation": "12345Toi", + }, + status=400, ) self.client.logout() @@ -196,11 +202,9 @@ class ProfileTestCase(ModoTestCase): def test_update_password_url(self): """Check if external is used when defined.""" - self.set_global_parameter( - "update_password_url", "http://update.password") + self.set_global_parameter("update_password_url", "http://update.password") non_local_user = factories.UserFactory( - username="user@external.com", groups=("SimpleUsers",), - is_local=False + username="user@external.com", groups=("SimpleUsers",), is_local=False ) self.client.force_login(non_local_user) url = reverse("core:user_profile") @@ -213,7 +217,6 @@ class ProfileTestCase(ModoTestCase): class APIAccessFormTestCase(ModoTestCase): - """Check form access.""" @classmethod @@ -250,10 +253,11 @@ class APICommunicationTestCase(ModoTestCase): def test_management_command(self): """Check command.""" with httmock.HTTMock( - mocks.modo_api_instance_search, - mocks.modo_api_instance_create, - mocks.modo_api_instance_update, - mocks.modo_api_versions): + mocks.modo_api_instance_search, + mocks.modo_api_instance_create, + mocks.modo_api_instance_update, + mocks.modo_api_versions, + ): management.call_command("communicate_with_public_api") self.assertEqual(models.LocalConfig.objects.first().api_pk, 100) self.assertEqual(len(mail.outbox), 0) @@ -265,28 +269,31 @@ class APICommunicationTestCase(ModoTestCase): # Enable notifications self.set_global_parameter("send_new_versions_email", True) with httmock.HTTMock( - mocks.modo_api_instance_search, - mocks.modo_api_instance_create, - mocks.modo_api_instance_update, - mocks.modo_api_versions): + mocks.modo_api_instance_search, + mocks.modo_api_instance_create, + mocks.modo_api_instance_update, + mocks.modo_api_versions, + ): management.call_command("communicate_with_public_api") self.assertEqual(len(mail.outbox), 1) # Call once again and check no new notification has been sent self.set_global_parameter("send_new_versions_email", True) with httmock.HTTMock( - mocks.modo_api_instance_search, - mocks.modo_api_instance_create, - mocks.modo_api_instance_update, - mocks.modo_api_versions): + mocks.modo_api_instance_search, + mocks.modo_api_instance_create, + mocks.modo_api_instance_update, + mocks.modo_api_versions, + ): management.call_command("communicate_with_public_api") self.assertEqual(len(mail.outbox), 1) # Make sure no new notification is sent when no updates with httmock.HTTMock( - mocks.modo_api_instance_search, - mocks.modo_api_instance_create, - mocks.modo_api_instance_update, - mocks.modo_api_versions_no_update): + mocks.modo_api_instance_search, + mocks.modo_api_instance_create, + mocks.modo_api_instance_update, + mocks.modo_api_versions_no_update, + ): management.call_command("communicate_with_public_api") self.assertEqual(len(mail.outbox), 1) diff --git a/modoboa/core/tests/test_extensions.py b/modoboa/core/tests/test_extensions.py index 3ef1ec764..8d41df2f5 100644 --- a/modoboa/core/tests/test_extensions.py +++ b/modoboa/core/tests/test_extensions.py @@ -56,12 +56,18 @@ class ExtensionTestCase(TestCase): """Check getter method.""" self.assertIsNone(self.pool.get_extension_infos("toto")) infos = self.pool.get_extension_infos("stupid_extension_1") - compare(infos, { - "name": "stupid_extension_1", "label": "Stupid extension", - "version": "1.0.0", "description": "A stupid extension", - "url": "stupid_extension_1", "always_active": False, - "topredirection_url": None - }) + compare( + infos, + { + "name": "stupid_extension_1", + "label": "Stupid extension", + "version": "1.0.0", + "description": "A stupid extension", + "url": "stupid_extension_1", + "always_active": False, + "topredirection_url": None, + }, + ) def test_list_all(self): """Check list_all method.""" diff --git a/modoboa/core/tests/test_ldap.py b/modoboa/core/tests/test_ldap.py index bc666ccca..d91d11359 100644 --- a/modoboa/core/tests/test_ldap.py +++ b/modoboa/core/tests/test_ldap.py @@ -20,21 +20,25 @@ class LDAPTestCaseMixin(object): def ldapauthbackend(self): """Return LDAPAuthBackend instance.""" from modoboa.lib.ldap_utils import LDAPAuthBackend + return LDAPAuthBackend() def activate_ldap_authentication(self): """Modify settings.""" - self.set_global_parameters({ - "authentication_type": "ldap", - "ldap_server_port": settings.LDAP_SERVER_PORT - }) + self.set_global_parameters( + { + "authentication_type": "ldap", + "ldap_server_port": settings.LDAP_SERVER_PORT, + } + ) def restore_user_password(self, username, new_password): """Restore user password to its initial state.""" for password in ["Toto1234", "test"]: try: self.ldapauthbackend.update_user_password( - username, password, new_password) + username, password, new_password + ) except exceptions.InternalError: pass else: @@ -46,30 +50,35 @@ class LDAPTestCaseMixin(object): self.client.logout() if restore_before: self.restore_user_password(user, password) - self.assertTrue( - self.client.login(username=user, password=password)) + self.assertTrue(self.client.login(username=user, password=password)) def searchbind_mode(self): """Apply settings required by the searchbind mode.""" - self.set_global_parameters({ - "ldap_auth_method": "searchbind", - "ldap_bind_dn": "cn=admin,dc=example,dc=com", - "ldap_bind_password": "test", - "ldap_search_base": "ou=users,dc=example,dc=com" - }) + self.set_global_parameters( + { + "ldap_auth_method": "searchbind", + "ldap_bind_dn": "cn=admin,dc=example,dc=com", + "ldap_bind_password": "test", + "ldap_search_base": "ou=users,dc=example,dc=com", + } + ) def directbind_mode(self): """Apply settings required by the directbind mode.""" - self.set_global_parameters({ - "ldap_auth_method": "directbind", - "ldap_user_dn_template": "cn=%(user)s,ou=users,dc=example,dc=com" - }) + self.set_global_parameters( + { + "ldap_auth_method": "directbind", + "ldap_user_dn_template": "cn=%(user)s,ou=users,dc=example,dc=com", + } + ) -@override_settings(AUTHENTICATION_BACKENDS=( - "modoboa.lib.authbackends.LDAPBackend", - "django.contrib.auth.backends.ModelBackend" -)) +@override_settings( + AUTHENTICATION_BACKENDS=( + "modoboa.lib.authbackends.LDAPBackend", + "django.contrib.auth.backends.ModelBackend", + ) +) class LDAPAuthenticationTestCase(LDAPTestCaseMixin, ModoTestCase): """Validate LDAP authentication scenarios.""" @@ -95,10 +104,12 @@ class LDAPAuthenticationTestCase(LDAPTestCaseMixin, ModoTestCase): self.authenticate(username, "test") self.check_created_user(username) - self.set_global_parameters({ - "ldap_admin_groups": "admins", - "ldap_groups_search_base": "ou=groups,dc=example,dc=com" - }) + self.set_global_parameters( + { + "ldap_admin_groups": "admins", + "ldap_groups_search_base": "ou=groups,dc=example,dc=com", + } + ) username = "mailadmin@example.com" self.authenticate(username, "test", False) self.check_created_user(username, "DomainAdmins") @@ -114,10 +125,12 @@ class LDAPAuthenticationTestCase(LDAPTestCaseMixin, ModoTestCase): # 1: must work because usernames of domain admins are not # always email addresses - self.set_global_parameters({ - "ldap_admin_groups": "admins", - "ldap_groups_search_base": "ou=groups,dc=example,dc=com" - }) + self.set_global_parameters( + { + "ldap_admin_groups": "admins", + "ldap_groups_search_base": "ou=groups,dc=example,dc=com", + } + ) username = "mailadmin" self.authenticate(username, "test", False) self.check_created_user("mailadmin@example.com", "DomainAdmins", False) @@ -134,10 +147,12 @@ class ProfileTestCase(LDAPTestCaseMixin, ModoTestCase): username="user@test.com", groups=("SimpleUsers",) ) - @override_settings(AUTHENTICATION_BACKENDS=( - "modoboa.lib.authbackends.LDAPBackend", - "django.contrib.auth.backends.ModelBackend" - )) + @override_settings( + AUTHENTICATION_BACKENDS=( + "modoboa.lib.authbackends.LDAPBackend", + "django.contrib.auth.backends.ModelBackend", + ) + ) def test_update_password_ldap(self): """Update password for an LDAP user.""" self.activate_ldap_authentication() @@ -147,7 +162,11 @@ class ProfileTestCase(LDAPTestCaseMixin, ModoTestCase): self.authenticate(username, "test") self.ajax_post( reverse("core:user_profile"), - {"language": "en", "oldpassword": "test", - "newpassword": "Toto1234", "confirmation": "Toto1234"} + { + "language": "en", + "oldpassword": "test", + "newpassword": "Toto1234", + "confirmation": "Toto1234", + }, ) self.authenticate(username, "Toto1234", False) diff --git a/modoboa/core/tests/test_views.py b/modoboa/core/tests/test_views.py index b91f9b9a8..5448f6c02 100644 --- a/modoboa/core/tests/test_views.py +++ b/modoboa/core/tests/test_views.py @@ -169,7 +169,8 @@ class DashboardTestCase(ModoTestCase): self.assertContains(response, "https://modoboa.org") self.set_global_parameter( - "rss_feed_url", "https://www.djangoproject.com/rss/weblog/") + "rss_feed_url", "https://www.djangoproject.com/rss/weblog/" + ) self.client.force_login(self.dadmin) response = self.client.get(url) self.assertContains(response, "djangoproject") @@ -200,7 +201,7 @@ class DashboardTestCase(ModoTestCase): """Load dashboard with DISABLE_DASHBOARD_EXTERNAL_QUERIES = True""" url = reverse("core:dashboard") response = self.client.get(url) - self.assertNotContains(response, 'https://modoboa.org/en/weblog/') + self.assertNotContains(response, "https://modoboa.org/en/weblog/") def test_root_dispatch(self): """Check root dispatching.""" @@ -236,19 +237,29 @@ class SettingsTestCase(ModoTestCase): settings["core-password_scheme"] = "magic_password_scheme" response = self.client.post(url, settings, format="json") self.assertEqual(response.status_code, 400) - compare(response.json(), { - "form_errors": {"password_scheme": ["Select a valid choice. \ -magic_password_scheme is not one of the available choices."]}, - "prefix": "core" - }) + compare( + response.json(), + { + "form_errors": { + "password_scheme": [ + "Select a valid choice. \ +magic_password_scheme is not one of the available choices." + ] + }, + "prefix": "core", + }, + ) settings["core-password_scheme"] = "plain" settings["core-rounds_number"] = "" response = self.client.post(url, settings, format="json") self.assertEqual(response.status_code, 400) - compare(response.json(), { - "form_errors": {"rounds_number": ["This field is required."]}, - "prefix": "core" - }) + compare( + response.json(), + { + "form_errors": {"rounds_number": ["This field is required."]}, + "prefix": "core", + }, + ) def test_sms_settings_clean(self): """Check sms settings validation.""" @@ -262,8 +273,7 @@ magic_password_scheme is not one of the available choices."]}, response = self.client.post(url, settings, format="json") self.assertEqual(response.status_code, 400) errors = response.json() - self.assertIn( - "sms_ovh_application_secret", errors["form_errors"]) + self.assertIn("sms_ovh_application_secret", errors["form_errors"]) def test_mx_check_settings_clean(self): settings = SETTINGS_SAMPLE.copy() @@ -275,6 +285,7 @@ magic_password_scheme is not one of the available choices."]}, param_tools.get_global_parameter("enable_ipv6_mx_checks", "admin") ) + class UserSettings(param_forms.UserParametersForm): """Stupid user settings form.""" @@ -286,10 +297,7 @@ class UserSettings(param_forms.UserParametersForm): def extra_user_menu(sender, location, user, **kwargs): """Return extra menu entry.""" return [ - {"name": "test_menu_entry", - "class": "ajaxnav", - "url": "toto/", - "label": "Test"} + {"name": "test_menu_entry", "class": "ajaxnav", "url": "toto/", "label": "Test"} ] diff --git a/modoboa/core/tests/utils.py b/modoboa/core/tests/utils.py index b9c841f64..74106e195 100644 --- a/modoboa/core/tests/utils.py +++ b/modoboa/core/tests/utils.py @@ -1,4 +1,5 @@ import os + def get_doveadm_test_path(): return "{}/doveadm".format(os.path.dirname(__file__)) diff --git a/modoboa/core/urls.py b/modoboa/core/urls.py index c6c6e4609..76b6e31fa 100644 --- a/modoboa/core/urls.py +++ b/modoboa/core/urls.py @@ -8,29 +8,34 @@ from . import views app_name = "core" urlpatterns = [ - path('', views.RootDispatchView.as_view(), name="root"), - path('dashboard/', views.DashboardView.as_view(), name="dashboard"), - - path('accounts/login/', views.dologin, name="login"), - path('accounts/logout/', views.dologout, name="logout"), - path('accounts/2fa_verify/', - views.TwoFactorCodeVerifyView.as_view(), - name='2fa_verify'), - - path('core/', views.viewsettings, name="index"), - path('core/parameters/', views.parameters, name="parameters"), - path('core/info/', views.information, name="information"), - path('core/logs/', views.logs, name="log_list"), - path('core/logs/page/', views.logs_page, name="logs_page"), - path('core/top_notifications/check/', - views.check_top_notifications, - name="top_notifications_check"), - - path('user/', views.index, name="user_index"), - path('user/preferences/', views.preferences, - name="user_preferences"), - path('user/profile/', views.profile, name="user_profile"), - path('user/api/', views.api_access, name="user_api_access"), - path('user/security/', views.security, name="user_security"), - path('robots.txt', TemplateView.as_view(template_name="core/robots.txt", content_type="text/plain")), + path("", views.RootDispatchView.as_view(), name="root"), + path("dashboard/", views.DashboardView.as_view(), name="dashboard"), + path("accounts/login/", views.dologin, name="login"), + path("accounts/logout/", views.dologout, name="logout"), + path( + "accounts/2fa_verify/", + views.TwoFactorCodeVerifyView.as_view(), + name="2fa_verify", + ), + path("core/", views.viewsettings, name="index"), + path("core/parameters/", views.parameters, name="parameters"), + path("core/info/", views.information, name="information"), + path("core/logs/", views.logs, name="log_list"), + path("core/logs/page/", views.logs_page, name="logs_page"), + path( + "core/top_notifications/check/", + views.check_top_notifications, + name="top_notifications_check", + ), + path("user/", views.index, name="user_index"), + path("user/preferences/", views.preferences, name="user_preferences"), + path("user/profile/", views.profile, name="user_profile"), + path("user/api/", views.api_access, name="user_api_access"), + path("user/security/", views.security, name="user_security"), + path( + "robots.txt", + TemplateView.as_view( + template_name="core/robots.txt", content_type="text/plain" + ), + ), ] diff --git a/modoboa/core/utils.py b/modoboa/core/utils.py index 9509e22ed..1ef002d64 100644 --- a/modoboa/core/utils.py +++ b/modoboa/core/utils.py @@ -26,12 +26,14 @@ def check_for_updates(): local_config = models.LocalConfig.objects.first() client = ModoAPIClient() extensions = exts_pool.list_all() - extensions = [{ - "label": "Modoboa", - "name": "modoboa", - "description": _("The core part of Modoboa"), - "version": client.local_core_version - }] + extensions + extensions = [ + { + "label": "Modoboa", + "name": "modoboa", + "description": _("The core part of Modoboa"), + "version": client.local_core_version, + } + ] + extensions update_avail = False for extension in extensions: pkgname = extension["name"].replace("_", "-") @@ -39,9 +41,8 @@ def check_for_updates(): if api_extension["name"] != pkgname: continue extension["last_version"] = api_extension["version"] - if ( - parse_version(api_extension["version"]) > - parse_version(extension["version"]) + if parse_version(api_extension["version"]) > parse_version( + extension["version"] ): extension["update"] = True extension["changelog_url"] = api_extension["url"] diff --git a/modoboa/core/views/__init__.py b/modoboa/core/views/__init__.py index 1036e8308..eb728b210 100644 --- a/modoboa/core/views/__init__.py +++ b/modoboa/core/views/__init__.py @@ -1,17 +1,24 @@ """Core views.""" from .admin import ( - check_top_notifications, information, logs, logs_page, parameters, - viewsettings + check_top_notifications, + information, + logs, + logs_page, + parameters, + viewsettings, ) from .auth import ( - PasswordResetView, dologin, dologout, VerifySMSCodeView, - ResendSMSCodeView, TwoFactorCodeVerifyView + PasswordResetView, + dologin, + dologout, + VerifySMSCodeView, + ResendSMSCodeView, + TwoFactorCodeVerifyView, ) from .base import RootDispatchView from .dashboard import DashboardView -from .user import ( - api_access, index, preferences, profile, security) +from .user import api_access, index, preferences, profile, security __all__ = [ "DashboardView", @@ -32,5 +39,5 @@ __all__ = [ "preferences", "profile", "viewsettings", - "security" + "security", ] diff --git a/modoboa/core/views/admin.py b/modoboa/core/views/admin.py index b4681dec7..7cd2bb21b 100644 --- a/modoboa/core/views/admin.py +++ b/modoboa/core/views/admin.py @@ -18,9 +18,7 @@ from .. import signals @login_required @user_passes_test(lambda u: u.is_superuser) def viewsettings(request, tplname="core/settings_header.html"): - return render(request, tplname, { - "selection": "settings" - }) + return render(request, tplname, {"selection": "settings"}) @login_required @@ -29,7 +27,8 @@ def parameters(request, tplname="core/parameters.html"): """View to display and save global parameters.""" if request.method == "POST": forms = param_tools.registry.get_forms( - "global", request.POST, localconfig=request.localconfig) + "global", request.POST, localconfig=request.localconfig + ) for formdef in forms: form = formdef["form"] if form.is_valid(): @@ -41,40 +40,52 @@ def parameters(request, tplname="core/parameters.html"): ) request.localconfig.save() return render_to_json_response(_("Parameters saved")) - return render_to_json_response({ - "left_selection": "parameters", - "content": render_to_string(tplname, { - "forms": param_tools.registry.get_forms( - "global", localconfig=request.localconfig) - }, request) - }) + return render_to_json_response( + { + "left_selection": "parameters", + "content": render_to_string( + tplname, + { + "forms": param_tools.registry.get_forms( + "global", localconfig=request.localconfig + ) + }, + request, + ), + } + ) @login_required @user_passes_test(lambda u: u.is_superuser) def information(request, tplname="core/information.html"): status, extensions = check_for_updates() - return render_to_json_response({ - "content": render_to_string(tplname, { - "update_avail": status, - "extensions": extensions, - }), - }) + return render_to_json_response( + { + "content": render_to_string( + tplname, + { + "update_avail": status, + "extensions": extensions, + }, + ), + } + ) def get_logs_page(request, page_id=None): """Return a page of logs.""" sort_order, sort_dir = get_sort_order( - request.GET, "date_created", - allowed_values=["date_created", "level", "logger", "message"] + request.GET, + "date_created", + allowed_values=["date_created", "level", "logger", "message"], ) if page_id is None: page_id = request.GET.get("page", None) if page_id is None: return None return get_listing_page( - Log.objects.all().order_by("%s%s" % (sort_dir, sort_order)), - page_id + Log.objects.all().order_by("%s%s" % (sort_dir, sort_order)), page_id ) @@ -87,11 +98,13 @@ def logs(request, tplname="core/logs.html"): """ page = get_logs_page(request, 1) - return render_to_json_response({ - "callback": "logs", - "content": render_to_string(tplname, {"logs": page.object_list}), - "page": page.number - }) + return render_to_json_response( + { + "callback": "logs", + "content": render_to_string(tplname, {"logs": page.object_list}), + "page": page.number, + } + ) @login_required @@ -104,7 +117,7 @@ def logs_page(request, tplname="core/logs_page.html"): else: context = { "rows": render_to_string(tplname, {"logs": page.object_list}), - "pages": [page.number] + "pages": [page.number], } return render_to_json_response(context) @@ -113,7 +126,7 @@ def logs_page(request, tplname="core/logs_page.html"): def check_top_notifications(request): """AJAX service to check for new top notifications to display.""" notifications = signals.get_top_notifications.send( - sender="top_notifications", include_all=True) - notifications = reduce( - lambda a, b: a + b, [notif[1] for notif in notifications]) + sender="top_notifications", include_all=True + ) + notifications = reduce(lambda a, b: a + b, [notif[1] for notif in notifications]) return render_to_json_response(notifications) diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py index 3cd254d1d..708708064 100644 --- a/modoboa/core/views/auth.py +++ b/modoboa/core/views/auth.py @@ -5,8 +5,7 @@ import logging import oath from django.conf import settings -from django.http import ( - HttpResponse, HttpResponseRedirect, Http404, JsonResponse) +from django.http import HttpResponse, HttpResponseRedirect, Http404, JsonResponse from django.template.loader import render_to_string from django.urls import reverse from django.utils import translation @@ -18,9 +17,7 @@ from django.views import generic from django.views.decorators.cache import never_cache from django.views.decorators.http import require_http_methods -from django.contrib.auth import ( - authenticate, login, logout, views as auth_views -) +from django.contrib.auth import authenticate, login, logout, views as auth_views from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.tokens import default_token_generator @@ -47,35 +44,38 @@ def dologin(request): form = forms.LoginForm(request.POST) if form.is_valid(): logger = logging.getLogger("modoboa.auth") - user = authenticate(username=form.cleaned_data["username"], - password=form.cleaned_data["password"]) + user = authenticate( + username=form.cleaned_data["username"], + password=form.cleaned_data["password"], + ) if user and user.is_active: - condition = ( - user.is_local and - param_tools.get_global_parameter( - "update_scheme", raise_exception=False) + condition = user.is_local and param_tools.get_global_parameter( + "update_scheme", raise_exception=False ) if condition: # check if password scheme is correct scheme = param_tools.get_global_parameter( - "password_scheme", raise_exception=False) + "password_scheme", raise_exception=False + ) # use SHA512CRYPT as default fallback if scheme is None: - pwhash = get_password_hasher('sha512crypt')() + pwhash = get_password_hasher("sha512crypt")() else: pwhash = get_password_hasher(scheme)() if not user.password.startswith(pwhash.scheme): logging.info( _("Password scheme mismatch. Updating %s password"), - user.username + user.username, ) user.set_password(form.cleaned_data["password"]) user.save() if pwhash.needs_rehash(user.password): logging.info( - _("Password hash parameter missmatch. " - "Updating %s password"), - user.username + _( + "Password hash parameter missmatch. " + "Updating %s password" + ), + user.username, ) user.set_password(form.cleaned_data["password"]) user.save() @@ -86,23 +86,25 @@ def dologin(request): translation.activate(request.user.language) - logger.info( - _("User '%s' successfully logged in") % user.username - ) + logger.info(_("User '%s' successfully logged in") % user.username) signals.user_login.send( sender="dologin", username=form.cleaned_data["username"], - password=form.cleaned_data["password"]) + password=form.cleaned_data["password"], + ) response = HttpResponseRedirect(find_nextlocation(request, user)) - response.set_cookie(settings.LANGUAGE_COOKIE_NAME, request.user.language) + response.set_cookie( + settings.LANGUAGE_COOKIE_NAME, request.user.language + ) return response - error = _( - "Your username and password didn't match. Please try again.") + error = _("Your username and password didn't match. Please try again.") logger.warning( "Failed connection attempt from '%(addr)s' as user '%(user)s'" - % {"addr": request.META["REMOTE_ADDR"], - "user": escape(form.cleaned_data["username"])} + % { + "addr": request.META["REMOTE_ADDR"], + "user": escape(form.cleaned_data["username"]), + } ) nextlocation = request.POST.get("next", "") @@ -112,16 +114,21 @@ def dologin(request): nextlocation = request.GET.get("next", "") httpcode = 200 - announcements = signals.get_announcements.send( - sender="login", location="loginpage") + announcements = signals.get_announcements.send(sender="login", location="loginpage") announcements = [announcement[1] for announcement in announcements] return HttpResponse( render_to_string( - "registration/login.html", { - "form": form, "error": error, "next": nextlocation, - "annoucements": announcements}, - request), - status=httpcode) + "registration/login.html", + { + "form": form, + "error": error, + "next": nextlocation, + "annoucements": announcements, + }, + request, + ), + status=httpcode, + ) dologin = never_cache(dologin) @@ -134,8 +141,8 @@ def dologout(request): signals.user_logout.send(sender="dologout", request=request) logger = logging.getLogger("modoboa.auth") logger.info( - _("User '{}' successfully logged out").format( - request.user.username)) + _("User '{}' successfully logged out").format(request.user.username) + ) logout(request) return HttpResponseRedirect(reverse("core:login")) @@ -147,24 +154,20 @@ class PasswordResetView(auth_views.PasswordResetView): def setup(self, request, *args, **kwargs): super().setup(request, *args, **kwargs) - self.from_email = request.localconfig.parameters.get_value( - "sender_address" - ) + self.from_email = request.localconfig.parameters.get_value("sender_address") def get_context_data(self, **kwargs): """Include help text.""" context = super().get_context_data(**kwargs) - context["announcement"] = ( - self.request.localconfig.parameters - .get_value("password_recovery_msg") + context["announcement"] = self.request.localconfig.parameters.get_value( + "password_recovery_msg" ) return context def form_valid(self, form): """Redirect to code verification page if needed.""" - sms_password_recovery = ( - self.request.localconfig.parameters - .get_value("sms_password_recovery") + sms_password_recovery = self.request.localconfig.parameters.get_value( + "sms_password_recovery" ) if not sms_password_recovery: return super().form_valid(form) @@ -174,13 +177,13 @@ class PasswordResetView(auth_views.PasswordResetView): if not user: # Fallback to email return super().form_valid(form) - backend = sms_backends.get_active_backend( - self.request.localconfig.parameters) + backend = sms_backends.get_active_backend(self.request.localconfig.parameters) secret = cryptutils.random_hex_key(20) code = oath.totp(secret) text = _( - "Please use the following code to recover your Modoboa password: {}" - .format(code) + "Please use the following code to recover your Modoboa password: {}".format( + code + ) ) if not backend.send(text, [str(user.phone_number)]): return super().form_valid(form) @@ -218,24 +221,22 @@ class ResendSMSCodeView(generic.View): """A view to resend validation code.""" def get(self, request, *args, **kwargs): - sms_password_recovery = ( - self.request.localconfig.parameters - .get_value("sms_password_recovery") + sms_password_recovery = self.request.localconfig.parameters.get_value( + "sms_password_recovery" ) if not sms_password_recovery: raise Http404 try: - user = models.User._default_manager.get( - pk=self.request.session["user_pk"]) + user = models.User._default_manager.get(pk=self.request.session["user_pk"]) except KeyError: raise Http404 - backend = sms_backends.get_active_backend( - self.request.localconfig.parameters) + backend = sms_backends.get_active_backend(self.request.localconfig.parameters) secret = cryptutils.random_hex_key(20) code = oath.totp(secret) text = _( - "Please use the following code to recover your Modoboa password: {}" - .format(code) + "Please use the following code to recover your Modoboa password: {}".format( + code + ) ) if not backend.send(text, [user.phone_number]): raise Http404 @@ -243,9 +244,9 @@ class ResendSMSCodeView(generic.View): return JsonResponse({"status": "ok"}) -class TwoFactorCodeVerifyView(LoginRequiredMixin, - UserFormKwargsMixin, - generic.FormView): +class TwoFactorCodeVerifyView( + LoginRequiredMixin, UserFormKwargsMixin, generic.FormView +): """View to verify a 2FA code after login.""" form_class = forms.Verify2FACodeForm @@ -254,6 +255,4 @@ class TwoFactorCodeVerifyView(LoginRequiredMixin, def form_valid(self, form): """Login user.""" django_otp.login(self.request, form.cleaned_data["tfa_code"]) - return HttpResponseRedirect( - find_nextlocation(self.request, self.request.user) - ) + return HttpResponseRedirect(find_nextlocation(self.request, self.request.user)) diff --git a/modoboa/core/views/base.py b/modoboa/core/views/base.py index 0de677140..483944af2 100644 --- a/modoboa/core/views/base.py +++ b/modoboa/core/views/base.py @@ -14,15 +14,13 @@ def find_nextlocation(request, user): # Redirect to profile on first login return reverse("core:user_index") nextlocation = request.POST.get("next", request.GET.get("next")) - condition = ( - nextlocation and - url_has_allowed_host_and_scheme(nextlocation, request.get_host()) + condition = nextlocation and url_has_allowed_host_and_scheme( + nextlocation, request.get_host() ) if condition: return nextlocation if request.user.role == "SimpleUsers": - topredir = request.localconfig.parameters.get_value( - "default_top_redirection") + topredir = request.localconfig.parameters.get_value("default_top_redirection") if topredir != "user": infos = exts_pool.get_extension_infos(topredir) nextlocation = infos["topredirection_url"] diff --git a/modoboa/core/views/dashboard.py b/modoboa/core/views/dashboard.py index 5443e9f90..0597bf822 100644 --- a/modoboa/core/views/dashboard.py +++ b/modoboa/core/views/dashboard.py @@ -29,9 +29,7 @@ class DashboardView(auth_mixins.AccessMixin, generic.TemplateView): def get_context_data(self, **kwargs): """Add context variables.""" context = super(DashboardView, self).get_context_data(**kwargs) - context.update({ - "selection": "dashboard", "widgets": {"left": [], "right": []} - }) + context.update({"selection": "dashboard", "widgets": {"left": [], "right": []}}) # Fetch latest news if self.request.user.language == "fr": lang = "fr" @@ -41,8 +39,9 @@ class DashboardView(auth_mixins.AccessMixin, generic.TemplateView): feed_url = "{}{}/weblog/feeds/".format(MODOBOA_WEBSITE_URL, lang) if self.request.user.role != "SuperAdmins": - custom_feed_url = ( - self.request.localconfig.parameters.get_value("rss_feed_url")) + custom_feed_url = self.request.localconfig.parameters.get_value( + "rss_feed_url" + ) if custom_feed_url: feed_url = custom_feed_url entries = [] @@ -55,10 +54,10 @@ class DashboardView(auth_mixins.AccessMixin, generic.TemplateView): context.update({"news": entries}) hide_features_widget = self.request.localconfig.parameters.get_value( - "hide_features_widget") + "hide_features_widget" + ) if self.request.user.is_superuser or not hide_features_widget: - url = "{}{}/api/projects/?featured=true".format( - MODOBOA_WEBSITE_URL, lang) + url = "{}{}/api/projects/?featured=true".format(MODOBOA_WEBSITE_URL, lang) features = [] if not settings.DISABLE_DASHBOARD_EXTERNAL_QUERIES: try: @@ -73,11 +72,11 @@ class DashboardView(auth_mixins.AccessMixin, generic.TemplateView): # Extra widgets result = signals.extra_admin_dashboard_widgets.send( - sender=self.__class__, user=self.request.user) + sender=self.__class__, user=self.request.user + ) for _receiver, widgets in result: for widget in widgets: - context["widgets"][widget["column"]].append( - widget["template"]) + context["widgets"][widget["column"]].append(widget["template"]) # FIXME: can raise conflicts... context.update(widget["context"]) diff --git a/modoboa/core/views/user.py b/modoboa/core/views/user.py index 9c5e6ae52..ce97e1698 100644 --- a/modoboa/core/views/user.py +++ b/modoboa/core/views/user.py @@ -34,27 +34,23 @@ def index(request, tplname="core/user_index.html"): def profile(request, tplname="core/user_profile.html"): """Profile detail/update view.""" update_password = True - results = signals.allow_password_change.send( - sender="profile", user=request.user) + results = signals.allow_password_change.send(sender="profile", user=request.user) if True in [result[1] for result in results]: update_password = False - password_url = request.localconfig.parameters.get_value( - "update_password_url") + password_url = request.localconfig.parameters.get_value("update_password_url") show_password_url = False if not request.user.is_local and password_url: show_password_url = True update_password = False if request.method == "POST": - form = ProfileForm( - update_password, request.POST, instance=request.user - ) + form = ProfileForm(update_password, request.POST, instance=request.user) if form.is_valid(): form.save() if update_password and form.cleaned_data["confirmation"] != "": - request.session["password"] = force_str(encrypt( - form.cleaned_data["confirmation"] - )) + request.session["password"] = force_str( + encrypt(form.cleaned_data["confirmation"]) + ) translation.activate(request.user.language) response = render_to_json_response(_("Profile updated")) response.set_cookie(settings.LANGUAGE_COOKIE_NAME, request.user.language) @@ -62,38 +58,50 @@ def profile(request, tplname="core/user_profile.html"): return render_to_json_response({"form_errors": form.errors}, status=400) form = ProfileForm(update_password, instance=request.user) - return render_to_json_response({ - "content": render_to_string(tplname, { - "form": form, - "show_password_url": show_password_url, - "password_url": password_url - }, request) - }) + return render_to_json_response( + { + "content": render_to_string( + tplname, + { + "form": form, + "show_password_url": show_password_url, + "password_url": password_url, + }, + request, + ) + } + ) @login_required def preferences(request): if request.method == "POST": - forms = param_tools.registry.get_forms( - "user", request.POST, user=request.user) + forms = param_tools.registry.get_forms("user", request.POST, user=request.user) for formdef in forms: form = formdef["form"] if form.is_valid(): form.save() continue - return render_to_json_response({ - "prefix": form.app, "form_errors": form.errors - }, status=400) + return render_to_json_response( + {"prefix": form.app, "form_errors": form.errors}, status=400 + ) request.user.save() return render_to_json_response(_("Preferences saved")) - return render_to_json_response({ - "content": render_to_string("core/user_preferences.html", { - "forms": param_tools.registry.get_forms( - "user", user=request.user, first_app="general") - }, request), - "onload_cb": "preferencesCallback", - }) + return render_to_json_response( + { + "content": render_to_string( + "core/user_preferences.html", + { + "forms": param_tools.registry.get_forms( + "user", user=request.user, first_app="general" + ) + }, + request, + ), + "onload_cb": "preferencesCallback", + } + ) @login_required @@ -108,14 +116,11 @@ def api_access(request): else: Token.objects.filter(user=request.user).delete() return render_to_json_response(_("Access updated")) - return render_to_json_response({ - "form_errors": form.errors - }, status=400) + return render_to_json_response({"form_errors": form.errors}, status=400) form = APIAccessForm(user=request.user) - return render_to_json_response({ - "content": render_to_string( - "core/api_access.html", {"form": form}, request) - }) + return render_to_json_response( + {"content": render_to_string("core/api_access.html", {"form": form}, request)} + ) @login_required @@ -139,8 +144,7 @@ def security(request): img.save(buf) context.update({"qrcode": buf.getvalue().decode()}) resp = { - "content": render_to_string( - "core/user_security.html", context, request), - "callback": "security" + "content": render_to_string("core/user_security.html", context, request), + "callback": "security", } return render_to_json_response(resp) diff --git a/modoboa/dmarc/api/v2/viewsets.py b/modoboa/dmarc/api/v2/viewsets.py index 652df877f..1bbb1455e 100644 --- a/modoboa/dmarc/api/v2/viewsets.py +++ b/modoboa/dmarc/api/v2/viewsets.py @@ -12,7 +12,7 @@ from . import serializers class DMARCViewSet(GetThrottleViewsetMixin, viewsets.GenericViewSet): - permission_classes = (permissions.IsAuthenticated, ) + permission_classes = (permissions.IsAuthenticated,) def get_queryset(self): """Filter queryset based on current user.""" @@ -22,7 +22,7 @@ class DMARCViewSet(GetThrottleViewsetMixin, viewsets.GenericViewSet): methods=["get"], detail=True, serializer_class=serializers.DMARCAligmentSerializer, - url_path="dmarc/alignment_stats" + url_path="dmarc/alignment_stats", ) def alignment_stats(self, request, **kwargs): domain = self.get_object() diff --git a/modoboa/dmarc/constants.py b/modoboa/dmarc/constants.py index 56cfdf7f7..914efdd9a 100644 --- a/modoboa/dmarc/constants.py +++ b/modoboa/dmarc/constants.py @@ -6,5 +6,5 @@ ALLOWED_REASON_TYPES = ( "trusted_forwarder", "mailing_list", "local_policy", - "other" + "other", ) diff --git a/modoboa/dmarc/forms.py b/modoboa/dmarc/forms.py index ca374b925..859a17358 100644 --- a/modoboa/dmarc/forms.py +++ b/modoboa/dmarc/forms.py @@ -16,10 +16,8 @@ class ReportOptionsForm(forms.Form): current_year = forms.IntegerField(widget=forms.widgets.HiddenInput) current_week = forms.IntegerField() - query = forms.ChoiceField( - choices=[("previous", "Previous"), ("next", "Next")]) - resolve_hostnames = forms.BooleanField( - initial=False, required=False) + query = forms.ChoiceField(choices=[("previous", "Previous"), ("next", "Next")]) + resolve_hostnames = forms.BooleanField(initial=False, required=False) def __init__(self, *args, **kwargs): """Constructor.""" @@ -40,36 +38,45 @@ class ParametersForm(param_forms.AdminParametersForm): enable_rlookups = form_utils.YesNoField( label=_("Enable reverse lookups"), initial=False, - help_text=_( - "Enable reverse DNS lookups (reports will be longer to display)" - ) + help_text=_("Enable reverse DNS lookups (reports will be longer to display)"), ) -GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([ - ("dns", { - "label": _("DNS settings"), - "params": collections.OrderedDict([ - ("enable_rlookups", { - "label": _("Enable reverse lookups"), - "help_text": _( - "Enable reverse DNS lookups (reports will be longer to display)" - ) - }) - ]) - }) -]) +GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict( + [ + ( + "dns", + { + "label": _("DNS settings"), + "params": collections.OrderedDict( + [ + ( + "enable_rlookups", + { + "label": _("Enable reverse lookups"), + "help_text": _( + "Enable reverse DNS lookups (reports will be longer to display)" + ), + }, + ) + ] + ), + }, + ) + ] +) def load_settings(): """Load app settings.""" from modoboa.parameters import tools as param_tools - param_tools.registry.add( - "global", ParametersForm, _("DMARC")) + param_tools.registry.add("global", ParametersForm, _("DMARC")) param_tools.registry.add2( - "global", "dmarc", _("DMARC"), + "global", + "dmarc", + _("DMARC"), GLOBAL_PARAMETERS_STRUCT, serializers.DmarcGlobalParametersSerializer, - True + True, ) diff --git a/modoboa/dmarc/handlers.py b/modoboa/dmarc/handlers.py index fcd89e7a7..992bb7a65 100644 --- a/modoboa/dmarc/handlers.py +++ b/modoboa/dmarc/handlers.py @@ -14,9 +14,11 @@ def dmarc_domain_actions(sender, user, domain, **kwargs): """Return a link to access domain report.""" if not models.Record.objects.filter(header_from=domain).exists(): return [] - return [{ - "name": "dmarc_report", - "url": reverse("dmarc:domain_report", args=[domain.pk]), - "title": _("Show DMARC report for {}").format(domain.name), - "img": "fa fa-pie-chart" - }] + return [ + { + "name": "dmarc_report", + "url": reverse("dmarc:domain_report", args=[domain.pk]), + "title": _("Show DMARC report for {}").format(domain.name), + "img": "fa fa-pie-chart", + } + ] diff --git a/modoboa/dmarc/lib.py b/modoboa/dmarc/lib.py index ee9e9aa63..565cfacd5 100644 --- a/modoboa/dmarc/lib.py +++ b/modoboa/dmarc/lib.py @@ -66,8 +66,7 @@ def import_record(xml_node, report): header_from = identifiers.find("header_from").text.split(".") domain = None while len(header_from) >= 2: - domain = admin_models.Domain.objects.filter( - name=".".join(header_from)).first() + domain = admin_models.Domain.objects.filter(name=".".join(header_from)).first() if domain is not None: record.header_from = domain break @@ -83,8 +82,11 @@ def import_record(xml_node, report): if not rnode: continue models.Result.objects.create( - record=record, type=rtype, domain=rnode.find("domain").text, - result=rnode.find("result").text) + record=record, + type=rtype, + domain=rnode.find("domain").text, + result=rnode.find("result").text, + ) @transaction.atomic @@ -94,15 +96,16 @@ def import_report(content): metadata = root.find("report_metadata") print( "Importing report {} received from {}".format( - metadata.find("report_id").text, - metadata.find("org_name").text) + metadata.find("report_id").text, metadata.find("org_name").text + ) ) reporter, created = models.Reporter.objects.get_or_create( email=metadata.find("email").text, - defaults={"org_name": metadata.find("org_name").text} + defaults={"org_name": metadata.find("org_name").text}, ) qs = models.Report.objects.filter( - reporter=reporter, report_id=metadata.find("report_id").text) + reporter=reporter, report_id=metadata.find("report_id").text + ) if qs.exists(): print("Report already imported.") return @@ -122,7 +125,7 @@ def import_report(content): node = policy_published.find(attr) if node is None or not node.text: if attr == "sp": - node = fromstring('unstated', forbid_dtd=True) + node = fromstring("unstated", forbid_dtd=True) else: print(f"Report skipped because of malformed data (empty {attr})") return @@ -165,12 +168,14 @@ def import_report_from_email(content): fpo = six.BytesIO(part.get_payload(decode=True)) # Try to get the actual file type of the buffer # required to make sure we are dealing with an XML file - file_type = magic.Magic(uncompress=True, mime=True).from_buffer(fpo.read(2048)) + file_type = magic.Magic(uncompress=True, mime=True).from_buffer( + fpo.read(2048) + ) fpo.seek(0) if file_type in FILE_TYPES: import_archive(fpo, content_type=part.get_content_type()) except (OSError, IOError): - print('Error: the attachment does not match the mimetype') + print("Error: the attachment does not match the mimetype") err = True else: fpo.close() @@ -214,10 +219,8 @@ def week_range(year, weeknumber): """Return start and end dates of a given week.""" tz = timezone.get_current_timezone() fmt = "%Y-%W-%w" - start_week = datetime.datetime.strptime( - "{}-{}-{}".format(year, weeknumber, 1), fmt) - end_week = datetime.datetime.strptime( - "{}-{}-{}".format(year, weeknumber, 0), fmt) + start_week = datetime.datetime.strptime("{}-{}-{}".format(year, weeknumber, 1), fmt) + end_week = datetime.datetime.strptime("{}-{}-{}".format(year, weeknumber, 0), fmt) return start_week.replace(tzinfo=tz), end_week.replace(tzinfo=tz) @@ -230,7 +233,7 @@ def insert_record(target: dict, record, name: str) -> None: target[name][record.source_ip] = { "total": 0, "spf": {"success": 0, "failure": 0}, - "dkim": {"success": 0, "failure": 0} + "dkim": {"success": 0, "failure": 0}, } target[name][record.source_ip]["total"] += record.count for typ in ["spf", "dkim"]: @@ -252,19 +255,11 @@ def get_aligment_stats(domain, period=None) -> dict: daterange = week_range(year, week) qargs = ( - (Q(report__start_date__gte=daterange[0], - report__start_date__lte=daterange[1]) | - Q(report__end_date__gte=daterange[0], - report__end_date__lte=daterange[1])) & - Q(header_from=domain) - ) + Q(report__start_date__gte=daterange[0], report__start_date__lte=daterange[1]) + | Q(report__end_date__gte=daterange[0], report__end_date__lte=daterange[1]) + ) & Q(header_from=domain) all_records = models.Record.objects.filter(qargs) - stats: dict = { - "aligned": {}, - "trusted": {}, - "forwarded": {}, - "failed": {} - } + stats: dict = {"aligned": {}, "trusted": {}, "forwarded": {}, "failed": {}} dns_names = {} if param_tools.get_global_parameter("enable_rlookups"): @@ -279,16 +274,21 @@ def get_aligment_stats(domain, period=None) -> dict: ext = tldextract.extract(str(resp[0].target)) if not ext.suffix: # invalid PTR record raise resolver.NXDOMAIN() - return (ip, '.'.join((ext.domain, ext.suffix)).lower()) - except (resolver.NXDOMAIN, resolver.YXDOMAIN, - resolver.NoAnswer, resolver.NoNameservers, - resolver.Timeout): + return (ip, ".".join((ext.domain, ext.suffix)).lower()) + except ( + resolver.NXDOMAIN, + resolver.YXDOMAIN, + resolver.NoAnswer, + resolver.NoNameservers, + resolver.Timeout, + ): return (None, None) ips = (r.source_ip for r in all_records) with concurrent.futures.ThreadPoolExecutor(max_workers=16) as pool: - dns_names = {i: n for (i, n) in - list(pool.map(get_domain_name_from_ip, ips))} + dns_names = { + i: n for (i, n) in list(pool.map(get_domain_name_from_ip, ips)) + } for record in all_records: name = dns_names.get(record.source_ip, _("Not resolved")) @@ -296,7 +296,9 @@ def get_aligment_stats(domain, period=None) -> dict: insert_record(stats["aligned"], record, name) elif record.dkim_result == "pass" or record.spf_result == "pass": insert_record(stats["trusted"], record, name) - elif record.reason_type == "local_policy" and record.reason_comment.startswith("arc=pass"): + elif record.reason_type == "local_policy" and record.reason_comment.startswith( + "arc=pass" + ): insert_record(stats["forwarded"], record, name) else: insert_record(stats["failed"], record, name) diff --git a/modoboa/dmarc/management/commands/import_aggregated_report.py b/modoboa/dmarc/management/commands/import_aggregated_report.py index 28d211ee9..fd16c7ac0 100644 --- a/modoboa/dmarc/management/commands/import_aggregated_report.py +++ b/modoboa/dmarc/management/commands/import_aggregated_report.py @@ -15,20 +15,24 @@ class Command(BaseCommand): def add_arguments(self, parser): """Add extra arguments to parser.""" parser.add_argument( - "--pipe", action="store_true", default=False, - help="Import a report from an email given on stdin") + "--pipe", + action="store_true", + default=False, + help="Import a report from an email given on stdin", + ) parser.add_argument( - "--imap", action="store_true", default=False, - help="Import a report from an IMAP mailbox") + "--imap", + action="store_true", + default=False, + help="Import a report from an IMAP mailbox", + ) + parser.add_argument("--host", default="localhost", help="IMAP host") parser.add_argument( - "--host", default="localhost", - help="IMAP host") + "--ssl", action="store_true", default=False, help="Connect using SSL" + ), parser.add_argument( - "--ssl", action="store_true", default=False, - help="Connect using SSL"), - parser.add_argument( - "--mailbox", default="INBOX", - help="IMAP mailbox to import reports from") + "--mailbox", default="INBOX", help="IMAP mailbox to import reports from" + ) def handle(self, *args, **options): """Entry point.""" diff --git a/modoboa/dmarc/migrations/0001_initial.py b/modoboa/dmarc/migrations/0001_initial.py index 4ce5a8b2a..a9be64344 100644 --- a/modoboa/dmarc/migrations/0001_initial.py +++ b/modoboa/dmarc/migrations/0001_initial.py @@ -7,84 +7,162 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('admin', '0002_migrate_from_modoboa_admin'), + ("admin", "0002_migrate_from_modoboa_admin"), ] operations = [ migrations.CreateModel( - name='Record', + name="Record", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('source_ip', models.GenericIPAddressField()), - ('count', models.IntegerField()), - ('disposition', models.CharField(max_length=10, choices=[(b'none', 'None'), (b'quarantine', 'Quarantine'), (b'reject', 'Reject')])), - ('dkim_result', models.CharField(max_length=9, choices=[(b'none', 'None'), (b'neutral', 'Neutral'), (b'pass', 'Pass'), (b'fail', 'Fail'), (b'temperror', 'Temporary error'), (b'permerror', 'Permanent error'), (b'policy', 'Policy')])), - ('spf_result', models.CharField(max_length=9, choices=[(b'none', 'None'), (b'neutral', 'Neutral'), (b'pass', 'Pass'), (b'fail', 'Fail'), (b'temperror', 'Temporary error'), (b'permerror', 'Permanent error'), (b'softfail', 'Soft failure')])), - ('reason_type', models.CharField(max_length=15, blank=True)), - ('reason_comment', models.CharField(max_length=100, blank=True)), - ('header_from', models.ForeignKey(to='admin.Domain', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("source_ip", models.GenericIPAddressField()), + ("count", models.IntegerField()), + ( + "disposition", + models.CharField( + max_length=10, + choices=[ + (b"none", "None"), + (b"quarantine", "Quarantine"), + (b"reject", "Reject"), + ], + ), + ), + ( + "dkim_result", + models.CharField( + max_length=9, + choices=[ + (b"none", "None"), + (b"neutral", "Neutral"), + (b"pass", "Pass"), + (b"fail", "Fail"), + (b"temperror", "Temporary error"), + (b"permerror", "Permanent error"), + (b"policy", "Policy"), + ], + ), + ), + ( + "spf_result", + models.CharField( + max_length=9, + choices=[ + (b"none", "None"), + (b"neutral", "Neutral"), + (b"pass", "Pass"), + (b"fail", "Fail"), + (b"temperror", "Temporary error"), + (b"permerror", "Permanent error"), + (b"softfail", "Soft failure"), + ], + ), + ), + ("reason_type", models.CharField(max_length=15, blank=True)), + ("reason_comment", models.CharField(max_length=100, blank=True)), + ( + "header_from", + models.ForeignKey(to="admin.Domain", on_delete=models.CASCADE), + ), + ], + options={}, + bases=(models.Model,), + ), + migrations.CreateModel( + name="Report", + fields=[ + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("report_id", models.CharField(max_length=100)), + ("start_date", models.DateTimeField()), + ("end_date", models.DateTimeField()), + ("policy_domain", models.CharField(max_length=100)), + ("policy_adkim", models.CharField(max_length=1)), + ("policy_aspf", models.CharField(max_length=1)), + ("policy_p", models.CharField(max_length=10)), + ("policy_sp", models.CharField(max_length=10)), + ("policy_pct", models.SmallIntegerField()), ], options={ + "permissions": (("view_report", "Can view report"),), }, bases=(models.Model,), ), migrations.CreateModel( - name='Report', + name="Reporter", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('report_id', models.CharField(max_length=100)), - ('start_date', models.DateTimeField()), - ('end_date', models.DateTimeField()), - ('policy_domain', models.CharField(max_length=100)), - ('policy_adkim', models.CharField(max_length=1)), - ('policy_aspf', models.CharField(max_length=1)), - ('policy_p', models.CharField(max_length=10)), - ('policy_sp', models.CharField(max_length=10)), - ('policy_pct', models.SmallIntegerField()), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("org_name", models.CharField(max_length=100)), + ("email", models.EmailField(unique=True, max_length=254)), ], - options={ - 'permissions': (('view_report', 'Can view report'),), - }, + options={}, bases=(models.Model,), ), migrations.CreateModel( - name='Reporter', + name="Result", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('org_name', models.CharField(max_length=100)), - ('email', models.EmailField(unique=True, max_length=254)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "type", + models.CharField( + max_length=4, choices=[(b"dkim", b"DKIM"), (b"spf", b"SPF")] + ), + ), + ("domain", models.CharField(max_length=100)), + ("result", models.CharField(max_length=9)), + ( + "record", + models.ForeignKey(to="dmarc.Record", on_delete=models.CASCADE), + ), ], - options={ - }, - bases=(models.Model,), - ), - migrations.CreateModel( - name='Result', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('type', models.CharField(max_length=4, choices=[(b'dkim', b'DKIM'), (b'spf', b'SPF')])), - ('domain', models.CharField(max_length=100)), - ('result', models.CharField(max_length=9)), - ('record', models.ForeignKey(to='dmarc.Record', on_delete=models.CASCADE)), - ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.AddField( - model_name='report', - name='reporter', - field=models.ForeignKey(to='dmarc.Reporter', on_delete=models.CASCADE), + model_name="report", + name="reporter", + field=models.ForeignKey(to="dmarc.Reporter", on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterUniqueTogether( - name='report', - unique_together=set([('reporter', 'report_id')]), + name="report", + unique_together=set([("reporter", "report_id")]), ), migrations.AddField( - model_name='record', - name='report', - field=models.ForeignKey(to='dmarc.Report', on_delete=models.CASCADE), + model_name="record", + name="report", + field=models.ForeignKey(to="dmarc.Report", on_delete=models.CASCADE), preserve_default=True, ), ] diff --git a/modoboa/dmarc/migrations/0002_auto_20180124_2311.py b/modoboa/dmarc/migrations/0002_auto_20180124_2311.py index bcaee13bc..b55e07d27 100644 --- a/modoboa/dmarc/migrations/0002_auto_20180124_2311.py +++ b/modoboa/dmarc/migrations/0002_auto_20180124_2311.py @@ -8,28 +8,59 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('dmarc', '0001_initial'), + ("dmarc", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='record', - name='disposition', - field=models.CharField(choices=[('none', 'None'), ('quarantine', 'Quarantine'), ('reject', 'Reject')], max_length=10), + model_name="record", + name="disposition", + field=models.CharField( + choices=[ + ("none", "None"), + ("quarantine", "Quarantine"), + ("reject", "Reject"), + ], + max_length=10, + ), ), migrations.AlterField( - model_name='record', - name='dkim_result', - field=models.CharField(choices=[('none', 'None'), ('neutral', 'Neutral'), ('pass', 'Pass'), ('fail', 'Fail'), ('temperror', 'Temporary error'), ('permerror', 'Permanent error'), ('policy', 'Policy')], max_length=9), + model_name="record", + name="dkim_result", + field=models.CharField( + choices=[ + ("none", "None"), + ("neutral", "Neutral"), + ("pass", "Pass"), + ("fail", "Fail"), + ("temperror", "Temporary error"), + ("permerror", "Permanent error"), + ("policy", "Policy"), + ], + max_length=9, + ), ), migrations.AlterField( - model_name='record', - name='spf_result', - field=models.CharField(choices=[('none', 'None'), ('neutral', 'Neutral'), ('pass', 'Pass'), ('fail', 'Fail'), ('temperror', 'Temporary error'), ('permerror', 'Permanent error'), ('softfail', 'Soft failure')], max_length=9), + model_name="record", + name="spf_result", + field=models.CharField( + choices=[ + ("none", "None"), + ("neutral", "Neutral"), + ("pass", "Pass"), + ("fail", "Fail"), + ("temperror", "Temporary error"), + ("permerror", "Permanent error"), + ("softfail", "Soft failure"), + ], + max_length=9, + ), ), migrations.AlterField( - model_name='result', - name='type', - field=models.CharField(choices=[('dkim', 'DKIM'), ('spf', 'SPF')], max_length=4), + model_name="result", + name="type", + field=models.CharField( + choices=[("dkim", "DKIM"), ("spf", "SPF")], max_length=4 + ), ), ] diff --git a/modoboa/dmarc/migrations/0003_auto_20200227_0936.py b/modoboa/dmarc/migrations/0003_auto_20200227_0936.py index ec57fc0c5..90a6fc871 100644 --- a/modoboa/dmarc/migrations/0003_auto_20200227_0936.py +++ b/modoboa/dmarc/migrations/0003_auto_20200227_0936.py @@ -6,12 +6,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('dmarc', '0002_auto_20180124_2311'), + ("dmarc", "0002_auto_20180124_2311"), ] operations = [ migrations.AlterModelOptions( - name='report', + name="report", options={}, ), ] diff --git a/modoboa/dmarc/migrations/0004_auto_20220926_0913.py b/modoboa/dmarc/migrations/0004_auto_20220926_0913.py index 287dd0e91..73b236c42 100644 --- a/modoboa/dmarc/migrations/0004_auto_20220926_0913.py +++ b/modoboa/dmarc/migrations/0004_auto_20220926_0913.py @@ -6,28 +6,36 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('dmarc', '0003_auto_20200227_0936'), + ("dmarc", "0003_auto_20200227_0936"), ] operations = [ migrations.AlterField( - model_name='record', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + model_name="record", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( - model_name='report', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + model_name="report", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( - model_name='reporter', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + model_name="reporter", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( - model_name='result', - name='id', - field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + model_name="result", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), ] diff --git a/modoboa/dmarc/migrations/0005_auto_20230418_1201.py b/modoboa/dmarc/migrations/0005_auto_20230418_1201.py index 548179458..89fac5ea1 100644 --- a/modoboa/dmarc/migrations/0005_auto_20230418_1201.py +++ b/modoboa/dmarc/migrations/0005_auto_20230418_1201.py @@ -6,28 +6,36 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('dmarc', '0004_auto_20220926_0913'), + ("dmarc", "0004_auto_20220926_0913"), ] operations = [ migrations.AlterField( - model_name='record', - name='id', - field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + model_name="record", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( - model_name='report', - name='id', - field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + model_name="report", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( - model_name='reporter', - name='id', - field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + model_name="reporter", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), migrations.AlterField( - model_name='result', - name='id', - field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + model_name="result", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), ), ] diff --git a/modoboa/dmarc/models.py b/modoboa/dmarc/models.py index baf57a849..08fef1f78 100644 --- a/modoboa/dmarc/models.py +++ b/modoboa/dmarc/models.py @@ -9,7 +9,7 @@ from modoboa.admin import models as admin_models POLICY_DISPOSITIONS = [ ("none", _("None")), ("quarantine", _("Quarantine")), - ("reject", _("Reject")) + ("reject", _("Reject")), ] COMMON_RESULTS = [ @@ -18,16 +18,12 @@ COMMON_RESULTS = [ ("pass", _("Pass")), ("fail", _("Fail")), ("temperror", _("Temporary error")), - ("permerror", _("Permanent error")) + ("permerror", _("Permanent error")), ] -DKIM_RESULTS = COMMON_RESULTS + [ - ("policy", _("Policy")) -] +DKIM_RESULTS = COMMON_RESULTS + [("policy", _("Policy"))] -SPF_RESULTS = COMMON_RESULTS + [ - ("softfail", _("Soft failure")) -] +SPF_RESULTS = COMMON_RESULTS + [("softfail", _("Soft failure"))] RECORD_TYPES = [ ("dkim", "DKIM"), @@ -36,7 +32,6 @@ RECORD_TYPES = [ class Reporter(models.Model): - """Report issuers.""" org_name = models.CharField(max_length=100) @@ -48,7 +43,6 @@ class Reporter(models.Model): class Report(models.Model): - """Aggregated reports.""" report_id = models.CharField(max_length=100) @@ -69,12 +63,10 @@ class Report(models.Model): def __str__(self): """Display provider and dates.""" - return "{}: {} -> {}".format( - self.reporter, self.start_date, self.end_date) + return "{}: {} -> {}".format(self.reporter, self.start_date, self.end_date) class Record(models.Model): - """Report records.""" report = models.ForeignKey(Report, on_delete=models.CASCADE) @@ -82,20 +74,15 @@ class Record(models.Model): count = models.IntegerField() # Policy evaluated - disposition = models.CharField( - max_length=10, choices=POLICY_DISPOSITIONS) - dkim_result = models.CharField( - max_length=9, choices=DKIM_RESULTS) - spf_result = models.CharField( - max_length=9, choices=SPF_RESULTS) - header_from = models.ForeignKey(admin_models.Domain, - on_delete=models.CASCADE) + disposition = models.CharField(max_length=10, choices=POLICY_DISPOSITIONS) + dkim_result = models.CharField(max_length=9, choices=DKIM_RESULTS) + spf_result = models.CharField(max_length=9, choices=SPF_RESULTS) + header_from = models.ForeignKey(admin_models.Domain, on_delete=models.CASCADE) reason_type = models.CharField(max_length=15, blank=True) reason_comment = models.CharField(max_length=100, blank=True) class Result(models.Model): - """Record results.""" record = models.ForeignKey(Record, on_delete=models.CASCADE) diff --git a/modoboa/dmarc/templatetags/dmarc_tags.py b/modoboa/dmarc/templatetags/dmarc_tags.py index 7112ee490..74c5b2eb4 100644 --- a/modoboa/dmarc/templatetags/dmarc_tags.py +++ b/modoboa/dmarc/templatetags/dmarc_tags.py @@ -16,17 +16,22 @@ def next_period(period): """Return next period.""" current = period.split("-") if int(current[1]) == 52: - week0 = datetime.datetime.strptime("{}-0-1".format(int(current[0]) + 1 ), "%Y-%W-%w") - week1 = datetime.datetime.strptime("{}-1-1".format(int(current[0]) + 1 ), "%Y-%W-%w") + week0 = datetime.datetime.strptime( + "{}-0-1".format(int(current[0]) + 1), "%Y-%W-%w" + ) + week1 = datetime.datetime.strptime( + "{}-1-1".format(int(current[0]) + 1), "%Y-%W-%w" + ) week52 = datetime.datetime.strptime("{}-52-1".format(current[0]), "%Y-%W-%w") - if week0 == week52 or week0 == week1 : - parts = (int(current[0]) + 1, 1 ) + if week0 == week52 or week0 == week1: + parts = (int(current[0]) + 1, 1) else: - parts = (int(current[0]) + 1, 0 ) + parts = (int(current[0]) + 1, 0) else: - parts = (current[0], int(current[1]) + 1 ) + parts = (current[0], int(current[1]) + 1) return mark_safe("{}-{}".format(parts[0], parts[1])) + @register.simple_tag def previous_period(period): """Return previous period.""" @@ -35,18 +40,20 @@ def previous_period(period): week0 = datetime.datetime.strptime("{}-0-1".format(current[0]), "%Y-%W-%w") week1 = datetime.datetime.strptime("{}-1-1".format(current[0]), "%Y-%W-%w") if week0 == week1: - parts = (int(current[0]) - 1, 52 ) + parts = (int(current[0]) - 1, 52) else: - parts = (current[0], 0 ) + parts = (current[0], 0) elif int(current[1]) == 0: week0 = datetime.datetime.strptime("{}-0-1".format(current[0]), "%Y-%W-%w") - week52 = datetime.datetime.strptime("{}-52-1".format(int(current[0]) - 1 ), "%Y-%W-%w") + week52 = datetime.datetime.strptime( + "{}-52-1".format(int(current[0]) - 1), "%Y-%W-%w" + ) if week0 == week52: - parts = (int(current[0]) - 1, 51 ) + parts = (int(current[0]) - 1, 51) else: - parts = (int(current[0]) - 1, 52 ) + parts = (int(current[0]) - 1, 52) else: - parts = (current[0], int(current[1]) - 1 ) + parts = (current[0], int(current[1]) - 1) return mark_safe("{}-{}".format(parts[0], parts[1])) @@ -55,7 +62,8 @@ def domain_sorted_items(domain_dict): """Return a list of tuples ordered alphabetically by domain names.""" if isinstance(domain_dict, dict): sorted_domain_dict = OrderedDict( - sorted(domain_dict.items(), key=lambda t: t[0])) + sorted(domain_dict.items(), key=lambda t: t[0]) + ) unresolved_label = _("Not resolved") unresolved = sorted_domain_dict.pop(unresolved_label, None) if unresolved: diff --git a/modoboa/dmarc/tests/mixins.py b/modoboa/dmarc/tests/mixins.py index 29c3a5de7..3f27def1a 100644 --- a/modoboa/dmarc/tests/mixins.py +++ b/modoboa/dmarc/tests/mixins.py @@ -43,5 +43,5 @@ class CallCommandMixin(object): fpath = os.path.join(path, f) if f.startswith(".") or not os.path.isfile(fpath): continue - with self.assertRaisesMessage(SystemExit, '65'): + with self.assertRaisesMessage(SystemExit, "65"): self.import_report(fpath) diff --git a/modoboa/dmarc/tests/test_management_command.py b/modoboa/dmarc/tests/test_management_command.py index 7a4b9fc33..1a23be9ed 100644 --- a/modoboa/dmarc/tests/test_management_command.py +++ b/modoboa/dmarc/tests/test_management_command.py @@ -21,8 +21,8 @@ class ManagementCommandTestCase(mixins.CallCommandMixin, ModoTestCase): self.import_fail_reports() self.assertTrue(self.domain.record_set.exists()) self.assertTrue( - models.Reporter.objects.filter( - org_name="FastMail Pty Ltd").exists()) + models.Reporter.objects.filter(org_name="FastMail Pty Ltd").exists() + ) # Ensure that reports from Yahoo are processed successfully. # These do not contain a sp attribute in policy_published self.assertTrue( diff --git a/modoboa/dmarc/urls.py b/modoboa/dmarc/urls.py index be59ad665..8c9859f92 100644 --- a/modoboa/dmarc/urls.py +++ b/modoboa/dmarc/urls.py @@ -7,6 +7,5 @@ from . import views app_name = "dmarc" urlpatterns = [ - path("domains//", views.DomainReportView.as_view(), - name="domain_report"), + path("domains//", views.DomainReportView.as_view(), name="domain_report"), ] diff --git a/modoboa/dmarc/views.py b/modoboa/dmarc/views.py index 45dad1161..547308bea 100644 --- a/modoboa/dmarc/views.py +++ b/modoboa/dmarc/views.py @@ -29,7 +29,7 @@ def insert_record(target, record, name): target[name][record.source_ip] = { "total": 0, "spf": {"pass": 0, "fail": 0}, - "dkim": {"pass": 0, "fail": 0} + "dkim": {"pass": 0, "fail": 0}, } target[name][record.source_ip]["total"] += record.count for typ in ["spf", "dkim"]: @@ -41,16 +41,12 @@ def week_range(year, weeknumber): """Return start and end dates of a given week.""" tz = timezone.get_current_timezone() fmt = "%Y-%W-%w" - start_week = datetime.datetime.strptime( - "{}-{}-{}".format(year, weeknumber, 1), fmt) - end_week = datetime.datetime.strptime( - "{}-{}-{}".format(year, weeknumber, 0), fmt) + start_week = datetime.datetime.strptime("{}-{}-{}".format(year, weeknumber, 1), fmt) + end_week = datetime.datetime.strptime("{}-{}-{}".format(year, weeknumber, 0), fmt) return start_week.replace(tzinfo=tz), end_week.replace(tzinfo=tz) -class DomainReportView( - auth_mixins.PermissionRequiredMixin, - generic.TemplateView): +class DomainReportView(auth_mixins.PermissionRequiredMixin, generic.TemplateView): """ListView for Report.""" permission_required = "dmarc.view_report" @@ -69,26 +65,22 @@ class DomainReportView( self.daterange = week_range(year, week) self.domain = admin_models.Domain.objects.get(pk=self.kwargs["pk"]) qargs = ( - (Q(report__start_date__gte=self.daterange[0], - report__start_date__lte=self.daterange[1]) | - Q(report__end_date__gte=self.daterange[0], - report__end_date__lte=self.daterange[1])) & - Q(header_from=self.domain) - ) + Q( + report__start_date__gte=self.daterange[0], + report__start_date__lte=self.daterange[1], + ) + | Q( + report__end_date__gte=self.daterange[0], + report__end_date__lte=self.daterange[1], + ) + ) & Q(header_from=self.domain) return models.Record.objects.select_related().filter(qargs) def get_context_data(self, *args, **kwargs): """Extra context data.""" - context = super(DomainReportView, self).get_context_data( - *args, **kwargs) + context = super(DomainReportView, self).get_context_data(*args, **kwargs) qset = self.get_queryset() - stats = { - "total": 0, - "aligned": 0, - "trusted": 0, - "forwarded": 0, - "failed": 0 - } + stats = {"total": 0, "aligned": 0, "trusted": 0, "forwarded": 0, "failed": 0} aligned = {} trusted = {} forwarded = {} @@ -107,16 +99,21 @@ class DomainReportView( ext = tldextract.extract(str(resp[0].target)) if not ext.suffix: # invalid PTR record raise resolver.NXDOMAIN() - return (ip, '.'.join((ext.domain, ext.suffix)).lower()) - except (resolver.NXDOMAIN, resolver.YXDOMAIN, - resolver.NoAnswer, resolver.NoNameservers, - resolver.Timeout): + return (ip, ".".join((ext.domain, ext.suffix)).lower()) + except ( + resolver.NXDOMAIN, + resolver.YXDOMAIN, + resolver.NoAnswer, + resolver.NoNameservers, + resolver.Timeout, + ): return (None, None) ips = (r.source_ip for r in all_records) with concurrent.futures.ThreadPoolExecutor(max_workers=16) as pool: - dns_names = {i: n for (i, n) in - list(pool.map(get_domain_name_from_ip, ips))} + dns_names = { + i: n for (i, n) in list(pool.map(get_domain_name_from_ip, ips)) + } for record in all_records: stats["total"] += record.count @@ -127,7 +124,10 @@ class DomainReportView( elif record.dkim_result == "pass" or record.spf_result == "pass": stats["trusted"] += record.count insert_record(trusted, record, name) - elif record.reason_type == "local_policy" and record.reason_comment.startswith("arc=pass"): + elif ( + record.reason_type == "local_policy" + and record.reason_comment.startswith("arc=pass") + ): stats["forwarded"] += record.count insert_record(forwarded, record, name) else: @@ -136,16 +136,26 @@ class DomainReportView( pie_data = {} if stats["total"]: - pie_data.update({ - "faligned": stats["aligned"] / float(stats["total"]) * 100, - "paligned": stats["trusted"] / float(stats["total"]) * 100, - "forwarded": stats["forwarded"] / float(stats["total"]) * 100, - "failed": stats["failed"] / float(stats["total"]) * 100 - }) + pie_data.update( + { + "faligned": stats["aligned"] / float(stats["total"]) * 100, + "paligned": stats["trusted"] / float(stats["total"]) * 100, + "forwarded": stats["forwarded"] / float(stats["total"]) * 100, + "failed": stats["failed"] / float(stats["total"]) * 100, + } + ) - context.update({ - "stats": stats, "aligned": aligned, "trusted": trusted, "forwarded": forwarded, "threats": threats, - "period": self.period, "daterange": self.daterange, - "domain": self.domain, "pie_data": pie_data - }) + context.update( + { + "stats": stats, + "aligned": aligned, + "trusted": trusted, + "forwarded": forwarded, + "threats": threats, + "period": self.period, + "daterange": self.daterange, + "domain": self.domain, + "pie_data": pie_data, + } + ) return context diff --git a/modoboa/dnstools/api/v2/serializers.py b/modoboa/dnstools/api/v2/serializers.py index b6f8d90c6..b8df2f1d9 100644 --- a/modoboa/dnstools/api/v2/serializers.py +++ b/modoboa/dnstools/api/v2/serializers.py @@ -46,6 +46,11 @@ class DNSDetailSerializer(serializers.ModelSerializer): class Meta: model = admin_models.Domain fields = ( - "mx_records", "dnsbl_results", "autoconfig_record", - "autodiscover_record", "spf_record", "dkim_record", "dmarc_record" + "mx_records", + "dnsbl_results", + "autoconfig_record", + "autodiscover_record", + "spf_record", + "dkim_record", + "dmarc_record", ) diff --git a/modoboa/dnstools/api/v2/tests.py b/modoboa/dnstools/api/v2/tests.py index 773f3e5c8..55faea932 100644 --- a/modoboa/dnstools/api/v2/tests.py +++ b/modoboa/dnstools/api/v2/tests.py @@ -16,22 +16,24 @@ class DNSViewSetTestCase(ModoAPITestCase): super().setUpTestData() admin_factories.populate_database() cls.spf_rec = factories.DNSRecordFactory( - type="spf", value="v=SPF1 mx -all", is_valid=True, - domain__name="test.com" + type="spf", value="v=SPF1 mx -all", is_valid=True, domain__name="test.com" ) cls.dmarc_rec = factories.DNSRecordFactory( - type="dmarc", value="XXX", is_valid=False, + type="dmarc", + value="XXX", + is_valid=False, error="Not a DMARC record", - domain__name="test.com" + domain__name="test.com", ) cls.dkim_rec = factories.DNSRecordFactory( - type="dkim", value="12345", is_valid=False, + type="dkim", + value="12345", + is_valid=False, error="Public key mismatchs", - domain__name="test.com" + domain__name="test.com", ) cls.ac_rec = factories.DNSRecordFactory( - type="autoconfig", value="1.2.3.4", is_valid=True, - domain__name="test.com" + type="autoconfig", value="1.2.3.4", is_valid=True, domain__name="test.com" ) def test_dns_detail(self): diff --git a/modoboa/dnstools/api/v2/viewsets.py b/modoboa/dnstools/api/v2/viewsets.py index 881844d79..365a9d3cc 100644 --- a/modoboa/dnstools/api/v2/viewsets.py +++ b/modoboa/dnstools/api/v2/viewsets.py @@ -12,15 +12,15 @@ from . import serializers class DNSViewSet(GetThrottleViewsetMixin, viewsets.GenericViewSet): """A viewset to provide extra routes related to DNS information.""" - permission_classes = (permissions.IsAuthenticated, ) + permission_classes = (permissions.IsAuthenticated,) def get_queryset(self): """Filter queryset based on current user.""" return admin_models.Domain.objects.get_for_admin(self.request.user) - @action(methods=["get"], - detail=True, - serializer_class=serializers.DNSDetailSerializer) + @action( + methods=["get"], detail=True, serializer_class=serializers.DNSDetailSerializer + ) def dns_detail(self, request, **kwargs): domain = self.get_object() serializer = self.get_serializer(domain) diff --git a/modoboa/dnstools/apps.py b/modoboa/dnstools/apps.py index cc7cbcead..af3257cd8 100644 --- a/modoboa/dnstools/apps.py +++ b/modoboa/dnstools/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class DnstoolsConfig(AppConfig): - name = 'modoboa.dnstools' + name = "modoboa.dnstools" diff --git a/modoboa/dnstools/constants.py b/modoboa/dnstools/constants.py index c579193ee..1ca6e6a45 100644 --- a/modoboa/dnstools/constants.py +++ b/modoboa/dnstools/constants.py @@ -25,5 +25,5 @@ DMARC_TAGS = { "ri": {"type": "int"}, "rua": {"type": "list", "regex": DMARC_URI_REGEX}, "ruf": {"type": "list", "regex": DMARC_URI_REGEX}, - "sp": {"values": ["none", "quarantine", "reject"]} + "sp": {"values": ["none", "quarantine", "reject"]}, } diff --git a/modoboa/dnstools/lib.py b/modoboa/dnstools/lib.py index e9edbb7f1..eb48b5068 100644 --- a/modoboa/dnstools/lib.py +++ b/modoboa/dnstools/lib.py @@ -16,8 +16,8 @@ def _get_record_type_value(records, rr_type): for record in records: # Multiple strings are separated by a space as described in: # https://tools.ietf.org/html/rfc4408#section-3.1.3 - value = str(record).replace('" "', '').strip('"') - if value.startswith('v={}'.format(rr_type)): + value = str(record).replace('" "', "").strip('"') + if value.startswith("v={}".format(rr_type)): return value return None @@ -25,21 +25,21 @@ def _get_record_type_value(records, rr_type): def get_spf_record(domain): """Return SPF record for domain (if any).""" records = admin_lib.get_dns_records(domain, "TXT") - return _get_record_type_value(records, 'spf1') + return _get_record_type_value(records, "spf1") def get_dkim_record(domain, selector): """Return DKIM records form domain (if any).""" name = "{}._domainkey.{}".format(selector, domain) records = admin_lib.get_dns_records(name, "TXT") - return _get_record_type_value(records, 'DKIM1') + return _get_record_type_value(records, "DKIM1") def get_dmarc_record(domain): """Return DMARC record for domain (if any).""" name = "_dmarc.{}".format(domain) records = admin_lib.get_dns_records(name, "TXT") - return _get_record_type_value(records, 'DMARC1') + return _get_record_type_value(records, "DMARC1") def _get_simple_record(name): @@ -108,8 +108,7 @@ def _check_domain_and_mask(value, mechanism): elif "/" in value: mechanism, mask = value.split("/") else: - raise DNSSyntaxError( - _("Invalid syntax for {} mechanism").format(mechanism)) + raise DNSSyntaxError(_("Invalid syntax for {} mechanism").format(mechanism)) if mask and (not mask.isdigit() or int(mask) > 32): raise DNSSyntaxError(_("Invalid mask found {}").format(mask)) @@ -134,8 +133,7 @@ def _check_simple(value, mechanism): return parts = value.split(":") if len(parts) != 2: - raise DNSSyntaxError( - _("Invalid syntax for {} mechanism").format(mechanism)) + raise DNSSyntaxError(_("Invalid syntax for {} mechanism").format(mechanism)) def check_spf_ptr(value): @@ -180,11 +178,11 @@ def check_spf_syntax(record): if len(modifier) != 2: raise DNSSyntaxError(_("Unknown mechanism {}").format(part)) if modifier[0] not in ["redirect", "exp"]: - raise DNSSyntaxError(_("Unknown modifier {}").format( - modifier[0])) + raise DNSSyntaxError(_("Unknown modifier {}").format(modifier[0])) if modifier[0] in modifiers: - raise DNSSyntaxError(_("Duplicate modifier {} found").format( - modifier[0])) + raise DNSSyntaxError( + _("Duplicate modifier {} found").format(modifier[0]) + ) modifiers.append(modifier[0]) if not len(mechanisms) and not len(modifiers): raise DNSSyntaxError(_("No mechanism found")) @@ -235,11 +233,11 @@ def check_dmarc_tag(tag, value): except ValueError: raise DNSSyntaxError(error + _(" not an integer")) if "min_value" in tdef and value < tdef["min_value"]: - raise DNSSyntaxError( - error + _(" less than {}").format(tdef["min_value"])) + raise DNSSyntaxError(error + _(" less than {}").format(tdef["min_value"])) if "max_value" in tdef and value > tdef["max_value"]: raise DNSSyntaxError( - error + _(" greater than {}").format(tdef["max_value"])) + error + _(" greater than {}").format(tdef["max_value"]) + ) else: check_dmarc_tag_string_value(tag, value) diff --git a/modoboa/dnstools/migrations/0001_initial.py b/modoboa/dnstools/migrations/0001_initial.py index 794895dbb..5ac4a06b9 100644 --- a/modoboa/dnstools/migrations/0001_initial.py +++ b/modoboa/dnstools/migrations/0001_initial.py @@ -9,20 +9,45 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('admin', '0014_auto_20181017_1628'), + ("admin", "0014_auto_20181017_1628"), ] operations = [ migrations.CreateModel( - name='DNSRecord', + name="DNSRecord", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('type', models.CharField(choices=[('spf', 'SPF'), ('dkim', 'DKIM'), ('dmarc', 'DMARC'), ('autoconfig', 'Autoconfig'), ('autodiscover', 'Autodiscover')], max_length=15)), - ('value', models.TextField(blank=True)), - ('is_valid', models.BooleanField(default=False)), - ('error', models.CharField(blank=True, max_length=50, null=True)), - ('updated', models.DateTimeField(default=django.utils.timezone.now)), - ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin.Domain')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "type", + models.CharField( + choices=[ + ("spf", "SPF"), + ("dkim", "DKIM"), + ("dmarc", "DMARC"), + ("autoconfig", "Autoconfig"), + ("autodiscover", "Autodiscover"), + ], + max_length=15, + ), + ), + ("value", models.TextField(blank=True)), + ("is_valid", models.BooleanField(default=False)), + ("error", models.CharField(blank=True, max_length=50, null=True)), + ("updated", models.DateTimeField(default=django.utils.timezone.now)), + ( + "domain", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="admin.Domain" + ), + ), ], ), ] diff --git a/modoboa/dnstools/models.py b/modoboa/dnstools/models.py index 73fad5ecf..e74e495c7 100644 --- a/modoboa/dnstools/models.py +++ b/modoboa/dnstools/models.py @@ -19,8 +19,11 @@ class DNSRecordManager(models.Manager): DNS queries are not performed while `ttl` (in seconds) is still valid. """ now = timezone.now() - record = self.get_queryset().filter( - domain=domain, type=rtype, updated__gt=now).first() + record = ( + self.get_queryset() + .filter(domain=domain, type=rtype, updated__gt=now) + .first() + ) if record: return record @@ -38,8 +41,7 @@ class DNSRecord(models.Model): """A model to store DNS records for Domain.""" domain = models.ForeignKey("admin.Domain", on_delete=models.CASCADE) - type = models.CharField( - max_length=15, choices=constants.DNS_RECORD_TYPES) + type = models.CharField(max_length=15, choices=constants.DNS_RECORD_TYPES) value = models.TextField(blank=True) is_valid = models.BooleanField(default=False) error = models.CharField(max_length=50, null=True, blank=True) @@ -54,7 +56,8 @@ class DNSRecord(models.Model): """Retrieve corresponding DNS record.""" if self.type == "dkim": self.value = lib.get_dkim_record( - self.domain.name, self.domain.dkim_key_selector) + self.domain.name, self.domain.dkim_key_selector + ) else: func = getattr(lib, "get_{}_record".format(self.type)) self.value = func(self.domain.name) diff --git a/modoboa/dnstools/tests.py b/modoboa/dnstools/tests.py index 0bf8d4f62..a3e57430d 100644 --- a/modoboa/dnstools/tests.py +++ b/modoboa/dnstools/tests.py @@ -30,8 +30,10 @@ BAD_SPF_RECORDS = [ ("v=spf1 mx/1000", "Invalid mask found 1000"), ("v=spf1 ptr|test.com", "Invalid syntax for ptr mechanism"), ("v=spf1 forward=test.com ~all", "Unknown modifier forward"), - ("v=spf1 redirect=test.com redirect=test.com ~all", - "Duplicate modifier redirect found"), + ( + "v=spf1 redirect=test.com redirect=test.com ~all", + "Duplicate modifier redirect found", + ), ("v=spf1", "No mechanism found"), ] @@ -62,8 +64,7 @@ BAD_DMARC_RECORDS = [ ("v=DMARC1; test", "Invalid tag test"), ("v=DMARC1; test=toto", "Unknown tag test"), ("v=DMARC1; adkim=g", "Wrong value g for tag adkim"), - ("v=DMARC1; rua=mail:toto@test.com", - "Wrong value mail:toto@test.com for tag rua"), + ("v=DMARC1; rua=mail:toto@test.com", "Wrong value mail:toto@test.com for tag rua"), ("v=DMARC1; rf=afrf,toto", "Wrong value toto for tag rf"), ("v=DMARC1; ri=XXX", "Wrong value XXX for tag ri: not an integer"), ("v=DMARC1; pct=-1", "Wrong value -1 for tag pct: less than 0"), @@ -75,7 +76,7 @@ GOOD_DMARC_RECORDS = [ "v=DMARC1; p=reject;; pct=100", "v=DMARC1; p=quarantine; sp=none; adkim=s; aspf=s; " "rua=mailto:dmarc-aggrep@ngyn.org; ruf=mailto:toto@test.com!24m; " - "rf=afrf; pct=100; ri=86400" + "rf=afrf; pct=100; ri=86400", ] BAD_DKIM_RECORDS = [ @@ -88,22 +89,18 @@ BAD_DKIM_RECORDS = [ class LibTestCase(SimpleTestCase): """TestCase for library methods.""" - @mock.patch('modoboa.admin.lib.get_dns_records') + @mock.patch("modoboa.admin.lib.get_dns_records") def test_get_record_type_value(self, mock_get_dns_records): mock_get_dns_records.return_value = [ TXT("IN", "TXT", ["v=spf1 mx -all"]), TXT("IN", "TXT", ["v=DKIM1 p=XXXXX", "YYYYY"]), TXT("IN", "TXT", ["v=DMARC1 p=reject"]), ] - self.assertEqual( - lib.get_spf_record("example.com"), "v=spf1 mx -all" - ) + self.assertEqual(lib.get_spf_record("example.com"), "v=spf1 mx -all") self.assertEqual( lib.get_dkim_record("example.com", "mail"), "v=DKIM1 p=XXXXXYYYYY" ) - self.assertEqual( - lib.get_dmarc_record("example.com"), "v=DMARC1 p=reject" - ) + self.assertEqual(lib.get_dmarc_record("example.com"), "v=DMARC1 p=reject") def test_check_spf_syntax(self): for record in BAD_SPF_RECORDS: @@ -138,44 +135,44 @@ class ViewsTestCase(ModoTestCase): """Create some records.""" super(ViewsTestCase, cls).setUpTestData() cls.spf_rec = factories.DNSRecordFactory( - type="spf", value="v=SPF1 mx -all", is_valid=True, - domain__name="test.com" + type="spf", value="v=SPF1 mx -all", is_valid=True, domain__name="test.com" ) cls.dmarc_rec = factories.DNSRecordFactory( - type="dmarc", value="XXX", is_valid=False, + type="dmarc", + value="XXX", + is_valid=False, error="Not a DMARC record", - domain__name="test.com" + domain__name="test.com", ) cls.dkim_rec = factories.DNSRecordFactory( - type="dkim", value="12345", is_valid=False, + type="dkim", + value="12345", + is_valid=False, error="Public key mismatchs", - domain__name="test.com" + domain__name="test.com", ) cls.ac_rec = factories.DNSRecordFactory( - type="autoconfig", value="1.2.3.4", is_valid=True, - domain__name="test.com" + type="autoconfig", value="1.2.3.4", is_valid=True, domain__name="test.com" ) def test_record_detail_view(self): url = reverse("dnstools:dns_record_detail", args=[self.spf_rec.pk]) response = self.client.get(url) - self.assertContains( - response, "A DNS record has been found and is valid") + self.assertContains(response, "A DNS record has been found and is valid") url = reverse("dnstools:dns_record_detail", args=[self.dmarc_rec.pk]) response = self.client.get(url) self.assertContains(response, "Not a DMARC record") def test_autoconfig_record_status_view(self): url = reverse( - "dnstools:autoconfig_records_status", args=[self.ac_rec.domain.pk]) + "dnstools:autoconfig_records_status", args=[self.ac_rec.domain.pk] + ) response = self.client.get(url) self.assertContains(response, "autoconfig record (Mozilla) found") - self.assertContains( - response, "autodiscover record (Microsoft) not found") + self.assertContains(response, "autodiscover record (Microsoft) not found") def test_domain_dns_configuration(self): - url = reverse( - "dnstools:domain_dns_configuration", args=[self.ac_rec.domain.pk]) + url = reverse("dnstools:domain_dns_configuration", args=[self.ac_rec.domain.pk]) response = self.client.get(url) self.assertContains(response, "[IP address of your Modoboa server]") @@ -184,37 +181,32 @@ class DomainTestCase(ModoTestCase): def test_dns_global_status(self): admin_factories.MXRecordFactory.create( - domain__name="test.com", name="mail.test.com", - updated=timezone.now()) + domain__name="test.com", name="mail.test.com", updated=timezone.now() + ) domain = admin_models.Domain.objects.get(name="test.com") self.assertEqual(domain.dns_global_status, "critical") factories.DNSRecordFactory.create( - type="spf", value="v=SPF1 mx -all", is_valid=True, - domain__name="test.com" + type="spf", value="v=SPF1 mx -all", is_valid=True, domain__name="test.com" ) self.assertEqual(domain.dns_global_status, "critical") factories.DNSRecordFactory( - type="dkim", value="12345", is_valid=True, - domain__name="test.com" + type="dkim", value="12345", is_valid=True, domain__name="test.com" ) self.assertEqual(domain.dns_global_status, "critical") factories.DNSRecordFactory( - type="dmarc", value="XXX", is_valid=True, - domain__name="test.com" + type="dmarc", value="XXX", is_valid=True, domain__name="test.com" ) self.assertEqual(domain.dns_global_status, "critical") factories.DNSRecordFactory( - type="autoconfig", value="1.2.3.4", is_valid=True, - domain__name="test.com" + type="autoconfig", value="1.2.3.4", is_valid=True, domain__name="test.com" ) self.assertEqual(domain.dns_global_status, "critical") factories.DNSRecordFactory( - type="autodiscover", value="1.2.3.4", is_valid=True, - domain__name="test.com" + type="autodiscover", value="1.2.3.4", is_valid=True, domain__name="test.com" ) self.assertEqual(domain.dns_global_status, "ok") diff --git a/modoboa/dnstools/urls.py b/modoboa/dnstools/urls.py index 2c43d053f..aa6369bfa 100644 --- a/modoboa/dnstools/urls.py +++ b/modoboa/dnstools/urls.py @@ -7,12 +7,19 @@ from . import views app_name = "dnstools" urlpatterns = [ - path('dnsrecords//', views.DNSRecordDetailView.as_view(), - name="dns_record_detail"), - path('dnsrecords/autoconfig//', - views.AutoConfigRecordsStatusView.as_view(), - name="autoconfig_records_status"), - path('configuration//', - views.DomainDNSConfigurationView.as_view(), - name="domain_dns_configuration"), + path( + "dnsrecords//", + views.DNSRecordDetailView.as_view(), + name="dns_record_detail", + ), + path( + "dnsrecords/autoconfig//", + views.AutoConfigRecordsStatusView.as_view(), + name="autoconfig_records_status", + ), + path( + "configuration//", + views.DomainDNSConfigurationView.as_view(), + name="domain_dns_configuration", + ), ] diff --git a/modoboa/dnstools/views.py b/modoboa/dnstools/views.py index dd2297e01..c8871c783 100644 --- a/modoboa/dnstools/views.py +++ b/modoboa/dnstools/views.py @@ -16,14 +16,12 @@ class DomainAccessRequiredMixin(auth_mixins.AccessMixin): def dispatch(self, request, *args, **kwargs): if not request.user.can_access(self.get_object()): return self.handle_no_permission() - return super(DomainAccessRequiredMixin, self).dispatch( - request, *args, **kwargs) + return super(DomainAccessRequiredMixin, self).dispatch(request, *args, **kwargs) class DNSRecordDetailView( - auth_mixins.LoginRequiredMixin, - DomainAccessRequiredMixin, - generic.DetailView): + auth_mixins.LoginRequiredMixin, DomainAccessRequiredMixin, generic.DetailView +): """View to display MX records.""" model = models.DNSRecord @@ -32,17 +30,21 @@ class DNSRecordDetailView( def get_context_data(self, **kwargs): """Add extra variables.""" context = super(DNSRecordDetailView, self).get_context_data(**kwargs) - context.update({ - "title": _("{} record of {}").format( - self.object.type.upper(), self.object.domain) - }) + context.update( + { + "title": _("{} record of {}").format( + self.object.type.upper(), self.object.domain + ) + } + ) return context class AutoConfigRecordsStatusView( - auth_mixins.LoginRequiredMixin, - auth_mixins.PermissionRequiredMixin, - generic.DetailView): + auth_mixins.LoginRequiredMixin, + auth_mixins.PermissionRequiredMixin, + generic.DetailView, +): """Autoconfig records status view.""" model = admin_models.Domain @@ -51,26 +53,22 @@ class AutoConfigRecordsStatusView( def get_queryset(self): """Add some prefetching.""" - return ( - admin_models.Domain.objects.get_for_admin(self.request.user) - .prefetch_related("dnsrecord_set") - ) + return admin_models.Domain.objects.get_for_admin( + self.request.user + ).prefetch_related("dnsrecord_set") def get_context_data(self, **kwargs): """Add extra data to context.""" - context = super(AutoConfigRecordsStatusView, self).get_context_data( - **kwargs) - context.update({ - "title": _("Auto configuration records for {}").format( - self.object.name) - }) + context = super(AutoConfigRecordsStatusView, self).get_context_data(**kwargs) + context.update( + {"title": _("Auto configuration records for {}").format(self.object.name)} + ) return context class DomainDNSConfigurationView( - auth_mixins.LoginRequiredMixin, - DomainAccessRequiredMixin, - generic.DetailView): + auth_mixins.LoginRequiredMixin, DomainAccessRequiredMixin, generic.DetailView +): """Page to display DNS configuration for a domain.""" model = admin_models.Domain @@ -78,9 +76,6 @@ class DomainDNSConfigurationView( def get_context_data(self, **kwargs): """Add extra variables.""" - context = super(DomainDNSConfigurationView, self).get_context_data( - **kwargs) - context.update({ - "title": _("DNS configuration for {}").format(self.object) - }) + context = super(DomainDNSConfigurationView, self).get_context_data(**kwargs) + context.update({"title": _("DNS configuration for {}").format(self.object)}) return context diff --git a/modoboa/imap_migration/api/v2/serializers.py b/modoboa/imap_migration/api/v2/serializers.py index f86616fdb..e94f67982 100644 --- a/modoboa/imap_migration/api/v2/serializers.py +++ b/modoboa/imap_migration/api/v2/serializers.py @@ -21,12 +21,12 @@ class IMAPMigrationSettingsSerializer(serializers.Serializer): # OfflineIMAP Filter settings create_folders = serializers.BooleanField(default=True) - folder_filter_exclude = serializers.CharField(required=False, - allow_blank=True, - default="") - folder_filter_include = serializers.CharField(required=False, - allow_blank=True, - default="") + folder_filter_exclude = serializers.CharField( + required=False, allow_blank=True, default="" + ) + folder_filter_include = serializers.CharField( + required=False, allow_blank=True, default="" + ) class EmailProviderDomainSerializer(serializers.ModelSerializer): @@ -35,11 +35,7 @@ class EmailProviderDomainSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) class Meta: - extra_kwargs = { - "name": { - "validators": [] - } - } + extra_kwargs = {"name": {"validators": []}} fields = ("id", "name", "new_domain") model = models.EmailProviderDomain @@ -61,8 +57,9 @@ class EmailProviderSerializer(serializers.ModelSerializer): if domains: to_create = [] for domain in domains: - to_create.append(models.EmailProviderDomain( - provider=provider, **domain)) + to_create.append( + models.EmailProviderDomain(provider=provider, **domain) + ) models.EmailProviderDomain.objects.bulk_create(to_create) return provider @@ -87,11 +84,13 @@ class EmailProviderSerializer(serializers.ModelSerializer): break to_create = [] for new_domain in domains: - to_create.append(models.EmailProviderDomain( - name=new_domain["name"], - new_domain=new_domain.get("new_domain"), - provider=instance - )) + to_create.append( + models.EmailProviderDomain( + name=new_domain["name"], + new_domain=new_domain.get("new_domain"), + provider=instance, + ) + ) models.EmailProviderDomain.objects.bulk_create(to_create) return instance @@ -108,14 +107,12 @@ class CheckAssociatedDomainSerializer(serializers.Serializer): """A serializer for checking associated domains.""" name = serializers.CharField() - new_domain = serializers.PrimaryKeyRelatedField( - read_only=True, required=False) + new_domain = serializers.PrimaryKeyRelatedField(read_only=True, required=False) def validate_new_domain(self, value): - domain_ids = ( - Domain.objects.get_for_admin(self.context["request"].user) - .values_list("id", flat=True) - ) + domain_ids = Domain.objects.get_for_admin( + self.context["request"].user + ).values_list("id", flat=True) if value not in domain_ids: raise serializers.ValidationError(_("Access denied")) return value @@ -138,4 +135,3 @@ class MigrationSerializer(serializers.ModelSerializer): depth = 1 fields = ("id", "provider", "mailbox", "username") model = models.Migration - diff --git a/modoboa/imap_migration/api/v2/viewsets.py b/modoboa/imap_migration/api/v2/viewsets.py index 0f52bcd05..ef069ac20 100644 --- a/modoboa/imap_migration/api/v2/viewsets.py +++ b/modoboa/imap_migration/api/v2/viewsets.py @@ -16,13 +16,13 @@ from . import serializers class EmailProviderViewSet(GetThrottleViewsetMixin, viewsets.ModelViewSet): """ViewSet class for EmailProvider.""" - filter_backends = (filters.SearchFilter, ) + filter_backends = (filters.SearchFilter,) permission_classes = ( permissions.IsAuthenticated, - permissions.DjangoModelPermissions + permissions.DjangoModelPermissions, ) queryset = models.EmailProvider.objects.all().prefetch_related("domains") - search_fields = ("name", ) + search_fields = ("name",) serializer_class = serializers.EmailProviderSerializer @action(methods=["post"], detail=False) @@ -39,7 +39,7 @@ class EmailProviderViewSet(GetThrottleViewsetMixin, viewsets.ModelViewSet): else: imaplib.IMAP4(address, port) except (socket.error, imaplib.IMAP4.error, ssl.SSLError): - return response.Response({'status': 'failed'}, 400) + return response.Response({"status": "failed"}, 400) return response.Response(status=200) @action(methods=["post"], detail=False) @@ -52,21 +52,27 @@ class EmailProviderViewSet(GetThrottleViewsetMixin, viewsets.ModelViewSet): return response.Response(status=200) -class MigrationViewSet(GetThrottleViewsetMixin, - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - mixins.DestroyModelMixin, - viewsets.GenericViewSet): +class MigrationViewSet( + GetThrottleViewsetMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + viewsets.GenericViewSet, +): """ViewSet class for Migration.""" - filter_backends = (filters.SearchFilter, ) + filter_backends = (filters.SearchFilter,) permission_classes = ( permissions.IsAuthenticated, - permissions.DjangoModelPermissions + permissions.DjangoModelPermissions, ) queryset = models.Migration.objects.all().select_related( - "provider", "mailbox__domain") + "provider", "mailbox__domain" + ) search_fields = ( - "username", "provider__name", "mailbox__address", - "mailbox__domain__name") + "username", + "provider__name", + "mailbox__address", + "mailbox__domain__name", + ) serializer_class = serializers.MigrationSerializer diff --git a/modoboa/imap_migration/app_settings.py b/modoboa/imap_migration/app_settings.py index d73d194da..ba3e90bf6 100644 --- a/modoboa/imap_migration/app_settings.py +++ b/modoboa/imap_migration/app_settings.py @@ -9,51 +9,86 @@ from modoboa.lib import form_utils from modoboa.parameters import forms as param_forms -IMAP_MIGRATION_PARAMETERS_STRUCT = collections.OrderedDict([ - ("general", { - "label": _("General"), - "params": collections.OrderedDict([ - ("enabled_imapmigration", { - "label": _("Enable IMAP Migration"), - "help_text": _( - "Enable IMAP Migration."), - }) - ]) - }), - ("offlineimap", { - "label": _("OfflineIMAP"), - "params": collections.OrderedDict([ - ("max_sync_accounts", { - "label": _("Concurrent sync jobs"), - "help_text": _( - "The maximum number of concurrent synchronization jobs") - }) - ]) - }), - ("offlineimapfilter", { - "label": _("OfflineIMAP Filter"), - "params": collections.OrderedDict([ - ("create_folders", { - "label": _("Create Folders"), - "help_text": _( - "Allow Creation of missing folders during sync"), - }), - ("folder_filter_exclude", { - "label": _("Folder Filter Exclusions"), - "help_text": _( - "Use a regular expression to explicitly include folders in sync. " - "Example: ^Trash$|Del") - }), - ("folder_filter_include", { - "label": _("Folder Filter Inclusions"), - "help_text": _( - "A comma seperated list of folders to explicitly include in sync " - "even if filtered by the Folder Filter Exclusions. Example: " - "debian.user, debian.personal ") - }) - ]) - }) -]) +IMAP_MIGRATION_PARAMETERS_STRUCT = collections.OrderedDict( + [ + ( + "general", + { + "label": _("General"), + "params": collections.OrderedDict( + [ + ( + "enabled_imapmigration", + { + "label": _("Enable IMAP Migration"), + "help_text": _("Enable IMAP Migration."), + }, + ) + ] + ), + }, + ), + ( + "offlineimap", + { + "label": _("OfflineIMAP"), + "params": collections.OrderedDict( + [ + ( + "max_sync_accounts", + { + "label": _("Concurrent sync jobs"), + "help_text": _( + "The maximum number of concurrent synchronization jobs" + ), + }, + ) + ] + ), + }, + ), + ( + "offlineimapfilter", + { + "label": _("OfflineIMAP Filter"), + "params": collections.OrderedDict( + [ + ( + "create_folders", + { + "label": _("Create Folders"), + "help_text": _( + "Allow Creation of missing folders during sync" + ), + }, + ), + ( + "folder_filter_exclude", + { + "label": _("Folder Filter Exclusions"), + "help_text": _( + "Use a regular expression to explicitly include folders in sync. " + "Example: ^Trash$|Del" + ), + }, + ), + ( + "folder_filter_include", + { + "label": _("Folder Filter Inclusions"), + "help_text": _( + "A comma seperated list of folders to explicitly include in sync " + "even if filtered by the Folder Filter Exclusions. Example: " + "debian.user, debian.personal " + ), + }, + ), + ] + ), + }, + ), + ] +) class ParametersForm(param_forms.AdminParametersForm): @@ -66,7 +101,7 @@ class ParametersForm(param_forms.AdminParametersForm): enabled_imapmigration = form_utils.YesNoField( label=_("Enable IMAP Migration"), initial=True, - help_text=_("Enable IMAP Migration.") + help_text=_("Enable IMAP Migration."), ) sep2 = form_utils.SeparatorField(label=_("OfflineIMAP settings")) @@ -74,7 +109,7 @@ class ParametersForm(param_forms.AdminParametersForm): max_sync_accounts = forms.IntegerField( label=_("Concurrent sync jobs"), initial=1, - help_text=_("The maximum number of concurrent synchronization jobs") + help_text=_("The maximum number of concurrent synchronization jobs"), ) sep3 = form_utils.SeparatorField(label=_("OfflineIMAP Filter settings")) @@ -82,7 +117,7 @@ class ParametersForm(param_forms.AdminParametersForm): create_folders = form_utils.YesNoField( label=_("Create Folders"), initial=True, - help_text=_("Allow Creation of missing folders during sync") + help_text=_("Allow Creation of missing folders during sync"), ) folder_filter_exclude = forms.CharField( @@ -92,7 +127,7 @@ class ParametersForm(param_forms.AdminParametersForm): help_text=_( "Use a regular expression to explicitly include folders in sync. " "Example: ^Trash$|Del" - ) + ), ) folder_filter_include = forms.CharField( @@ -103,5 +138,5 @@ class ParametersForm(param_forms.AdminParametersForm): "A comma seperated list of folders to explicitly include in sync " "even if filtered by the Folder Filter Exclusions. Example: " "debian.user, debian.personal " - ) + ), ) diff --git a/modoboa/imap_migration/apps.py b/modoboa/imap_migration/apps.py index e3cf9fb5e..73dc71835 100644 --- a/modoboa/imap_migration/apps.py +++ b/modoboa/imap_migration/apps.py @@ -15,12 +15,16 @@ def load_imapmigration_settings(): from .api.v2 import serializers param_tools.registry.add( - "global", app_settings.ParametersForm, gettext_lazy("IMAP Migration")) + "global", app_settings.ParametersForm, gettext_lazy("IMAP Migration") + ) param_tools.registry.add2( - "global", "imap_migration", gettext_lazy("IMAP Migration"), + "global", + "imap_migration", + gettext_lazy("IMAP Migration"), app_settings.IMAP_MIGRATION_PARAMETERS_STRUCT, serializers.IMAPMigrationSettingsSerializer, - True) + True, + ) class IMAPMigrationConfig(AppConfig): diff --git a/modoboa/imap_migration/auth_backends.py b/modoboa/imap_migration/auth_backends.py index ac6afe3b7..1765e8fda 100644 --- a/modoboa/imap_migration/auth_backends.py +++ b/modoboa/imap_migration/auth_backends.py @@ -23,14 +23,18 @@ class IMAPBackend: if not param_tools.get_global_parameter("enabled_imapmigration"): return None condition = not param_tools.get_global_parameter( - "auto_create_domain_and_mailbox", "admin") + "auto_create_domain_and_mailbox", "admin" + ) if condition: # Automatic domain/mailbox is disabled. Deny auth to # prevent further issues... return None self.address, domain = email_utils.split_mailbox(username) - provider_domain = models.EmailProviderDomain.objects.filter( - name=domain).select_related("provider").first() + provider_domain = ( + models.EmailProviderDomain.objects.filter(name=domain) + .select_related("provider") + .first() + ) if not provider_domain: # Domain not allowed for migration: failure return None @@ -42,12 +46,10 @@ class IMAPBackend: else: conn = imaplib.IMAP4(address, port) except (socket.error, imaplib.IMAP4.error, ssl.SSLError) as error: - raise ModoboaException( - _("Connection to IMAP server failed: %s") % error) + raise ModoboaException(_("Connection to IMAP server failed: %s") % error) try: - typ, data = conn.login( - smart_bytes(username), smart_str(password)) + typ, data = conn.login(smart_bytes(username), smart_str(password)) except imaplib.IMAP4.error: typ = "NO" conn.logout() @@ -66,12 +68,12 @@ class IMAPBackend: orig_username = username # Check if old addresses must be converted if self.provider_domain.new_domain: - username = u"{}@{}".format( - self.address, self.provider_domain.new_domain.name) + username = "{}@{}".format( + self.address, self.provider_domain.new_domain.name + ) user, created = core_models.User.objects.get_or_create( - username__iexact=username, defaults={ - "username": username.lower(), "email": username.lower() - } + username__iexact=username, + defaults={"username": username.lower(), "email": username.lower()}, ) if created: user.set_password(password) @@ -81,7 +83,7 @@ class IMAPBackend: provider=self.provider_domain.provider, mailbox=user.mailbox, username=orig_username, - password=password + password=password, ) else: # What happens if an account already exists? diff --git a/modoboa/imap_migration/checks.py b/modoboa/imap_migration/checks.py index 9ace9a5ea..9d34064d4 100644 --- a/modoboa/imap_migration/checks.py +++ b/modoboa/imap_migration/checks.py @@ -8,11 +8,14 @@ from modoboa.core import models as core_models W001 = Warning( - _("Automatic domain/mailbox creation is disabled which means IMAP " - "authentication won't work."), - hint=_("Go to the online parameters panel (admin tab) and activate " - "this feature."), - id="modoboa-imap-migration.W001" + _( + "Automatic domain/mailbox creation is disabled which means IMAP " + "authentication won't work." + ), + hint=_( + "Go to the online parameters panel (admin tab) and activate " "this feature." + ), + id="modoboa-imap-migration.W001", ) @@ -23,7 +26,6 @@ def check_auto_creation_is_enabled(app_configs, **kwargs): if not lc: return [] errors = [] - if not lc.parameters.get_value( - "auto_create_domain_and_mailbox", app="admin"): + if not lc.parameters.get_value("auto_create_domain_and_mailbox", app="admin"): errors.append(W001) return errors diff --git a/modoboa/imap_migration/management/commands/generate_offlineimap_config.py b/modoboa/imap_migration/management/commands/generate_offlineimap_config.py index 6e3cdc90a..7d18d77ef 100644 --- a/modoboa/imap_migration/management/commands/generate_offlineimap_config.py +++ b/modoboa/imap_migration/management/commands/generate_offlineimap_config.py @@ -20,25 +20,26 @@ class Command(BaseCommand): def add_arguments(self, parser): """Add extra arguments to command line.""" parser.add_argument( - "--output", default="/tmp/offlineimap.conf", - help="Path of the generated file") + "--output", + default="/tmp/offlineimap.conf", + help="Path of the generated file", + ) def handle(self, *args, **options): """Entry point.""" load_admin_settings() - conf = dict( - param_tools.get_global_parameters("imap_migration")) + conf = dict(param_tools.get_global_parameters("imap_migration")) context = { "imap_create_folders": conf["create_folders"], "imap_folder_filter_exclude": conf["folder_filter_exclude"], "imap_folder_filter_include": ", ".join( - "'{0}'".format(w) - for w in conf["folder_filter_include"].split(",")), + "'{0}'".format(w) for w in conf["folder_filter_include"].split(",") + ), "migrations": Migration.objects.select_related( - "provider", "mailbox__domain"), + "provider", "mailbox__domain" + ), } with open(options["output"], "w") as fpo: - content = render_to_string( - "imap_migration/offlineimap.conf", context) + content = render_to_string("imap_migration/offlineimap.conf", context) fpo.write(content) os.chmod(options["output"], stat.S_IRUSR | stat.S_IWUSR) diff --git a/modoboa/imap_migration/migrations/0001_initial.py b/modoboa/imap_migration/migrations/0001_initial.py index 416ae4c91..f0d7a70ca 100644 --- a/modoboa/imap_migration/migrations/0001_initial.py +++ b/modoboa/imap_migration/migrations/0001_initial.py @@ -7,19 +7,29 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('admin', '__first__'), + ("admin", "__first__"), ] operations = [ migrations.CreateModel( - name='Migration', + name="Migration", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('_password', models.CharField(max_length=100)), - ('mailbox', models.ForeignKey(to='admin.Mailbox', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("_password", models.CharField(max_length=100)), + ( + "mailbox", + models.ForeignKey(to="admin.Mailbox", on_delete=models.CASCADE), + ), ], - options={ - }, + options={}, bases=(models.Model,), ), ] diff --git a/modoboa/imap_migration/migrations/0002_password.py b/modoboa/imap_migration/migrations/0002_password.py index 83c455926..a6d4786d1 100644 --- a/modoboa/imap_migration/migrations/0002_password.py +++ b/modoboa/imap_migration/migrations/0002_password.py @@ -7,13 +7,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('imap_migration', '0001_initial'), + ("imap_migration", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='migration', - name='_password', + model_name="migration", + name="_password", field=models.CharField(max_length=255), ), ] diff --git a/modoboa/imap_migration/migrations/0003_auto_20190207_0919.py b/modoboa/imap_migration/migrations/0003_auto_20190207_0919.py index eb0867b77..9e274868a 100644 --- a/modoboa/imap_migration/migrations/0003_auto_20190207_0919.py +++ b/modoboa/imap_migration/migrations/0003_auto_20190207_0919.py @@ -21,12 +21,13 @@ def create_default_provider(apps, schema_editor): name="Default", address=params["server_address"], port=params["server_port"], - secured=params["secured"] + secured=params["secured"], ) for migration in Migration.objects.select_related("mailbox__domain"): migration.provider = provider migration.username = ( - migration.mailbox.address + "@" + migration.mailbox.domain.name) + migration.mailbox.address + "@" + migration.mailbox.domain.name + ) migration.save(update_fields=["provider", "username"]) @@ -37,45 +38,79 @@ def backwards(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('admin', '0014_auto_20181017_1628'), - ('imap_migration', '0002_password'), + ("admin", "0014_auto_20181017_1628"), + ("imap_migration", "0002_password"), ] operations = [ migrations.CreateModel( - name='EmailProvider', + name="EmailProvider", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50)), - ('address', models.CharField(max_length=200)), - ('port', models.PositiveIntegerField(default=143)), - ('secured', models.BooleanField(default=False)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50)), + ("address", models.CharField(max_length=200)), + ("port", models.PositiveIntegerField(default=143)), + ("secured", models.BooleanField(default=False)), ], options={ - 'ordering': ['name'], + "ordering": ["name"], }, ), migrations.CreateModel( - name='EmailProviderDomain', + name="EmailProviderDomain", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, unique=True)), - ('new_domain', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='admin.Domain')), - ('provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='imap_migration.EmailProvider')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, unique=True)), + ( + "new_domain", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="admin.Domain", + ), + ), + ( + "provider", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="imap_migration.EmailProvider", + ), + ), ], options={ - 'ordering': ['provider', 'name'], + "ordering": ["provider", "name"], }, ), migrations.AddField( - model_name='migration', - name='username', + model_name="migration", + name="username", field=models.CharField(max_length=254, null=True), ), migrations.AddField( - model_name='migration', - name='provider', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='imap_migration.EmailProvider'), + model_name="migration", + name="provider", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="imap_migration.EmailProvider", + ), ), - migrations.RunPython(create_default_provider, backwards) + migrations.RunPython(create_default_provider, backwards), ] diff --git a/modoboa/imap_migration/migrations/0004_auto_20190207_0939.py b/modoboa/imap_migration/migrations/0004_auto_20190207_0939.py index 4b1f5f59b..f2a03ec49 100644 --- a/modoboa/imap_migration/migrations/0004_auto_20190207_0939.py +++ b/modoboa/imap_migration/migrations/0004_auto_20190207_0939.py @@ -9,18 +9,21 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('imap_migration', '0003_auto_20190207_0919'), + ("imap_migration", "0003_auto_20190207_0919"), ] operations = [ migrations.AlterField( - model_name='migration', - name='provider', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='imap_migration.EmailProvider'), + model_name="migration", + name="provider", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="imap_migration.EmailProvider", + ), ), migrations.AlterField( - model_name='migration', - name='username', + model_name="migration", + name="username", field=models.CharField(max_length=254), ), ] diff --git a/modoboa/imap_migration/migrations/0005_auto_20210401_0938.py b/modoboa/imap_migration/migrations/0005_auto_20210401_0938.py index f49ac7383..328128c3d 100644 --- a/modoboa/imap_migration/migrations/0005_auto_20210401_0938.py +++ b/modoboa/imap_migration/migrations/0005_auto_20210401_0938.py @@ -7,18 +7,22 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('imap_migration', '0004_auto_20190207_0939'), + ("imap_migration", "0004_auto_20190207_0939"), ] operations = [ migrations.AlterField( - model_name='emailproviderdomain', - name='provider', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='domains', to='imap_migration.EmailProvider'), + model_name="emailproviderdomain", + name="provider", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="domains", + to="imap_migration.EmailProvider", + ), ), migrations.AlterField( - model_name='migration', - name='username', + model_name="migration", + name="username", field=models.CharField(max_length=254, unique=True), ), ] diff --git a/modoboa/imap_migration/models.py b/modoboa/imap_migration/models.py index d3660acfa..eda2fe41c 100644 --- a/modoboa/imap_migration/models.py +++ b/modoboa/imap_migration/models.py @@ -24,10 +24,12 @@ class EmailProviderDomain(models.Model): """Email provider domain class.""" provider = models.ForeignKey( - EmailProvider, on_delete=models.CASCADE, related_name="domains") + EmailProvider, on_delete=models.CASCADE, related_name="domains" + ) name = models.CharField(max_length=100, unique=True) new_domain = models.ForeignKey( - "admin.Domain", on_delete=models.SET_NULL, null=True, blank=True) + "admin.Domain", on_delete=models.SET_NULL, null=True, blank=True + ) class Meta: ordering = ["provider", "name"] diff --git a/modoboa/imap_migration/tests.py b/modoboa/imap_migration/tests.py index d87480174..8093f9d5f 100644 --- a/modoboa/imap_migration/tests.py +++ b/modoboa/imap_migration/tests.py @@ -28,10 +28,10 @@ class DataMixin(object): def setUpTestData(cls): super(DataMixin, cls).setUpTestData() admin_factories.populate_database() - cls.mb = admin_models.Mailbox.objects.get( - user__username="user@test.com") + cls.mb = admin_models.Mailbox.objects.get(user__username="user@test.com") cls.migration = factories.MigrationFactory( - password="Toto1234", mailbox=cls.mb, username="user@test.com") + password="Toto1234", mailbox=cls.mb, username="user@test.com" + ) # class ViewsTestCase(DataMixin, ModoTestCase): @@ -69,8 +69,7 @@ class AuthenticationTestCase(DataMixin, ModoTestCase): response = self.client.post(url, data) self.assertEqual(response.status_code, 401) - self.set_global_parameter( - "auto_create_domain_and_mailbox", False, app="admin") + self.set_global_parameter("auto_create_domain_and_mailbox", False, app="admin") data = {"username": "new_user2@test.com", "password": "Toto1234"} response = self.client.post(url, data) self.assertEqual(response.status_code, 401) @@ -79,8 +78,7 @@ class AuthenticationTestCase(DataMixin, ModoTestCase): def test_authenticate_and_rename(self, mock_imap): """Check address renaming.""" domain = admin_models.Domain.objects.get(name="test.com") - factories.EmailProviderDomainFactory( - name="gmail.com", new_domain=domain) + factories.EmailProviderDomainFactory(name="gmail.com", new_domain=domain) mock_imap.return_value.login.return_value = ["OK", b""] url = reverse("core:login") data = {"username": "new_user@gmail.com", "password": "Toto1234"} @@ -93,12 +91,10 @@ class AuthenticationTestCase(DataMixin, ModoTestCase): def test_authenticate_conflicts(self, mock_imap): """Check potential conflicts.""" core_factories.UserFactory( - username="admin2@test.com", groups=("DomainAdmins", ), - password="{PLAIN}toto" + username="admin2@test.com", groups=("DomainAdmins",), password="{PLAIN}toto" ) domain = admin_models.Domain.objects.get(name="test.com") - factories.EmailProviderDomainFactory( - name="gmail.com", new_domain=domain) + factories.EmailProviderDomainFactory(name="gmail.com", new_domain=domain) mock_imap.return_value.login.return_value = ["OK", b""] url = reverse("core:login") data = {"username": "admin@gmail.com", "password": "Toto1234"} @@ -140,51 +136,42 @@ class ViewSetTestCase(DataMixin, ModoAPITestCase): "address": "imap.google.com", "port": 143, "secured": True, - "domains": [ - {"id": 1000, - "name": "gmail.com"} - ] + "domains": [{"id": 1000, "name": "gmail.com"}], } response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 201) self.assertTrue( - models.EmailProviderDomain.objects.filter(name="gmail.com") - .exists() + models.EmailProviderDomain.objects.filter(name="gmail.com").exists() ) def test_update_provider(self): - provider_domain = factories.EmailProviderDomainFactory( - name="outlook.com") - url = reverse( - "v2:emailprovider-detail", args=[provider_domain.provider.pk]) + provider_domain = factories.EmailProviderDomainFactory(name="outlook.com") + url = reverse("v2:emailprovider-detail", args=[provider_domain.provider.pk]) data = { "id": provider_domain.provider.id, "name": provider_domain.provider.name, "address": provider_domain.provider.address, "port": provider_domain.provider.port, "domains": [ - {"id": provider_domain.id, - "name": "hotmail.com", - "new_domain": None}, - {"name": "gmail.com", - "new_domain": admin_models.Domain.objects.get( - name="test.com").pk} - ] + {"id": provider_domain.id, "name": "hotmail.com", "new_domain": None}, + { + "name": "gmail.com", + "new_domain": admin_models.Domain.objects.get(name="test.com").pk, + }, + ], } response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, 200) provider_domain.refresh_from_db() self.assertEqual(provider_domain.name, data["domains"][0]["name"]) self.assertTrue( - models.EmailProviderDomain.objects.filter(name="gmail.com") - .exists() + models.EmailProviderDomain.objects.filter(name="gmail.com").exists() ) data["domains"].pop() response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, 200) self.assertFalse( - models.EmailProviderDomain.objects.filter(name="gmail.com") - .exists() + models.EmailProviderDomain.objects.filter(name="gmail.com").exists() ) @@ -192,13 +179,6 @@ class ChecksTestCase(ModoTestCase): """Deploy checks test case.""" def test_auto_creation_check(self): - self.assertEqual( - checks.check_auto_creation_is_enabled(None), - [] - ) - self.set_global_parameter( - "auto_create_domain_and_mailbox", False, app="admin") - self.assertEqual( - checks.check_auto_creation_is_enabled(None), - [checks.W001] - ) + self.assertEqual(checks.check_auto_creation_is_enabled(None), []) + self.set_global_parameter("auto_create_domain_and_mailbox", False, app="admin") + self.assertEqual(checks.check_auto_creation_is_enabled(None), [checks.W001]) diff --git a/modoboa/imap_migration/views.py b/modoboa/imap_migration/views.py index d50cf61d3..334518a21 100644 --- a/modoboa/imap_migration/views.py +++ b/modoboa/imap_migration/views.py @@ -5,9 +5,11 @@ from django.contrib.auth import mixins as auth_mixins from django.views import generic -class IndexView(auth_mixins.LoginRequiredMixin, - auth_mixins.PermissionRequiredMixin, - generic.TemplateView): +class IndexView( + auth_mixins.LoginRequiredMixin, + auth_mixins.PermissionRequiredMixin, + generic.TemplateView, +): """Index view.""" permission_required = "modoboa.imap_migration.add_migration" diff --git a/modoboa/ldapsync/apps.py b/modoboa/ldapsync/apps.py index 4ccad8839..a39f85d45 100644 --- a/modoboa/ldapsync/apps.py +++ b/modoboa/ldapsync/apps.py @@ -2,7 +2,7 @@ from django.apps import AppConfig class LdapsyncConfig(AppConfig): - name = 'modoboa.ldapsync' + name = "modoboa.ldapsync" def ready(self): from . import handlers # noqa diff --git a/modoboa/ldapsync/lib.py b/modoboa/ldapsync/lib.py index ae2c87120..441c3dbc8 100644 --- a/modoboa/ldapsync/lib.py +++ b/modoboa/ldapsync/lib.py @@ -15,8 +15,7 @@ from modoboa.lib.exceptions import InternalError def create_connection(srv_address, srv_port, config, username, password): """Create a new connection with given server.""" uri = "{}:{}".format(srv_address, srv_port) - uri = "{}://{}".format( - "ldaps" if config["ldap_secured"] == "ssl" else "ldap", uri) + uri = "{}://{}".format("ldaps" if config["ldap_secured"] == "ssl" else "ldap", uri) conn = ldap.initialize(uri) conn.protocol_version = 3 conn.set_option(ldap.OPT_X_TLS_DEMAND, True) @@ -24,7 +23,7 @@ def create_connection(srv_address, srv_port, config, username, password): conn.set_option(ldap.OPT_REFERRALS, 0) conn.simple_bind_s( force_str(username if username else config["ldap_sync_bind_dn"]), - force_str(password if password else config["ldap_sync_bind_password"]) + force_str(password if password else config["ldap_sync_bind_password"]), ) return conn @@ -33,8 +32,11 @@ def get_connection(config, username=None, password=None): """Get a new connection to the LDAP directory.""" try: conn = create_connection( - config["ldap_server_address"], config["ldap_server_port"], - config, username, password + config["ldap_server_address"], + config["ldap_server_port"], + config, + username, + password, ) except ldap.LDAPError: if not config["ldap_enable_secondary_server"]: @@ -42,7 +44,9 @@ def get_connection(config, username=None, password=None): conn = create_connection( config["ldap_secondary_server_address"], config["ldap_secondary_server_port"], - config, username, password + config, + username, + password, ) return conn @@ -50,27 +54,22 @@ def get_connection(config, username=None, password=None): def get_user_password(user, disable=False): """Return ready-to-use password from user instance.""" scheme, password = user.password.split("}") - return ( - force_bytes(scheme) + - b"}" + - b"#" if disable else b"" + - force_bytes(password) - ) + return force_bytes(scheme) + b"}" + b"#" if disable else b"" + force_bytes(password) def create_ldap_account(user, dn, conn): """Create new account.""" attrs = { "objectClass": [ - force_bytes("inetOrgPerson"), force_bytes("organizationalPerson") + force_bytes("inetOrgPerson"), + force_bytes("organizationalPerson"), ], "uid": [force_bytes(user.username)], "sn": [force_bytes(user.last_name)], "givenName": [force_bytes(user.first_name)], "cn": [force_bytes(user.username)], "displayName": [force_bytes(user.fullname)], - "mail": [ - force_bytes(user.email), force_bytes(user.secondary_email)], + "mail": [force_bytes(user.email), force_bytes(user.secondary_email)], "homePhone": [force_bytes(user.phone_number)], } if user.password: @@ -80,17 +79,16 @@ def create_ldap_account(user, dn, conn): try: conn.add_s(dn, ldif) except ldap.LDAPError as e: - raise InternalError( - _("Failed to create LDAP account: {}").format(e) - ) + raise InternalError(_("Failed to create LDAP account: {}").format(e)) def check_if_dn_exists(conn, dn): """Check if DN already exists in directory.""" try: res = conn.search_s( - force_str(dn), ldap.SCOPE_SUBTREE, - force_str("(&(objectClass=inetOrgPerson))") + force_str(dn), + ldap.SCOPE_SUBTREE, + force_str("(&(objectClass=inetOrgPerson))"), ) res = res[0][0] except ldap.LDAPError: @@ -120,8 +118,7 @@ def update_ldap_account(user, config): try: conn.modify_s(dn, ldif) except ldap.LDAPError as e: - raise InternalError( - _("Failed to update LDAP account: {}").format(e)) + raise InternalError(_("Failed to update LDAP account: {}").format(e)) def delete_ldap_account(user, config): @@ -134,42 +131,33 @@ def delete_ldap_account(user, config): try: conn.delete_s(dn) except ldap.LDAPError as e: - raise InternalError( - _("Failed to delete LDAP account: {}").format(e) - ) + raise InternalError(_("Failed to delete LDAP account: {}").format(e)) else: password = get_user_password(user, True) - ldif = [ - (ldap.MOD_REPLACE, "userPassword", password) - ] + ldif = [(ldap.MOD_REPLACE, "userPassword", password)] try: conn.modify_s(dn, ldif) except ldap.LDAPError as e: - raise InternalError( - _("Failed to disable LDAP account: {}").format(e)) + raise InternalError(_("Failed to disable LDAP account: {}").format(e)) def find_user_groups(conn, config, dn, entry): """Retrieve groups for given user.""" condition = ( - config["ldap_is_active_directory"] or - config["ldap_group_type"] == "groupofnames" + config["ldap_is_active_directory"] + or config["ldap_group_type"] == "groupofnames" ) if condition: flt = "(member={})".format(dn) elif config["ldap_group_type"] == "posixgroup": flt = "(memberUid={})".format(force_str(entry["uid"][0])) - result = conn.search_s( - config["ldap_groups_search_base"], - ldap.SCOPE_SUBTREE, - flt - ) + result = conn.search_s(config["ldap_groups_search_base"], ldap.SCOPE_SUBTREE, flt) groups = [] for dn, entry in result: if not dn: continue - groups.append(dn.split(',')[0].split('=')[1]) + groups.append(dn.split(",")[0].split("=")[1]) return groups @@ -189,7 +177,7 @@ def import_accounts_from_ldap(config): result = conn.search_s( config["ldap_import_search_base"], ldap.SCOPE_SUBTREE, - config["ldap_import_search_filter"] + config["ldap_import_search_filter"], ) admin_groups = config["ldap_admin_groups"].split(";") for dn, entry in result: @@ -219,20 +207,15 @@ def import_accounts_from_ldap(config): defaults = { "username": username.lower(), "is_local": False, - "language": settings.LANGUAGE_CODE + "language": settings.LANGUAGE_CODE, } user, created = core_models.User.objects.get_or_create( - username__iexact=username, - defaults=defaults + username__iexact=username, defaults=defaults ) if created: core_models.populate_callback(user, role) - attr_map = { - "first_name": "givenName", - "email": "mail", - "last_name": "sn" - } + attr_map = {"first_name": "givenName", "email": "mail", "last_name": "sn"} for attr, ldap_attr in attr_map.items(): if ldap_attr in entry: setattr(user, attr, force_str(entry[ldap_attr][0])) @@ -243,13 +226,13 @@ def import_accounts_from_ldap(config): def build_ldap_uri(config, node=""): - """ Building LDAP uris for dovecot conf """ + """Building LDAP uris for dovecot conf""" if node: node += "_" return "{}://{}:{}".format( "ldaps" if config["ldap_secured"] == "ssl" else "ldap", config["ldap_{}server_address".format(node)], - config["ldap_{}server_port".format(node)] + config["ldap_{}server_port".format(node)], ) @@ -271,12 +254,19 @@ def update_dovecot_config_file(config): user_filter = config["ldap_search_filter"].replace("(user)s", "u") with open(conf_file, "w") as fp: - fp.write("""uris = {uris} + fp.write( + """uris = {uris} dn = "{bind_dn}" dnpass = '{bind_pwd}' base = {base} user_filter = {user_filter} pass_filter = {pass_filter} -""".format(uris=uris, bind_dn=bind_dn, bind_pwd=bind_pwd, base=base, - user_filter=user_filter, pass_filter=user_filter) +""".format( + uris=uris, + bind_dn=bind_dn, + bind_pwd=bind_pwd, + base=base, + user_filter=user_filter, + pass_filter=user_filter, + ) ) diff --git a/modoboa/ldapsync/management/commands/update_dovecot_conf.py b/modoboa/ldapsync/management/commands/update_dovecot_conf.py index dc476beeb..a1c3f9402 100644 --- a/modoboa/ldapsync/management/commands/update_dovecot_conf.py +++ b/modoboa/ldapsync/management/commands/update_dovecot_conf.py @@ -20,8 +20,7 @@ class Command(BaseCommand): return config = dict(param_tools.get_global_parameters("core")) condition = ( - config["authentication_type"] == "ldap" and - config["ldap_dovecot_sync"] + config["authentication_type"] == "ldap" and config["ldap_dovecot_sync"] ) if condition: lib.update_dovecot_config_file(config) diff --git a/modoboa/ldapsync/tests.py b/modoboa/ldapsync/tests.py index 0429cebdb..5da55337b 100644 --- a/modoboa/ldapsync/tests.py +++ b/modoboa/ldapsync/tests.py @@ -22,19 +22,22 @@ class LDAPExportTestCase(ModoTestCase): def setUp(self): super().setUp() - self.set_global_parameters({ - "ldap_enable_sync": True, - "ldap_server_port": settings.LDAP_SERVER_PORT, - "ldap_sync_bind_dn": "cn=admin,dc=example,dc=com", - "ldap_sync_bind_password": "test", - "ldap_sync_account_dn_template": ( - "cn=%(user)s,ou=users,dc=example,dc=com"), - }, app="core") + self.set_global_parameters( + { + "ldap_enable_sync": True, + "ldap_server_port": settings.LDAP_SERVER_PORT, + "ldap_sync_bind_dn": "cn=admin,dc=example,dc=com", + "ldap_sync_bind_password": "test", + "ldap_sync_account_dn_template": ( + "cn=%(user)s,ou=users,dc=example,dc=com" + ), + }, + app="core", + ) self.config = dict(param_tools.get_global_parameters("core")) self.conn = lib.get_connection(self.config) self.username = "testldap@test.com" - self.dn = self.config["ldap_sync_account_dn_template"] % { - "user": self.username} + self.dn = self.config["ldap_sync_account_dn_template"] % {"user": self.username} def reset_ldap_directory(self): try: @@ -48,7 +51,7 @@ class LDAPExportTestCase(ModoTestCase): username=self.username, first_name="Test", last_name="LDAP", - groups=("SimpleUsers",) + groups=("SimpleUsers",), ) self.assertTrue(lib.check_if_dn_exists(self.conn, self.dn)) @@ -59,8 +62,9 @@ class LDAPExportTestCase(ModoTestCase): lib.get_connection(self.config, self.dn, "toto") res = self.conn.search_s( - force_str(self.dn), ldap.SCOPE_SUBTREE, - force_str("(&(objectClass=inetOrgPerson))") + force_str(self.dn), + ldap.SCOPE_SUBTREE, + force_str("(&(objectClass=inetOrgPerson))"), ) self.assertIn(force_bytes(user.last_name), res[0][1]["sn"]) @@ -70,7 +74,7 @@ class LDAPExportTestCase(ModoTestCase): username=self.username, first_name="Test", last_name="LDAP", - groups=("DomainAdmins", ) + groups=("DomainAdmins",), ) self.assertFalse(lib.check_if_dn_exists(self.conn, self.dn)) @@ -80,12 +84,13 @@ class LDAPExportTestCase(ModoTestCase): username=self.username, first_name="Test", last_name="LDAP", - groups=("SimpleUsers",) + groups=("SimpleUsers",), ) user.delete() ldap_record = self.conn.search_s( - force_str(self.dn), ldap.SCOPE_SUBTREE, - force_str("(&(objectClass=inetOrgPerson))") + force_str(self.dn), + ldap.SCOPE_SUBTREE, + force_str("(&(objectClass=inetOrgPerson))"), ) password = ldap_record[0][1]["userPassword"][0].split(b"}")[1] self.assertTrue(password.startswith(b"#")) @@ -96,10 +101,9 @@ class LDAPExportTestCase(ModoTestCase): username=self.username, first_name="Test", last_name="LDAP", - groups=("SimpleUsers",) + groups=("SimpleUsers",), ) - self.set_global_parameter( - "ldap_sync_delete_remote_account", True, app="core") + self.set_global_parameter("ldap_sync_delete_remote_account", True, app="core") user.delete() self.assertFalse(lib.check_if_dn_exists(self.conn, self.dn)) @@ -110,23 +114,27 @@ class LDAPImportTestCase(ModoTestCase): def setUp(self): super().setUp() - self.set_global_parameters({ - "ldap_enable_import": True, - "ldap_server_port": settings.LDAP_SERVER_PORT, - "ldap_sync_bind_dn": "cn=admin,dc=example,dc=com", - "ldap_sync_bind_password": "test", - "ldap_import_search_base": "ou=users,dc=example,dc=com", - "ldap_import_search_filter": "(objectClass=person)", - "ldap_groups_search_base": "ou=groups,dc=example,dc=com", - "ldap_admin_groups": "admins", - }, app="core") + self.set_global_parameters( + { + "ldap_enable_import": True, + "ldap_server_port": settings.LDAP_SERVER_PORT, + "ldap_sync_bind_dn": "cn=admin,dc=example,dc=com", + "ldap_sync_bind_password": "test", + "ldap_import_search_base": "ou=users,dc=example,dc=com", + "ldap_import_search_filter": "(objectClass=person)", + "ldap_groups_search_base": "ou=groups,dc=example,dc=com", + "ldap_admin_groups": "admins", + }, + app="core", + ) self.config = dict(param_tools.get_global_parameters("core")) self.conn = lib.get_connection(self.config) def test_import_from_ldap(self): """Check management command.""" call_command("import_from_ldap_directory") - self.assertTrue(core_models.User.objects.filter( - username="testuser@example.com").exists()) + self.assertTrue( + core_models.User.objects.filter(username="testuser@example.com").exists() + ) admin = core_models.User.objects.get(username="mailadmin@example.com") self.assertEqual(admin.role, "DomainAdmins") diff --git a/modoboa/lib/api_client.py b/modoboa/lib/api_client.py index e47b38cf1..1f485fd55 100644 --- a/modoboa/lib/api_client.py +++ b/modoboa/lib/api_client.py @@ -13,13 +13,13 @@ logger = logging.getLogger("modoboa.admin") class ModoAPIClient(object): - """A simple client for the public API.""" def __init__(self, api_url=None): """Constructor.""" if api_url is None: from django.conf import settings + self._api_url = settings.MODOBOA_API_URL else: self._api_url = api_url @@ -32,9 +32,7 @@ class ModoAPIClient(object): try: resp = requests.get(url, params=params) except RequestException as err: - logger.critical( - _("Failed to communicate with public API: %s"), str(err) - ) + logger.critical(_("Failed to communicate with public API: %s"), str(err)) return None if resp.status_code != 200: return None @@ -46,7 +44,8 @@ class ModoAPIClient(object): if self._local_core_version is None: try: self._local_core_version = pkg_resources.get_distribution( - "modoboa").version + "modoboa" + ).version except pkg_resources.DistributionNotFound: self._local_core_version = "unknown" return self._local_core_version @@ -58,13 +57,11 @@ class ModoAPIClient(object): def register_instance(self, hostname): """Register this instance.""" - url = "{}instances/search/?hostname={}".format( - self._api_url, hostname) + url = "{}instances/search/?hostname={}".format(self._api_url, hostname) instance = self.__send_request(url) if instance is None: url = "{}instances/".format(self._api_url) - data = { - "hostname": hostname, "known_version": self.local_core_version} + data = {"hostname": hostname, "known_version": self.local_core_version} try: response = requests.post(url, data=data) except RequestException as err: @@ -83,9 +80,7 @@ class ModoAPIClient(object): try: response = requests.put(url, data=data) except RequestException as err: - logger.critical( - _("Failed to communicate with public API: %s"), str(err) - ) + logger.critical(_("Failed to communicate with public API: %s"), str(err)) return if response.status_code != 200: logger.critical( diff --git a/modoboa/lib/authbackends.py b/modoboa/lib/authbackends.py index 34b64641e..543efd0f8 100644 --- a/modoboa/lib/authbackends.py +++ b/modoboa/lib/authbackends.py @@ -37,11 +37,12 @@ class SMTPBackend(object): We assume the username is a valid email address. """ user, created = User.objects.get_or_create( - username__iexact=username, defaults={ + username__iexact=username, + defaults={ "username": username.lower(), "email": username.lower(), - "language": settings.LANGUAGE_CODE - } + "language": settings.LANGUAGE_CODE, + }, ) if created: populate_callback(user) @@ -58,9 +59,7 @@ class SMTPBackend(object): try: - from django_auth_ldap.backend import ( - LDAPBackend as orig_LDAPBackend, _LDAPUser - ) + from django_auth_ldap.backend import LDAPBackend as orig_LDAPBackend, _LDAPUser class LDAPBackendBase(orig_LDAPBackend): @@ -68,8 +67,7 @@ try: """Load LDAP settings.""" param_tools.apply_to_django_settings() super().__init__(*args, **kwargs) - self.global_params = dict( - param_tools.get_global_parameters("core")) + self.global_params = dict(param_tools.get_global_parameters("core")) def get_or_build_user(self, username, ldap_user): """ @@ -88,7 +86,7 @@ try: if domain is None: # Try to find associated email email = None - for attr in ['mail', 'userPrincipalName']: + for attr in ["mail", "userPrincipalName"]: if attr in ldap_user.attrs: email = ldap_user.attrs[attr][0] break @@ -104,8 +102,8 @@ try: defaults={ "username": username.lower(), "is_local": False, - "language": settings.LANGUAGE_CODE - } + "language": settings.LANGUAGE_CODE, + }, ) if created: populate_callback(user, group) @@ -130,7 +128,6 @@ try: """Return fullname for given setting.""" return "{}{}".format(cls.settings_prefix, setting) - class LDAPBackend(LDAPBackendBase): """Primary LDAP backend.""" @@ -138,7 +135,6 @@ try: srv_address_setting_name = "ldap_server_address" srv_port_setting_name = "ldap_server_port" - class LDAPSecondaryBackend(LDAPBackendBase): """Secondary LDAP backend.""" @@ -146,6 +142,5 @@ try: srv_address_setting_name = "ldap_secondary_server_address" srv_port_setting_name = "ldap_secondary_server_port" - except ImportError: pass diff --git a/modoboa/lib/connections.py b/modoboa/lib/connections.py index fcc1b51eb..d830d509c 100644 --- a/modoboa/lib/connections.py +++ b/modoboa/lib/connections.py @@ -2,7 +2,6 @@ from modoboa.lib.cryptutils import decrypt class ConnectionsManager(type): - """ Singleton pattern implementation. @@ -25,8 +24,7 @@ class ConnectionsManager(type): kwargs["password"] = decrypt(kwargs["password"]) if self.instances[key] is None: - self.instances[key] = \ - super(ConnectionsManager, self).__call__(**kwargs) + self.instances[key] = super(ConnectionsManager, self).__call__(**kwargs) else: self.instances[key].refresh(key, kwargs["password"]) return self.instances[key] diff --git a/modoboa/lib/db_utils.py b/modoboa/lib/db_utils.py index 03b279f80..6c3f0e575 100644 --- a/modoboa/lib/db_utils.py +++ b/modoboa/lib/db_utils.py @@ -19,8 +19,7 @@ def db_type(cname="default"): :return: a string or None """ if cname not in settings.DATABASES: - raise InternalError( - _("Connection to database %s not configured" % cname)) + raise InternalError(_("Connection to database %s not configured" % cname)) for t in ["postgres", "mysql", "sqlite"]: if settings.DATABASES[cname]["ENGINE"].find(t) != -1: return t diff --git a/modoboa/lib/email_utils.py b/modoboa/lib/email_utils.py index ffb102ec3..793304d8b 100644 --- a/modoboa/lib/email_utils.py +++ b/modoboa/lib/email_utils.py @@ -68,8 +68,7 @@ class Email(object): # updated to use True or False. self.links = bool(links) elif links == "0": - raise TypeError("links == \"0\" is not valid, did you mean True or " - "False?") + raise TypeError('links == "0" is not valid, did you mean True or ' "False?") else: raise TypeError("links should be a boolean value") @@ -102,10 +101,7 @@ class Email(object): if self._msg is not None: for header in self._basic_headers: value = self.get_header(self.msg, header) - self._headers.append({ - "name": header, - "value": value - }) + self._headers.append({"name": header, "value": value}) return self._headers @@ -173,11 +169,9 @@ class Email(object): self._parse_inline_image(msg, level) if self.contents["plain"]: - self.contents["plain"] = self._post_process_plain( - self.contents["plain"]) + self.contents["plain"] = self._post_process_plain(self.contents["plain"]) if self.contents["html"]: - self.contents["html"] = self._post_process_html( - self.contents["html"]) + self.contents["html"] = self._post_process_html(self.contents["html"]) def _parse_text(self, msg, level=0): content_type = msg.get_content_subtype() @@ -222,12 +216,12 @@ class Email(object): return self._images[cid] = "data:%s;base64,%s" % ( - content_type, "".join(msg.get_payload().splitlines(False))) + content_type, + "".join(msg.get_payload().splitlines(False)), + ) def _post_process_plain(self, content): - mail_text = ( - _RE_REMOVE_EXTRA_WHITESPACE.sub("\n\n", content).strip() - ) + mail_text = _RE_REMOVE_EXTRA_WHITESPACE.sub("\n\n", content).strip() mail_text = escape(mail_text) return smart_str(mail_text) @@ -249,10 +243,9 @@ class Email(object): embedded=True, frames=True, add_nofollow=True, - safe_attrs=safe_attrs + safe_attrs=safe_attrs, ) - mail_text = lxml.html.tostring( - cleaner.clean_html(html), encoding="unicode") + mail_text = lxml.html.tostring(cleaner.clean_html(html), encoding="unicode") return smart_str(mail_text) def _map_cid(self, url): @@ -288,8 +281,7 @@ class Email(object): def split_address(address): """Split an e-mail address into local part and domain.""" - assert isinstance(address, str),\ - "address should be of type str" + assert isinstance(address, str), "address should be of type str" if "@" not in address: local_part = address domain = None @@ -300,27 +292,25 @@ def split_address(address): def split_local_part(local_part, delimiter=None): """Split a local part into local part and extension.""" - assert isinstance(local_part, str),\ - "local_part should be of type str" - assert isinstance(delimiter, str) or delimiter is None,\ - "delimiter should be of type str" + assert isinstance(local_part, str), "local_part should be of type str" + assert ( + isinstance(delimiter, str) or delimiter is None + ), "delimiter should be of type str" extension = None if local_part.lower() in ["mailer-daemon", "double-bounce"]: # never split these special addresses pass - elif ( - delimiter == "-" and - (local_part.startswith("owner-") or - local_part.endswith("-request")) + elif delimiter == "-" and ( + local_part.startswith("owner-") or local_part.endswith("-request") ): # don't split owner-* or *-request if - is the delimiter, they are # special addresses used by mail lists. pass elif ( - delimiter and - delimiter in local_part and - local_part[0] != delimiter and - local_part[-1] != delimiter + delimiter + and delimiter in local_part + and local_part[0] != delimiter + and local_part[-1] != delimiter ): local_part, extension = local_part.split(delimiter, 1) @@ -345,8 +335,7 @@ def split_mailbox(mailbox, return_extension=False): def decode(value_bytes, encoding, append_to_error=""): """Try to decode the given string.""" - assert isinstance(value_bytes, bytes),\ - "value_bytes should be of type bytes" + assert isinstance(value_bytes, bytes), "value_bytes should be of type bytes" if len(value_bytes) == 0: # short circuit for empty strings return "" @@ -355,13 +344,10 @@ def decode(value_bytes, encoding, append_to_error=""): except (UnicodeDecodeError, LookupError): encoding = chardet.detect(value_bytes) try: - value = value_bytes.decode( - encoding["encoding"], "replace" - ) + value = value_bytes.decode(encoding["encoding"], "replace") except (TypeError, UnicodeDecodeError) as exc: raise InternalError( - _("unable to determine encoding of string") + - append_to_error + _("unable to determine encoding of string") + append_to_error ) from exc return value @@ -399,8 +385,9 @@ def set_email_headers(msg, subject, sender, rcpt): msg["From"] = sender msg["To"] = prepare_addresses(rcpt) msg["Message-ID"] = make_msgid() - msg["User-Agent"] = "Modoboa %s" % \ - (pkg_resources.get_distribution("modoboa").version) + msg["User-Agent"] = "Modoboa %s" % ( + pkg_resources.get_distribution("modoboa").version + ) msg["Date"] = formatdate(time.time(), True) @@ -426,8 +413,7 @@ def _sendmail(sender, rcpt, msgstring, server="localhost", port=25): return True, None -def sendmail_simple( - sender, rcpt, subject="Sample message", content="", **kwargs): +def sendmail_simple(sender, rcpt, subject="Sample message", content="", **kwargs): """Simple way to send a text message Send a text/plain message with basic headers (msg-id, date). @@ -465,7 +451,10 @@ def sendmail_fromfile(sender, rcpt, fname): content = """From: %s To: %s -""" % (sender, rcpt) +""" % ( + sender, + rcpt, + ) content += fp.read() fp.close() @@ -475,11 +464,13 @@ To: %s def send_notification(recipient, subject, tpl, sender=None, **kwargs): """Send notification by email.""" if not sender: - local_config = ( - core_models.LocalConfig.objects.select_related("site").first()) + local_config = core_models.LocalConfig.objects.select_related("site").first() sender = local_config.parameters.get_value("sender_address") content = render_to_string(tpl, kwargs) msg = EmailMessage( - subject, content.strip(), sender, [recipient], + subject, + content.strip(), + sender, + [recipient], ) msg.send() diff --git a/modoboa/lib/exceptions.py b/modoboa/lib/exceptions.py index 8b6dc22fe..25eae6697 100644 --- a/modoboa/lib/exceptions.py +++ b/modoboa/lib/exceptions.py @@ -11,6 +11,7 @@ class ModoboaException(Exception): """ Base class for Modoboa custom exceptions. """ + http_code = None def __init__(self, *args, **kwargs): @@ -24,6 +25,7 @@ class InternalError(ModoboaException): """ Use this exception for system errors, missing dependencies, etc. """ + http_code = 500 @@ -33,6 +35,7 @@ class BadRequest(ModoboaException): format (example: wrong CSV line) or doesn't respect validation rules. """ + http_code = 400 @@ -41,6 +44,7 @@ class NotFound(ModoboaException): Use this exception to indicate the requested resource could not be found. """ + http_code = 404 @@ -49,6 +53,7 @@ class Conflict(ModoboaException): Use this exception to indicate that the request could not be processed because of conflict in the request. """ + http_code = 409 @@ -67,6 +72,7 @@ class PermDeniedException(ModoboaException): Use this exception when a user tries to do something he is not allowed to. """ + http_code = 403 def __init__(self, msg=None): diff --git a/modoboa/lib/fields.py b/modoboa/lib/fields.py index 19941473c..65fa08ac0 100644 --- a/modoboa/lib/fields.py +++ b/modoboa/lib/fields.py @@ -11,9 +11,7 @@ from . import validators class DomainNameField(forms.fields.CharField): """A subclass of CharField that only accepts a valid domain name.""" - default_error_messages = { - "invalid": gettext_lazy("Enter a valid domain name") - } + default_error_messages = {"invalid": gettext_lazy("Enter a valid domain name")} default_validators = [validators.validate_hostname] @@ -40,26 +38,24 @@ class UTF8AndEmptyUserEmailField(forms.fields.EmailField): class DRFEmailFieldUTF8(serializers.CharField): """Custom DRF email field to support UTF8.""" - default_error_messages = { - "invalid": gettext_lazy("Enter a valid email address.") - } + default_error_messages = {"invalid": gettext_lazy("Enter a valid email address.")} def __init__(self, **kwargs): super(DRFEmailFieldUTF8, self).__init__(**kwargs) validator = validators.UTF8EmailValidator( - message=self.error_messages["invalid"]) + message=self.error_messages["invalid"] + ) self.validators.append(validator) class DRFEmailFieldUTF8AndEmptyUser(serializers.CharField): """Custom DRF email field to support UTF8 and empty local part.""" - default_error_messages = { - "invalid": gettext_lazy("Enter a valid email address.") - } + default_error_messages = {"invalid": gettext_lazy("Enter a valid email address.")} def __init__(self, **kwargs): super(DRFEmailFieldUTF8AndEmptyUser, self).__init__(**kwargs) validator = validators.UTF8AndEmptyUserEmailValidator( - message=self.error_messages["invalid"]) + message=self.error_messages["invalid"] + ) self.validators.append(validator) diff --git a/modoboa/lib/form_utils.py b/modoboa/lib/form_utils.py index 1118fc8f0..0b6d3e750 100644 --- a/modoboa/lib/form_utils.py +++ b/modoboa/lib/form_utils.py @@ -124,10 +124,13 @@ class WizardForm(ABC): if self.steps[stepid].check_access(self): break stepid -= 1 - return render_to_json_response({ - "title": self.steps[stepid].title, "id": self.steps[stepid].uid, - "stepid": stepid - }) + return render_to_json_response( + { + "title": self.steps[stepid].title, + "id": self.steps[stepid].uid, + "stepid": stepid, + } + ) def next_step(self): """Go to the next step if previous forms are valid.""" @@ -138,24 +141,30 @@ class WizardForm(ABC): if self.steps[cpt].check_access(self): statuses.append(self.steps[cpt].form.is_valid()) if False in statuses: - return render_to_json_response({ - "stepid": stepid, "id": self.steps[stepid - 1].uid, - "form_errors": self.errors - }, status=400) + return render_to_json_response( + { + "stepid": stepid, + "id": self.steps[stepid - 1].uid, + "form_errors": self.errors, + }, + status=400, + ) while stepid < len(self.steps): if self.steps[stepid].check_access(self): break stepid += 1 if stepid == len(self.steps): return self.done() - return render_to_json_response({ - "title": self.steps[stepid].title, "id": self.steps[stepid].uid, - "stepid": stepid - }) + return render_to_json_response( + { + "title": self.steps[stepid].title, + "id": self.steps[stepid].uid, + "stepid": stepid, + } + ) def extra_context(self, context): - """Provide additional information to template's context. - """ + """Provide additional information to template's context.""" pass def process(self): @@ -180,7 +189,6 @@ class WizardForm(ABC): class DynamicForm(object): - """ A form which accepts dynamic fields. @@ -193,8 +201,7 @@ class DynamicForm(object): data = {} def _create_field(self, typ, name, value=None, pos=None): - """Create a new form field. - """ + """Create a new form field.""" self.fields[name] = typ(label="", required=False) if value is not None: self.fields[name].initial = value @@ -211,7 +218,7 @@ class DynamicForm(object): :param string pattern: pattern used to find field instances :param typ: a form field class """ - expr = re.compile(r'%s_\d+' % pattern) + expr = re.compile(r"%s_\d+" % pattern) values = [] for k, v in list(qdict.items()): if k == pattern or expr.match(k): @@ -292,8 +299,7 @@ class TabForms(object): """ to_remove = [] for f in self.forms: - if mandatory_only and \ - ("mandatory" not in f or not f["mandatory"]): + if mandatory_only and ("mandatory" not in f or not f["mandatory"]): continue elif optional_only and ("mandatory" in f and f["mandatory"]): continue @@ -308,8 +314,7 @@ class TabForms(object): @abc.abstractmethod def save(self): - """Save objects here. - """ + """Save objects here.""" def remove_tab(self, tabid): for f in self.forms: @@ -325,8 +330,7 @@ class TabForms(object): yield form def extra_context(self, context): - """"Provide additional information to template's context. - """ + """ "Provide additional information to template's context.""" pass @abc.abstractmethod @@ -337,23 +341,22 @@ class TabForms(object): """ def process(self): - """Process the received request. - """ + """Process the received request.""" if self.request.method == "POST": if self.is_valid(): self.save() return self.done() - return render_to_json_response( - {"form_errors": self.errors}, status=400 - ) + return render_to_json_response({"form_errors": self.errors}, status=400) context = { "tabs": self, } if self.forms: - context.update({ - "action_label": _("Update"), - "action_classes": "submit", - }) + context.update( + { + "action_label": _("Update"), + "action_classes": "submit", + } + ) self.extra_context(context) active_tab_id = self.request.GET.get("active_tab", "default") if active_tab_id != "default": @@ -368,6 +371,7 @@ class UserKwargModelFormMixin: self.user = kwargs.pop("user") super().__init__(*args, **kwargs) + # # Custom fields from here # @@ -391,12 +395,11 @@ class YesNoField(TypedChoiceField): def __init__(self, *args, **kwargs): """Constructor.""" - kwargs.update({ - "choices": ( - (True, gettext_lazy("Yes")), - (False, gettext_lazy("No")) - ), - "widget": HorizontalRadioSelect(), - "coerce": lambda x: x == "True" - }) + kwargs.update( + { + "choices": ((True, gettext_lazy("Yes")), (False, gettext_lazy("No"))), + "widget": HorizontalRadioSelect(), + "coerce": lambda x: x == "True", + } + ) super(YesNoField, self).__init__(*args, **kwargs) diff --git a/modoboa/lib/imap_utf7.py b/modoboa/lib/imap_utf7.py index c40348ab8..ea0a757b2 100644 --- a/modoboa/lib/imap_utf7.py +++ b/modoboa/lib/imap_utf7.py @@ -55,7 +55,7 @@ import codecs # encoding -PRINTABLE = set(range(0x20, 0x26)) | set(range(0x27, 0x7f)) +PRINTABLE = set(range(0x20, 0x26)) | set(range(0x27, 0x7F)) def modified_utf7(s): @@ -135,6 +135,7 @@ codecs.register(imap4_utf_7) # testing methods + def imapUTF7Encode(ust): # NOQA:N802 "Returns imap utf-7 encoded version of string" return ust.encode("imap4-utf-7") @@ -165,7 +166,10 @@ def utf7SequenceToUTF8(seq): # NOQA:N802 return [itm.decode("imap4-utf-7").encode("utf-8") for itm in seq] -__all__ = ["imapUTF7Encode", "imapUTF7Decode", ] +__all__ = [ + "imapUTF7Encode", + "imapUTF7Decode", +] if __name__ == "__main__": diff --git a/modoboa/lib/ldap_utils.py b/modoboa/lib/ldap_utils.py index cbadc8e1e..8c0219c9f 100644 --- a/modoboa/lib/ldap_utils.py +++ b/modoboa/lib/ldap_utils.py @@ -48,9 +48,7 @@ class LDAPAuthBackend(object): def __init__(self): param_tools.apply_to_django_settings() self.global_params = dict(param_tools.get_global_parameters("core")) - self.server_uri = self._setting( - "AUTH_LDAP_SERVER_URI", "ldap://localhost" - ) + self.server_uri = self._setting("AUTH_LDAP_SERVER_URI", "ldap://localhost") self.pwd_attr = self._setting("LDAP_PASSWORD_ATTR", "userPassword") self.ldap_ad = self._setting("LDAP_ACTIVE_DIRECTORY", False) self.conn = None @@ -68,9 +66,7 @@ class LDAPAuthBackend(object): conn = ldap.initialize(self.server_uri) conn.set_option(ldap.OPT_X_TLS_DEMAND, True) conn.set_option(ldap.OPT_DEBUG_LEVEL, 255) - conn.simple_bind_s( - force_str(dn), force_str(password) - ) + conn.simple_bind_s(force_str(dn), force_str(password)) return conn def connect_to_server(self, user, password): @@ -116,14 +112,15 @@ class LDAPAuthBackend(object): self.connect_to_server(user, password) user_dn = self._find_user_dn(user) if self.ldap_ad: - newpassword = ( - ('"%s"' % newpassword).encode("utf-16").lstrip("\377\376") + newpassword = ('"%s"' % newpassword).encode("utf-16").lstrip("\377\376") + ldif = [ + ( + ldap.MOD_REPLACE, + force_str(self.pwd_attr), + force_bytes(self._crypt_password(newpassword)), ) - ldif = [(ldap.MOD_REPLACE, - force_str(self.pwd_attr), - force_bytes(self._crypt_password(newpassword)))] + ] try: self.conn.modify_s(force_str(user_dn), ldif) except ldap.LDAPError as e: - raise InternalError( - _("Failed to update password: {}").format(e)) + raise InternalError(_("Failed to update password: {}").format(e)) diff --git a/modoboa/lib/listing.py b/modoboa/lib/listing.py index 3e5f432d1..7d1f9799d 100644 --- a/modoboa/lib/listing.py +++ b/modoboa/lib/listing.py @@ -36,7 +36,7 @@ def get_listing_page(objects, pagenum): paginator = Paginator( objects, param_tools.get_global_parameter("items_per_page", app="core"), - allow_empty_first_page=False + allow_empty_first_page=False, ) try: page = paginator.page(int(pagenum)) diff --git a/modoboa/lib/middleware.py b/modoboa/lib/middleware.py index 4d02b7f15..db93f0fc5 100644 --- a/modoboa/lib/middleware.py +++ b/modoboa/lib/middleware.py @@ -6,13 +6,11 @@ from django.utils.encoding import smart_str from modoboa.lib import signals as lib_signals from modoboa.lib.exceptions import ModoboaException -from modoboa.lib.web_utils import ( - _render_error, ajax_response, render_to_json_response -) +from modoboa.lib.web_utils import _render_error, ajax_response, render_to_json_response def is_ajax(request): - return request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' + return request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest" class AjaxLoginRedirect(MiddlewareMixin): @@ -34,15 +32,12 @@ class CommonExceptionCatcher(MiddlewareMixin): if is_ajax(request) or "/api/" in request.path: if exception.http_code is None: return ajax_response( - request, status="ko", respmsg=smart_str(exception), - norefresh=True + request, status="ko", respmsg=smart_str(exception), norefresh=True ) return render_to_json_response( smart_str(exception), status=exception.http_code ) - return _render_error( - request, user_context={"error": smart_str(exception)} - ) + return _render_error(request, user_context={"error": smart_str(exception)}) class RequestCatcherMiddleware(MiddlewareMixin): diff --git a/modoboa/lib/migrations/0001_initial.py b/modoboa/lib/migrations/0001_initial.py index 7ffa0a642..b4898789e 100644 --- a/modoboa/lib/migrations/0001_initial.py +++ b/modoboa/lib/migrations/0001_initial.py @@ -10,26 +10,45 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Parameter', + name="Parameter", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=255)), - ('value', models.CharField(max_length=255)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=255)), + ("value", models.CharField(max_length=255)), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.CreateModel( - name='UserParameter', + name="UserParameter", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=255)), - ('value', models.CharField(max_length=255)), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=255)), + ("value", models.CharField(max_length=255)), + ( + "user", + models.ForeignKey( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), + ), ], - options={ - }, + options={}, bases=(models.Model,), ), ] diff --git a/modoboa/lib/migrations/0002_rename_parameters.py b/modoboa/lib/migrations/0002_rename_parameters.py index 90dcb9647..1127cd699 100644 --- a/modoboa/lib/migrations/0002_rename_parameters.py +++ b/modoboa/lib/migrations/0002_rename_parameters.py @@ -12,13 +12,12 @@ APPLICATIONS = [ ("webmail", "modoboa_webmail"), ] + def rename_app_parameters(app, model): """Rename all parameters for a given app.""" qset = model.objects.filter(name__startswith=app[0]) for param in qset: - param.name = param.name.replace( - "{}.".format(app[0]), "{}.".format(app[1]) - ) + param.name = param.name.replace("{}.".format(app[0]), "{}.".format(app[1])) param.save() @@ -34,9 +33,7 @@ def rename_parameters(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('lib', '0001_initial'), + ("lib", "0001_initial"), ] - operations = [ - migrations.RunPython(rename_parameters) - ] + operations = [migrations.RunPython(rename_parameters)] diff --git a/modoboa/lib/migrations/0003_rename_parameters.py b/modoboa/lib/migrations/0003_rename_parameters.py index 4dd10cdff..be11772ce 100644 --- a/modoboa/lib/migrations/0003_rename_parameters.py +++ b/modoboa/lib/migrations/0003_rename_parameters.py @@ -23,10 +23,8 @@ def rename_parameters(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('lib', '0002_rename_parameters'), - ('admin', '0001_initial'), + ("lib", "0002_rename_parameters"), + ("admin", "0001_initial"), ] - operations = [ - migrations.RunPython(rename_parameters) - ] + operations = [migrations.RunPython(rename_parameters)] diff --git a/modoboa/lib/migrations/0004_auto_20151114_1409.py b/modoboa/lib/migrations/0004_auto_20151114_1409.py index 5c62719ba..2804d63ed 100644 --- a/modoboa/lib/migrations/0004_auto_20151114_1409.py +++ b/modoboa/lib/migrations/0004_auto_20151114_1409.py @@ -13,8 +13,8 @@ def move_language_preference(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('lib', '0003_rename_parameters'), - ('core', '0005_user_language'), + ("lib", "0003_rename_parameters"), + ("core", "0005_user_language"), ] operations = [ diff --git a/modoboa/lib/migrations/0005_auto_20160416_1449.py b/modoboa/lib/migrations/0005_auto_20160416_1449.py index 250bc6be7..6d96cc5c9 100644 --- a/modoboa/lib/migrations/0005_auto_20160416_1449.py +++ b/modoboa/lib/migrations/0005_auto_20160416_1449.py @@ -12,9 +12,7 @@ def rename_limits_parameters(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('lib', '0004_auto_20151114_1409'), + ("lib", "0004_auto_20151114_1409"), ] - operations = [ - migrations.RunPython(rename_limits_parameters) - ] + operations = [migrations.RunPython(rename_limits_parameters)] diff --git a/modoboa/lib/migrations/0006_auto_20161104_2206.py b/modoboa/lib/migrations/0006_auto_20161104_2206.py index 10bf96658..89e745294 100644 --- a/modoboa/lib/migrations/0006_auto_20161104_2206.py +++ b/modoboa/lib/migrations/0006_auto_20161104_2206.py @@ -5,18 +5,18 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('lib', '0005_auto_20160416_1449'), + ("lib", "0005_auto_20160416_1449"), ] operations = [ migrations.DeleteModel( - name='Parameter', + name="Parameter", ), migrations.RemoveField( - model_name='userparameter', - name='user', + model_name="userparameter", + name="user", ), migrations.DeleteModel( - name='UserParameter', + name="UserParameter", ), ] diff --git a/modoboa/lib/paginator.py b/modoboa/lib/paginator.py index 3dfc8f54a..36f534879 100644 --- a/modoboa/lib/paginator.py +++ b/modoboa/lib/paginator.py @@ -2,8 +2,8 @@ Custom pagination tools. """ -class Page(object): +class Page(object): """Class to represent a page.""" def __init__(self, paginator, pageid, id_start, id_stop, baseurl=None): @@ -45,9 +45,7 @@ class Page(object): def has_next(self): """Tell if a next page is available or not.""" if self._has_next is None: - self._has_next = ( - True if self.id_stop < self.paginator.total else False - ) + self._has_next = True if self.id_stop < self.paginator.total else False return self._has_next @property @@ -69,7 +67,6 @@ class Page(object): class Paginator(object): - """Pagination class.""" def __init__(self, total, elems_per_page): diff --git a/modoboa/lib/permissions.py b/modoboa/lib/permissions.py index 2c5efb4bb..61dc3fb7f 100644 --- a/modoboa/lib/permissions.py +++ b/modoboa/lib/permissions.py @@ -20,16 +20,15 @@ def get_account_roles(user, account=None): """ result = [core_constants.SIMPLEUSERS_ROLE] filters = core_signals.user_can_set_role.send( - sender="get_account_roles", user=user, role="DomainAdmins", - account=account) - condition = ( - user.has_perm("admin.add_domain") and - (not filters or True in [flt[1] for flt in filters])) + sender="get_account_roles", user=user, role="DomainAdmins", account=account + ) + condition = user.has_perm("admin.add_domain") and ( + not filters or True in [flt[1] for flt in filters] + ) if condition: result += [core_constants.DOMAINADMINS_ROLE] if user.is_superuser: - result += [ - core_constants.RESELLERS_ROLE, core_constants.SUPERADMINS_ROLE] + result += [core_constants.RESELLERS_ROLE, core_constants.SUPERADMINS_ROLE] return sorted(result, key=lambda role: role[1]) @@ -51,7 +50,8 @@ def grant_access_to_object(user, obj, is_owner=False): """ ct = ContentType.objects.get_for_model(obj) entry, created = ObjectAccess.objects.get_or_create( - user=user, content_type=ct, object_id=obj.id) + user=user, content_type=ct, object_id=obj.id + ) entry.is_owner = is_owner entry.save() if not created or not is_owner: @@ -59,9 +59,7 @@ def grant_access_to_object(user, obj, is_owner=False): for su in User.objects.filter(is_superuser=True): if su == user: continue - ObjectAccess.objects.get_or_create( - user=su, content_type=ct, object_id=obj.id - ) + ObjectAccess.objects.get_or_create(user=su, content_type=ct, object_id=obj.id) def grant_access_to_objects(user, objects, ct): @@ -75,8 +73,7 @@ def grant_access_to_objects(user, objects, ct): :param ct: the content type """ for obj in objects: - ObjectAccess.objects.get_or_create( - user=user, content_type=ct, object_id=obj.id) + ObjectAccess.objects.get_or_create(user=user, content_type=ct, object_id=obj.id) def ungrant_access_to_object(obj, user=None): @@ -100,17 +97,11 @@ def ungrant_access_to_object(obj, user=None): except ObjectAccess.DoesNotExist: pass try: - ObjectAccess.objects.get( - content_type=ct, object_id=obj.id, is_owner=True - ) + ObjectAccess.objects.get(content_type=ct, object_id=obj.id, is_owner=True) except ObjectAccess.DoesNotExist: - grant_access_to_object( - User.objects.filter(is_superuser=True)[0], obj, True - ) + grant_access_to_object(User.objects.filter(is_superuser=True)[0], obj, True) else: - ObjectAccess.objects.filter( - content_type=ct, object_id=obj.id - ).delete() + ObjectAccess.objects.filter(content_type=ct, object_id=obj.id).delete() def ungrant_access_to_objects(objects): @@ -146,8 +137,7 @@ def add_permissions_to_group(group, permissions): for appname, modelname, permname in permissions: ct = ContentType.objects.get_by_natural_key(appname, modelname) - if group.permissions.filter( - content_type=ct, codename=permname).exists(): + if group.permissions.filter(content_type=ct, codename=permname).exists(): continue group.permissions.add( Permission.objects.get(content_type=ct, codename=permname) diff --git a/modoboa/lib/singleton.py b/modoboa/lib/singleton.py index 977a8590b..e49acc309 100644 --- a/modoboa/lib/singleton.py +++ b/modoboa/lib/singleton.py @@ -4,8 +4,7 @@ class _Singleton(type): def __call__(self, *args, **kwargs): if self not in self._instances: - self._instances[self] = \ - super(_Singleton, self).__call__(*args, **kwargs) + self._instances[self] = super(_Singleton, self).__call__(*args, **kwargs) return self._instances[self] diff --git a/modoboa/lib/sysutils.py b/modoboa/lib/sysutils.py index 6877b3fd0..ded7c0a55 100644 --- a/modoboa/lib/sysutils.py +++ b/modoboa/lib/sysutils.py @@ -62,8 +62,9 @@ def doveadm_cmd(params, pinput=None, capture_output=True, **kwargs): dpath = force_str(output).strip() else: known_paths = getattr( - settings, "DOVEADM_LOOKUP_PATH", - ("/usr/bin/doveadm", "/usr/local/bin/doveadm") + settings, + "DOVEADM_LOOKUP_PATH", + ("/usr/bin/doveadm", "/usr/local/bin/doveadm"), ) for fpath in known_paths: if os.path.isfile(fpath) and os.access(fpath, os.X_OK): @@ -73,11 +74,13 @@ def doveadm_cmd(params, pinput=None, capture_output=True, **kwargs): 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) + return exec_cmd( + "{} {}".format(dpath, params), + sudo_user=sudo_user, + pinput=pinput, + capture_output=capture_output, + **kwargs, + ) raise OSError("doveadm command not found") diff --git a/modoboa/lib/templatetags/form_tags.py b/modoboa/lib/templatetags/form_tags.py index 869f9fdd5..4bd43ceb7 100644 --- a/modoboa/lib/templatetags/form_tags.py +++ b/modoboa/lib/templatetags/form_tags.py @@ -26,8 +26,9 @@ def render_form(form, tpl=None): def configure_field_classes(field): """Add required CSS classes to field.""" - if isinstance(field.field.widget, forms.CheckboxInput) or \ - isinstance(field.field.widget, forms.RadioSelect): + if isinstance(field.field.widget, forms.CheckboxInput) or isinstance( + field.field.widget, forms.RadioSelect + ): return if "class" in field.field.widget.attrs: field.field.widget.attrs["class"] += " form-control" @@ -36,9 +37,7 @@ def configure_field_classes(field): @register.simple_tag -def render_field( - field, help_display_mode="tooltip", label_width="col-sm-4", - **options): +def render_field(field, help_display_mode="tooltip", label_width="col-sm-4", **options): """Render a field.""" from modoboa.core.templatetags.core_tags import visirule @@ -46,8 +45,10 @@ def render_field( return "%s" % (visirule(field), smart_str(field.label)) configure_field_classes(field) context = { - "field": field, "help_display_mode": help_display_mode, - "label_width": label_width, "deactivate_if_empty": True + "field": field, + "help_display_mode": help_display_mode, + "label_width": label_width, + "deactivate_if_empty": True, } context.update(options) return render_to_string("common/generic_field.html", context) @@ -75,8 +76,13 @@ def render_fields_group(form, pattern): return render_to_string( "common/generic_fields_group.html", - {"label": label, "help_text": first.help_text, "group": group, - "haserror": haserror, "pattern": pattern} + { + "label": label, + "help_text": first.help_text, + "group": group, + "haserror": haserror, + "pattern": pattern, + }, ) diff --git a/modoboa/lib/templatetags/lib_tags.py b/modoboa/lib/templatetags/lib_tags.py index 2ca57dcbe..64e931f15 100644 --- a/modoboa/lib/templatetags/lib_tags.py +++ b/modoboa/lib/templatetags/lib_tags.py @@ -29,16 +29,19 @@ def tolist(values): @register.simple_tag def alert(msg, typ): - t = Template("""""" + ) # NOQA:E501 return t.render(Context({"type": typ, "msg": msg})) @register.simple_tag def render_link(linkdef, mdclass=""): - t = Template(""" {% if link.img %}{% endif %} -{{ link.label }}""") # NOQA:E501 +{{ link.label }}""" + ) # NOQA:E501 return t.render(Context({"link": linkdef, "mdclass": mdclass})) @register.simple_tag def render_post_link(linkdef, request): - t = Template("""
+ t = Template( + """ {% csrf_token %} {% if link.img %}{% endif %}{{ link.label }} -
""") +""" + ) return t.render(RequestContext(request, {"link": linkdef})) @@ -77,12 +83,14 @@ def fromunix(value): @register.simple_tag def render_tags(tags): - t = Template("""{% for tag in tags %} + t = Template( + """{% for tag in tags %} {{ tag.label }} {% endfor %} -""") # NOQA:E501 +""" + ) # NOQA:E501 return t.render(Context({"tags": tags})) @@ -94,25 +102,21 @@ def extra_static_content(caller, st_type, user): :param str st_type: content type (css or js) :param ``User`` user: connected user """ - tpl = template.Template( - "{% for sc in static_content %}{{ sc|safe }}{% endfor %}" - ) + tpl = template.Template("{% for sc in static_content %}{{ sc|safe }}{% endfor %}") static_content = core_signals.extra_static_content.send( - sender="extra_static_content", - caller=caller, st_type=st_type, user=user) - static_content = [result[1] for result in static_content] - return tpl.render( - template.Context({"static_content": static_content}) + sender="extra_static_content", caller=caller, st_type=st_type, user=user ) + static_content = [result[1] for result in static_content] + return tpl.render(template.Context({"static_content": static_content})) @register.filter(name="localize_header_name") def localize_header_name(headername): - """ Localizes the header names """ + """Localizes the header names""" names = { "From": _("From"), "To": _("To"), "Date": _("Date"), - "Subject": _("Subject") + "Subject": _("Subject"), } return names.get(headername, headername) diff --git a/modoboa/lib/test_utils.py b/modoboa/lib/test_utils.py index e4bb1e4d9..60fc012fd 100644 --- a/modoboa/lib/test_utils.py +++ b/modoboa/lib/test_utils.py @@ -23,8 +23,8 @@ class MapFilesTestCaseMixin(object): def _test_maps_generation(self, engine): dburl = "{0}://user:password@localhost/testdb".format(engine) call_command( - "generate_postfix_maps", - "--dburl", dburl, "--destdir", self.workdir) + "generate_postfix_maps", "--dburl", dburl, "--destdir", self.workdir + ) for mapfile in self.MAP_FILES: path = "{0}/{1}".format(self.workdir, mapfile) diff --git a/modoboa/lib/tests/__init__.py b/modoboa/lib/tests/__init__.py index f37024fab..85363577a 100644 --- a/modoboa/lib/tests/__init__.py +++ b/modoboa/lib/tests/__init__.py @@ -24,6 +24,7 @@ except socket.error: try: import ldap # noqa + NO_LDAP = False except ImportError: NO_LDAP = True @@ -52,6 +53,7 @@ class ParametersMixin(object): self.localconfig.parameters.set_values(parameters, app=app) self.localconfig.save() + class SimpleModoTestCase(ParametersMixin, TestCase): """Simple class to add parameters editing.""" @@ -67,8 +69,7 @@ class ModoTestCase(ParametersMixin, TestCase): def setUp(self, username="admin", password="password"): """Initiate test context.""" - self.assertEqual( - self.client.login(username=username, password=password), True) + self.assertEqual(self.client.login(username=username, password=password), True) self.workdir = tempfile.mkdtemp() self.set_global_parameter("storage_dir", self.workdir, app="pdfcredentials") @@ -76,7 +77,8 @@ class ModoTestCase(ParametersMixin, TestCase): if params is None: params = {} response = getattr(self.client, method)( - url, params, HTTP_X_REQUESTED_WITH="XMLHttpRequest") + url, params, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + ) self.assertEqual(response.status_code, status) return response.json() @@ -102,7 +104,8 @@ class ModoAPITestCase(ParametersMixin, APITestCase): super(ModoAPITestCase, cls).setUpTestData() management.call_command("load_initial_data") cls.token = Token.objects.create( - user=core_models.User.objects.get(username="admin")) + user=core_models.User.objects.get(username="admin") + ) def setUp(self): """Setup.""" diff --git a/modoboa/lib/tests/test_cryptutils.py b/modoboa/lib/tests/test_cryptutils.py index e50c3adac..adc1f6d20 100644 --- a/modoboa/lib/tests/test_cryptutils.py +++ b/modoboa/lib/tests/test_cryptutils.py @@ -6,10 +6,10 @@ from django.test.utils import override_settings from modoboa.lib import cryptutils -@override_settings(SECRET_KEY="!8o(-dbbl3e+*bh7nx-^xysdt)1gso*%@4ze4-9_9o+i&" - ";t--u_") +@override_settings( + SECRET_KEY="!8o(-dbbl3e+*bh7nx-^xysdt)1gso*%@4ze4-9_9o+i&" ";t--u_" +) class CryptUtilsTest(SimpleTestCase): - """Tests for modoboa.lib.cryptutils""" def test_valid_secret_key_generated(self): @@ -27,8 +27,10 @@ class CryptUtilsTest(SimpleTestCase): def test_decrypt(self): """Decrypt a string""" - value = "gAAAAABaXSWk4Via-aYP7diek9MmfknUiEY8szrgxIytdXrbfc"\ - "YlbOYiNG01zqkn3a06P1xPXe5XD2SP4UrIvCWzhLs-FO19Cw==" + value = ( + "gAAAAABaXSWk4Via-aYP7diek9MmfknUiEY8szrgxIytdXrbfc" + "YlbOYiNG01zqkn3a06P1xPXe5XD2SP4UrIvCWzhLs-FO19Cw==" + ) expected_output = "test" output = cryptutils.decrypt(value) self.assertEqual(output, expected_output) diff --git a/modoboa/lib/tests/test_email_utils.py b/modoboa/lib/tests/test_email_utils.py index 405cec905..1bb1572ea 100644 --- a/modoboa/lib/tests/test_email_utils.py +++ b/modoboa/lib/tests/test_email_utils.py @@ -5,12 +5,11 @@ import os from django.test import SimpleTestCase from django.utils.encoding import smart_bytes, smart_str -from ..email_utils import ( - Email, prepare_addresses, split_address, split_local_part -) +from ..email_utils import Email, prepare_addresses, split_address, split_local_part SAMPLES_DIR = os.path.realpath( - os.path.join(os.path.dirname(__file__), "sample_messages")) + os.path.join(os.path.dirname(__file__), "sample_messages") +) class EmailTestImplementation(Email): @@ -38,8 +37,7 @@ class EmailTests(SimpleTestCase): def _get_expected_output(self, message_id, **kwargs): ext = kwargs["dformat"] if "dformat" in kwargs else "plain" ext += "_links" if "links" in kwargs and kwargs["links"] else "_nolinks" - message_path = os.path.join(SAMPLES_DIR, - "%s-output-%s.txt" % (message_id, ext)) + message_path = os.path.join(SAMPLES_DIR, "%s-output-%s.txt" % (message_id, ext)) assert os.path.isfile(message_path), "%s does not exist." % message_path with open(message_path, "rb") as fp: @@ -56,19 +54,18 @@ class EmailTests(SimpleTestCase): def test_invalid_links_value(self): """modoboa-amavis set links = "0"; in python "0" == True, fixed in PR 79 - https://github.com/modoboa/modoboa-amavis/pull/79""" + https://github.com/modoboa/modoboa-amavis/pull/79""" with self.assertRaises(TypeError) as cm: EmailTestImplementation("text_plain", links="0") ex_message = cm.exception.messages self.assertEqual( - ex_message, - "links == \"0\" is not valid, did you mean True or False?" + ex_message, 'links == "0" is not valid, did you mean True or False?' ) def test_links_value_for_webmail(self): """modoboa-webmail sets links = 0 or 1 - TODO: this should be fixed in modoboa-webmail to use True/False""" + TODO: this should be fixed in modoboa-webmail to use True/False""" email = EmailTestImplementation("text_plain", links=0) self.assertFalse(email.links) @@ -108,7 +105,6 @@ class EmailTests(SimpleTestCase): class EmailAddressParserTests(SimpleTestCase): - """Tests for split_address() and split_local_part().""" def test_split_address_with_domain(self): @@ -157,13 +153,15 @@ class EmailAddressParserTests(SimpleTestCase): """Check a list of e-mail addresses is sepearted correctly.""" # value is an array with one long string not 3 sepearte values. value = [ - "\"Doe, John\" ;" - "\"John Smith\" ," + '"Doe, John" ;' + '"John Smith" ,' "admin@sub.example.com" ] - expected_output = "\"Doe, John\" ,"\ - "John Smith ,"\ - "admin@sub.example.com" + expected_output = ( + '"Doe, John" ,' + "John Smith ," + "admin@sub.example.com" + ) output = prepare_addresses(value) self.assertEqual(output, expected_output) @@ -171,8 +169,8 @@ class EmailAddressParserTests(SimpleTestCase): """Check a list of e-mail addresses is sepearted correctly.""" # value is an array with one long string not 3 sepearte values. value = [ - "\"Doe, John\" ;" - "\"John Smith\" ," + '"Doe, John" ;' + '"John Smith" ,' "admin@sub.example.com" ] expected_output = [ @@ -188,8 +186,8 @@ class EmailAddressParserTests(SimpleTestCase): # value is one long string not 3 sepearte values, prepare_addresses # should convert it to a list. value = ( - "\"Doe, John\" ;" - "\"John Smith\" ," + '"Doe, John" ;' + '"John Smith" ,' "admin@sub.example.com" ) expected_output = [ diff --git a/modoboa/lib/tests/test_u2u_decode.py b/modoboa/lib/tests/test_u2u_decode.py index 464aa4a24..c96d59d58 100644 --- a/modoboa/lib/tests/test_u2u_decode.py +++ b/modoboa/lib/tests/test_u2u_decode.py @@ -11,14 +11,20 @@ class U2UTestCase(TestCase): def test_header_decoding(self): """Simple decoding.""" samples = [ - ("=?ISO-8859-15?Q?=20Profitez de tous les services en ligne sur " - "impots.gouv.fr?=", - "Profitez de tous les services en ligne sur impots.gouv.fr"), - ("=?ISO-8859-1?Q?Accus=E9?= de =?ISO-8859-1?Q?r=E9ception?= de " - "votre annonce", - "Accusé de réception de votre annonce"), - ("Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord", - "Sm\xf6rg\xe5sbord"), + ( + "=?ISO-8859-15?Q?=20Profitez de tous les services en ligne sur " + "impots.gouv.fr?=", + "Profitez de tous les services en ligne sur impots.gouv.fr", + ), + ( + "=?ISO-8859-1?Q?Accus=E9?= de =?ISO-8859-1?Q?r=E9ception?= de " + "votre annonce", + "Accusé de réception de votre annonce", + ), + ( + "Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord", + "Sm\xf6rg\xe5sbord", + ), # The following case currently fails because of the way we split # encoded words to parse them separately, which can lead to # unexpected unicode decode errors... I think it will work fine on @@ -35,22 +41,22 @@ class U2UTestCase(TestCase): """Check address decoding.""" mailsploit_sample = ( "=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?==?utf-8?Q?=00?=" - "=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?=@mailsploit.com") + "=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?=@mailsploit.com" + ) expected_result = ( "=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?==?utf-8?Q??=" - "=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?=@mailsploit.com") + "=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?=@mailsploit.com" + ) self.assertEqual( - u2u_decode.decode_address(mailsploit_sample), - ("", expected_result) + u2u_decode.decode_address(mailsploit_sample), ("", expected_result) ) mailsploit_sample = ( '"=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?==?utf-8?Q?=0A=00?="\n' "<=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?==?utf-8?Q?=0A=00?=" - "@mailsploit.com>") + "@mailsploit.com>" + ) expected_result = ( "potus@whitehouse.gov", - "=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?==?utf-8?Q??=" - "@mailsploit.com") - self.assertEqual( - u2u_decode.decode_address(mailsploit_sample), - expected_result) + "=?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3Y=?==?utf-8?Q??=" "@mailsploit.com", + ) + self.assertEqual(u2u_decode.decode_address(mailsploit_sample), expected_result) diff --git a/modoboa/lib/throttle.py b/modoboa/lib/throttle.py index d972742be..46a830386 100644 --- a/modoboa/lib/throttle.py +++ b/modoboa/lib/throttle.py @@ -8,7 +8,7 @@ class UserDdosPerView(SimpleRateThrottle): applied on a per view basis for authentificated users. """ - scope = 'ddos' + scope = "ddos" def get_cache_key(self, request, view): if request.user and request.user.is_authenticated: @@ -16,8 +16,8 @@ class UserDdosPerView(SimpleRateThrottle): else: ident = self.get_ident(request) return self.cache_format % { - 'scope': hash(resolve(request.path).url_name), - 'ident': ident + "scope": hash(resolve(request.path).url_name), + "ident": ident, } @@ -27,18 +27,18 @@ class UserLesserDdosUser(UserDdosPerView): applied on a per view basis for authentificated users. """ - scope = 'ddos_lesser' + scope = "ddos_lesser" class LoginThrottle(SimpleRateThrottle): """Custom throttle to reset the cache counter on success.""" - scope = 'login' + scope = "login" def get_cache_key(self, request, view): return self.cache_format % { - 'scope': self.scope, - 'ident': self.get_ident(request) + "scope": self.scope, + "ident": self.get_ident(request), } def reset_cache(self, request): @@ -48,17 +48,17 @@ class LoginThrottle(SimpleRateThrottle): class PasswordResetRequestThrottle(LoginThrottle): - scope = 'password_recovery_request' + scope = "password_recovery_request" class PasswordResetTotpThrottle(LoginThrottle): - scope = 'password_recovery_totp_check' + scope = "password_recovery_totp_check" class PasswordResetApplyThrottle(LoginThrottle): - scope = 'password_recovery_apply' + scope = "password_recovery_apply" class GetThrottleViewsetMixin: @@ -72,8 +72,14 @@ class GetThrottleViewsetMixin: throttles = [UserRateThrottle()] actions = [ - "list", "retrieve", "validate", "dns_detail", "me", - "dns_detail", "applications", "structure" + "list", + "retrieve", + "validate", + "dns_detail", + "me", + "dns_detail", + "applications", + "structure", ] if self.action in actions: throttles.append(UserLesserDdosUser()) diff --git a/modoboa/lib/validators.py b/modoboa/lib/validators.py index 645e18dfa..e54c48ca5 100644 --- a/modoboa/lib/validators.py +++ b/modoboa/lib/validators.py @@ -8,7 +8,6 @@ from django.utils.translation import gettext_lazy class HostnameValidator(object): - """Validator for fqdn.""" message = gettext_lazy("Enter a valid domain name") @@ -37,7 +36,6 @@ validate_hostname = HostnameValidator() class UTF8EmailValidator(EmailValidator): - """Validator for addresses using non-ASCII characters.""" # unicode letters range (must be a unicode string, not a raw string) @@ -57,11 +55,9 @@ validate_utf8_email = UTF8EmailValidator() class UTF8AndEmptyUserEmailValidator(UTF8EmailValidator): - """Same as upper + allows empty local part.""" - user_regex = re.compile( - r"(^$|" + UTF8EmailValidator.user_regex_raw, re.IGNORECASE) + user_regex = re.compile(r"(^$|" + UTF8EmailValidator.user_regex_raw, re.IGNORECASE) validate_utf8_and_empty_user_email = UTF8AndEmptyUserEmailValidator() diff --git a/modoboa/lib/web_utils.py b/modoboa/lib/web_utils.py index 443da7f81..ae6333e79 100644 --- a/modoboa/lib/web_utils.py +++ b/modoboa/lib/web_utils.py @@ -16,15 +16,15 @@ from django.template.loader import render_to_string def _render_error(request, errortpl="error", user_context=None): if user_context is None: user_context = {} - return render( - request, "common/%s.html" % errortpl, user_context - ) + return render(request, "common/%s.html" % errortpl, user_context) def render_actions(actions): - t = template.Template("""{% load lib_tags %} + t = template.Template( + """{% load lib_tags %} {% for a in actions %}{% render_link a %}{% endfor %} -""") +""" + ) return t.render(template.Context({"actions": actions})) @@ -39,9 +39,16 @@ def getctx(status, level=1, callback=None, **kwargs): return ctx -def ajax_response(request, status="ok", respmsg=None, - url=None, ajaxnav=False, norefresh=False, - template=None, **kwargs): +def ajax_response( + request, + status="ok", + respmsg=None, + url=None, + ajaxnav=False, + norefresh=False, + template=None, + **kwargs, +): """Ajax response shortcut Simple shortcut that sends an JSON response. If a template is @@ -118,18 +125,18 @@ def size2integer(value, output_unit="B"): return 0 if output_unit == "B": if m.group(2)[0] in ["K", "k"]: - return int(m.group(1)) * 2 ** 10 + return int(m.group(1)) * 2**10 if m.group(2)[0] in ["M", "m"]: - return int(m.group(1)) * 2 ** 20 + return int(m.group(1)) * 2**20 if m.group(2)[0] in ["G", "g"]: - return int(m.group(1)) * 2 ** 30 + return int(m.group(1)) * 2**30 elif output_unit == "MB": if m.group(2)[0] in ["K", "k"]: - return int(int(m.group(1)) / 2 ** 10) + return int(int(m.group(1)) / 2**10) if m.group(2)[0] in ["M", "m"]: return int(m.group(1)) if m.group(2)[0] in ["G", "g"]: - return int(m.group(1)) * 2 ** 10 + return int(m.group(1)) * 2**10 else: raise ValueError("Unsupported output unit {}".format(output_unit)) return 0 @@ -143,8 +150,7 @@ class NavigationParameters(object): def __init__(self, request, sessionkey): self.request = request self.sessionkey = sessionkey - self.parameters = [("pattern", "", True), - ("criteria", "from_addr", False)] + self.parameters = [("pattern", "", True), ("criteria", "from_addr", False)] def __getitem__(self, key): """Retrieve an item.""" @@ -167,8 +173,7 @@ class NavigationParameters(object): self["page"] = int(self.request.GET.get("page", 1)) def store(self): - """Store navigation parameters into session. - """ + """Store navigation parameters into session.""" if self.sessionkey not in self.request.session: self.request.session[self.sessionkey] = {} self._store_page() diff --git a/modoboa/limits/api/v1/tests.py b/modoboa/limits/api/v1/tests.py index 5d3db87c8..8b6c0bcfe 100644 --- a/modoboa/limits/api/v1/tests.py +++ b/modoboa/limits/api/v1/tests.py @@ -22,21 +22,20 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): """Create test data.""" super(APIAdminLimitsTestCase, cls).setUpTestData() for name, _definition in utils.get_user_limit_templates(): - cls.localconfig.parameters.set_value( - "deflt_user_{0}_limit".format(name), 2) + cls.localconfig.parameters.set_value("deflt_user_{0}_limit".format(name), 2) cls.localconfig.save() populate_database() cls.user = User.objects.get(username="admin@test.com") cls.da_token = Token.objects.create(user=cls.user) cls.reseller = core_factories.UserFactory( - username="reseller", groups=("Resellers", ), + username="reseller", + groups=("Resellers",), ) cls.r_token = Token.objects.create(user=cls.reseller) def test_domadmins_limit(self): """Check domain admins limit.""" - self.client.credentials( - HTTP_AUTHORIZATION="Token " + self.r_token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.r_token.key) limit = self.reseller.userobjectlimit_set.get(name="domain_admins") url = reverse("v1:account-list") @@ -66,18 +65,14 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): "username": user.username, "role": "DomainAdmins", "password": "Toto1234", - "mailbox": { - "full_address": user.username, - "quota": user.mailbox.quota - } + "mailbox": {"full_address": user.username, "quota": user.mailbox.quota}, } response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, 400) def test_domains_limit(self): """Check domains limit.""" - self.client.credentials( - HTTP_AUTHORIZATION="Token " + self.r_token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.r_token.key) limit = self.reseller.userobjectlimit_set.get(name="domains") quota = self.reseller.userobjectlimit_set.get(name="quota") quota.max_value = 3 @@ -98,25 +93,23 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): data["name"] = "test5.com" response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 403) - self.assertEqual( - response.content.decode("utf-8"), '"Domains: limit reached"' - ) + self.assertEqual(response.content.decode("utf-8"), '"Domains: limit reached"') resp = self.client.delete( - reverse("v1:domain-detail", - args=[Domain.objects.get(name="test4.com").pk])) + reverse("v1:domain-detail", args=[Domain.objects.get(name="test4.com").pk]) + ) self.assertEqual(resp.status_code, 204) data["quota"] = 0 response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 400) self.assertEqual( response.content.decode(), - '"You\'re not allowed to define unlimited values"') + '"You\'re not allowed to define unlimited values"', + ) def test_domain_aliases_limit(self): """Check domain aliases limit.""" - self.client.credentials( - HTTP_AUTHORIZATION="Token " + self.r_token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.r_token.key) domain = Domain.objects.get(name="test.com") domain.add_admin(self.reseller) limit = self.reseller.userobjectlimit_set.get(name="domain_aliases") @@ -137,8 +130,7 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): def test_mailboxes_limit(self): """Check mailboxes limit.""" - self.client.credentials( - HTTP_AUTHORIZATION="Token " + self.da_token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.da_token.key) limit = self.user.userobjectlimit_set.get(name="mailboxes") url = reverse("v1:account-list") @@ -146,10 +138,7 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): "username": "fromapi@test.com", "role": "SimpleUsers", "password": "Toto1234", - "mailbox": { - "full_address": "fromapi@test.com", - "quota": 10 - } + "mailbox": {"full_address": "fromapi@test.com", "quota": 10}, } response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 201) @@ -168,16 +157,17 @@ class APIAdminLimitsTestCase(lib_tests.ModoAPITestCase): def test_aliases_limit(self): """Check mailbox aliases limit.""" - self.client.credentials( - HTTP_AUTHORIZATION="Token " + self.da_token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.da_token.key) limit = self.user.userobjectlimit_set.get(name="mailbox_aliases") url = reverse("v1:alias-list") data = { "address": "alias_fromapi@test.com", "recipients": [ - "user@test.com", "postmaster@test.com", "user_éé@nonlocal.com" - ] + "user@test.com", + "postmaster@test.com", + "user_éé@nonlocal.com", + ], } response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 201) @@ -200,11 +190,11 @@ class APIDomainLimitsTestCase(lib_tests.ModoAPITestCase): def setUpTestData(cls): # NOQA:N802 """Create test data.""" super(APIDomainLimitsTestCase, cls).setUpTestData() - cls.localconfig.parameters.set_value( - "enable_domain_limits", True) + cls.localconfig.parameters.set_value("enable_domain_limits", True) for name, _definition in utils.get_domain_limit_templates(): cls.localconfig.parameters.set_value( - "deflt_domain_{0}_limit".format(name), 2) + "deflt_domain_{0}_limit".format(name), 2 + ) cls.localconfig.save() populate_database() @@ -218,10 +208,7 @@ class APIDomainLimitsTestCase(lib_tests.ModoAPITestCase): "username": "fromapi@test.com", "role": "SimpleUsers", "password": "Toto1234", - "mailbox": { - "full_address": "fromapi@test.com", - "quota": 10 - } + "mailbox": {"full_address": "fromapi@test.com", "quota": 10}, } response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 400) @@ -251,8 +238,10 @@ class APIDomainLimitsTestCase(lib_tests.ModoAPITestCase): data = { "address": "alias_fromapi@test.com", "recipients": [ - "user@test.com", "postmaster@test.com", "user_éé@nonlocal.com" - ] + "user@test.com", + "postmaster@test.com", + "user_éé@nonlocal.com", + ], } response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 400) @@ -266,14 +255,14 @@ class ResourcesAPITestCase(lib_tests.ModoAPITestCase): """Create test data.""" super(ResourcesAPITestCase, cls).setUpTestData() for name, _definition in utils.get_user_limit_templates(): - cls.localconfig.parameters.set_value( - "deflt_user_{0}_limit".format(name), 2) + cls.localconfig.parameters.set_value("deflt_user_{0}_limit".format(name), 2) cls.localconfig.save() populate_database() cls.user = User.objects.get(username="admin@test.com") cls.da_token = Token.objects.create(user=cls.user) cls.reseller = core_factories.UserFactory( - username="reseller", groups=("Resellers", ), + username="reseller", + groups=("Resellers",), ) cls.r_token = Token.objects.create(user=cls.reseller) @@ -288,19 +277,17 @@ class ResourcesAPITestCase(lib_tests.ModoAPITestCase): "domain_admins": 2, "domain_aliases": 2, "domains": 2, - "mailbox_aliases": 2 + "mailbox_aliases": 2, } compare(expected, response.data) # As reseller => fails - self.client.credentials( - HTTP_AUTHORIZATION="Token {}".format(self.r_token.key)) + self.client.credentials(HTTP_AUTHORIZATION="Token {}".format(self.r_token.key)) response = self.client.get(url) self.assertEqual(response.status_code, 404) # As domain admin => fails - self.client.credentials( - HTTP_AUTHORIZATION="Token {}".format(self.da_token.key)) + self.client.credentials(HTTP_AUTHORIZATION="Token {}".format(self.da_token.key)) response = self.client.get(url) self.assertEqual(response.status_code, 404) @@ -313,12 +300,11 @@ class ResourcesAPITestCase(lib_tests.ModoAPITestCase): response = self.client.put(url, resources) self.assertEqual(response.status_code, 200) self.assertEqual( - self.reseller.userobjectlimit_set.get(name="domains").max_value, - 1000) + self.reseller.userobjectlimit_set.get(name="domains").max_value, 1000 + ) # As domain admin => fails - self.client.credentials( - HTTP_AUTHORIZATION="Token {}".format(self.da_token.key)) + self.client.credentials(HTTP_AUTHORIZATION="Token {}".format(self.da_token.key)) resources.update({"domains": 2, "mailboxes": 2}) url = reverse("v1:resources-detail", args=[self.user.pk]) response = self.client.put(url, resources) @@ -326,18 +312,17 @@ class ResourcesAPITestCase(lib_tests.ModoAPITestCase): # As reseller => ok permissions.grant_access_to_object(self.reseller, self.user, True) - self.client.credentials( - HTTP_AUTHORIZATION="Token {}".format(self.r_token.key)) + self.client.credentials(HTTP_AUTHORIZATION="Token {}".format(self.r_token.key)) resources.update({"domains": 500, "mailboxes": 500}) url = reverse("v1:resources-detail", args=[self.user.pk]) response = self.client.put(url, resources) self.assertEqual(response.status_code, 200) self.assertEqual( - self.user.userobjectlimit_set.get(name="domains").max_value, - 500) + self.user.userobjectlimit_set.get(name="domains").max_value, 500 + ) self.assertEqual( - self.reseller.userobjectlimit_set.get(name="domains").max_value, - 502) + self.reseller.userobjectlimit_set.get(name="domains").max_value, 502 + ) resources.update({"domains": 1003}) response = self.client.put(url, resources) self.assertEqual(response.status_code, 424) diff --git a/modoboa/limits/api/v1/urls.py b/modoboa/limits/api/v1/urls.py index 621a83286..637450c46 100644 --- a/modoboa/limits/api/v1/urls.py +++ b/modoboa/limits/api/v1/urls.py @@ -5,7 +5,6 @@ from rest_framework import routers from . import viewsets router = routers.SimpleRouter() -router.register( - r"resources", viewsets.ResourcesViewSet, basename="resources") +router.register(r"resources", viewsets.ResourcesViewSet, basename="resources") urlpatterns = router.urls diff --git a/modoboa/limits/api/v1/viewsets.py b/modoboa/limits/api/v1/viewsets.py index 006cb7454..7f4545977 100644 --- a/modoboa/limits/api/v1/viewsets.py +++ b/modoboa/limits/api/v1/viewsets.py @@ -11,10 +11,11 @@ from . import serializers class ResourcesViewSet( - GetThrottleViewsetMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - viewsets.GenericViewSet): + GetThrottleViewsetMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + viewsets.GenericViewSet, +): """Resources viewset.""" permission_classes = [IsAuthenticated, DjangoModelPermissions] @@ -25,8 +26,9 @@ class ResourcesViewSet( user = self.request.user ids = list( user.objectaccess_set.filter( - content_type=ContentType.objects.get_for_model(user)) - .values_list("object_id", flat=True)) + content_type=ContentType.objects.get_for_model(user) + ).values_list("object_id", flat=True) + ) if not user.is_superuser and user.pk in ids: ids.remove(user.pk) return core_models.User.objects.filter(pk__in=ids) diff --git a/modoboa/limits/app_settings.py b/modoboa/limits/app_settings.py index 001a0ccf9..98ea522ae 100644 --- a/modoboa/limits/app_settings.py +++ b/modoboa/limits/app_settings.py @@ -19,7 +19,7 @@ class ParametersForm(param_forms.AdminParametersForm): enable_admin_limits = YesNoField( label=_("Enable per-admin limits"), initial=True, - help_text=_("Enable or disable per-admin limits") + help_text=_("Enable or disable per-admin limits"), ) deflt_user_domain_admins_limit = forms.IntegerField( @@ -30,8 +30,7 @@ class ParametersForm(param_forms.AdminParametersForm): "administrator. (0 to deny any creation, -1 to allow unlimited " "creations)" ), - widget=forms.widgets.TextInput( - attrs={"class": "col-md-1 form-control"}) + widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"}), ) deflt_user_domains_limit = forms.IntegerField( label=_("Domains"), @@ -40,8 +39,7 @@ class ParametersForm(param_forms.AdminParametersForm): "Maximum number of allowed domains for a new administrator. " "(0 to deny any creation, -1 to allow unlimited creations)" ), - widget=forms.widgets.TextInput( - attrs={"class": "col-md-1 form-control"}) + widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"}), ) deflt_user_domain_aliases_limit = forms.IntegerField( label=_("Domain aliases"), @@ -51,8 +49,7 @@ class ParametersForm(param_forms.AdminParametersForm): "administrator. (0 to deny any creation, -1 to allow " "unlimited creations)" ), - widget=forms.widgets.TextInput( - attrs={"class": "col-md-1 form-control"}) + widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"}), ) deflt_user_mailboxes_limit = forms.IntegerField( label=_("Mailboxes"), @@ -61,8 +58,7 @@ class ParametersForm(param_forms.AdminParametersForm): "Maximum number of allowed mailboxes for a new administrator. " "(0 to deny any creation, -1 to allow unlimited creations)" ), - widget=forms.widgets.TextInput( - attrs={"class": "col-md-1 form-control"}) + widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"}), ) deflt_user_mailbox_aliases_limit = forms.IntegerField( label=_("Mailbox aliases"), @@ -71,8 +67,7 @@ class ParametersForm(param_forms.AdminParametersForm): "Maximum number of allowed aliases for a new administrator. " "(0 to deny any creation, -1 to allow unlimited creations)" ), - widget=forms.widgets.TextInput( - attrs={"class": "col-md-1 form-control"}) + widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"}), ) deflt_user_quota_limit = forms.IntegerField( label=_("Quota"), @@ -81,8 +76,7 @@ class ParametersForm(param_forms.AdminParametersForm): "The quota a reseller will be allowed to share between the " "domains he creates. (0 means no quota)" ), - widget=forms.widgets.TextInput( - attrs={"class": "col-md-1 form-control"}) + widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"}), ) domain_limits_sep = SeparatorField(label=_("Default per-domain limits")) @@ -90,7 +84,7 @@ class ParametersForm(param_forms.AdminParametersForm): enable_domain_limits = YesNoField( label=_("Enable per-domain limits"), initial=False, - help_text=_("Enable or disable per-domain limits") + help_text=_("Enable or disable per-domain limits"), ) deflt_domain_domain_admins_limit = forms.IntegerField( @@ -101,8 +95,7 @@ class ParametersForm(param_forms.AdminParametersForm): "domain. (0 to deny any creation, -1 to allow unlimited " "creations)" ), - widget=forms.widgets.TextInput( - attrs={"class": "col-md-1 form-control"}) + widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"}), ) deflt_domain_domain_aliases_limit = forms.IntegerField( label=_("Domain aliases"), @@ -112,8 +105,7 @@ class ParametersForm(param_forms.AdminParametersForm): "domain. (0 to deny any creation, -1 to allow " "unlimited creations)" ), - widget=forms.widgets.TextInput( - attrs={"class": "col-md-1 form-control"}) + widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"}), ) deflt_domain_mailboxes_limit = forms.IntegerField( label=_("Mailboxes"), @@ -122,8 +114,7 @@ class ParametersForm(param_forms.AdminParametersForm): "Maximum number of allowed mailboxes for a new domain. " "(0 to deny any creation, -1 to allow unlimited creations)" ), - widget=forms.widgets.TextInput( - attrs={"class": "col-md-1 form-control"}) + widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"}), ) deflt_domain_mailbox_aliases_limit = forms.IntegerField( label=_("Mailbox aliases"), @@ -132,8 +123,7 @@ class ParametersForm(param_forms.AdminParametersForm): "Maximum number of allowed aliases for a new domain. " "(0 to deny any creation, -1 to allow unlimited creations)" ), - widget=forms.widgets.TextInput( - attrs={"class": "col-md-1 form-control"}) + widget=forms.widgets.TextInput(attrs={"class": "col-md-1 form-control"}), ) visibility_rules = { @@ -149,107 +139,155 @@ class ParametersForm(param_forms.AdminParametersForm): } -GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([ - ("per_admin_limits", { - "label": _("Default per-admin limits"), - "params": collections.OrderedDict([ - ("enable_admin_limits", { - "label": _("Enable per-admin limits"), - "help_text": _("Enable or disable per-admin limits") - }), - ("deflt_user_domain_admins_limit", { - "label": _("Domain admins"), - "display": "enable_admin_limits=true", - "help_text": _( - "Maximum number of allowed domain administrators for a new " - "administrator. (0 to deny any creation, -1 to allow " - "unlimited creations)" +GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict( + [ + ( + "per_admin_limits", + { + "label": _("Default per-admin limits"), + "params": collections.OrderedDict( + [ + ( + "enable_admin_limits", + { + "label": _("Enable per-admin limits"), + "help_text": _("Enable or disable per-admin limits"), + }, + ), + ( + "deflt_user_domain_admins_limit", + { + "label": _("Domain admins"), + "display": "enable_admin_limits=true", + "help_text": _( + "Maximum number of allowed domain administrators for a new " + "administrator. (0 to deny any creation, -1 to allow " + "unlimited creations)" + ), + }, + ), + ( + "deflt_user_domains_limit", + { + "label": _("Domains"), + "display": "enable_admin_limits=true", + "help_text": _( + "Maximum number of allowed domains for a new administrator." + " (0 to deny any creation, -1 to allow unlimited creations)" + ), + }, + ), + ( + "deflt_user_domain_aliases_limit", + { + "label": _("Domain aliases"), + "display": "enable_admin_limits=true", + "help_text": _( + "Maximum number of allowed domain aliases for a new " + "administrator. (0 to deny any creation, -1 to allow " + "unlimited creations)" + ), + }, + ), + ( + "deflt_user_mailboxes_limit", + { + "label": _("Mailboxes"), + "display": "enable_admin_limits=true", + "help_text": _( + "Maximum number of allowed mailboxes for a new " + "administrator. (0 to deny any creation, -1 to allow " + "unlimited creations)" + ), + }, + ), + ( + "deflt_user_mailbox_aliases_limit", + { + "label": _("Mailbox aliases"), + "display": "enable_admin_limits=true", + "help_text": _( + "Maximum number of allowed aliases for a new administrator." + " (0 to deny any creation, -1 to allow unlimited creations)" + ), + }, + ), + ( + "deflt_user_quota_limit", + { + "label": _("Quota"), + "help_text": _( + "The quota a reseller will be allowed to share between the " + "domains he creates. (0 means no quota)" + ), + }, + ), + ] ), - }), - ("deflt_user_domains_limit", { - "label": _("Domains"), - "display": "enable_admin_limits=true", - "help_text": _( - "Maximum number of allowed domains for a new administrator." - " (0 to deny any creation, -1 to allow unlimited creations)" + }, + ), + ( + "per_domain_limits", + { + "label": _("Default per-domain limits"), + "params": collections.OrderedDict( + [ + ( + "enable_domain_limits", + { + "label": _("Enable per-domain limits"), + "help_text": _("Enable or disable per-domain limits"), + }, + ), + ( + "deflt_domain_domain_admins_limit", + { + "label": _("Domain admins"), + "display": "enable_domain_limits=true", + "help_text": _( + "Maximum number of allowed domain administrators for a new " + "domain. (0 to deny any creation, -1 to allow unlimited " + "creations)" + ), + }, + ), + ( + "deflt_domain_domain_aliases_limit", + { + "label": _("Domain aliases"), + "display": "enable_domain_limits=true", + "help_text": _( + "Maximum number of allowed domain aliases for a new " + "domain. (0 to deny any creation, -1 to allow " + "unlimited creations)" + ), + }, + ), + ( + "deflt_domain_mailboxes_limit", + { + "label": _("Mailboxes"), + "display": "enable_domain_limits=true", + "help_text": _( + "Maximum number of allowed mailboxes for a new domain. " + "(0 to deny any creation, -1 to allow unlimited creations)" + ), + }, + ), + ( + "deflt_domain_mailbox_aliases_limit", + { + "label": _("Mailbox aliases"), + "display": "enable_domain_limits=true", + "help_text": _( + "Maximum number of allowed aliases for a new domain. " + "(0 to deny any creation, -1 to allow unlimited creations)" + ), + }, + ), + ] ), - }), - ("deflt_user_domain_aliases_limit", { - "label": _("Domain aliases"), - "display": "enable_admin_limits=true", - "help_text": _( - "Maximum number of allowed domain aliases for a new " - "administrator. (0 to deny any creation, -1 to allow " - "unlimited creations)" - ), - }), - ("deflt_user_mailboxes_limit", { - "label": _("Mailboxes"), - "display": "enable_admin_limits=true", - "help_text": _( - "Maximum number of allowed mailboxes for a new " - "administrator. (0 to deny any creation, -1 to allow " - "unlimited creations)" - ), - }), - ("deflt_user_mailbox_aliases_limit", { - "label": _("Mailbox aliases"), - "display": "enable_admin_limits=true", - "help_text": _( - "Maximum number of allowed aliases for a new administrator." - " (0 to deny any creation, -1 to allow unlimited creations)" - ), - }), - ("deflt_user_quota_limit", { - "label": _("Quota"), - "help_text": _( - "The quota a reseller will be allowed to share between the " - "domains he creates. (0 means no quota)" - ), - }) - ]) - }), - ("per_domain_limits", { - "label": _("Default per-domain limits"), - "params": collections.OrderedDict([ - ("enable_domain_limits", { - "label": _("Enable per-domain limits"), - "help_text": _("Enable or disable per-domain limits") - }), - ("deflt_domain_domain_admins_limit", { - "label": _("Domain admins"), - "display": "enable_domain_limits=true", - "help_text": _( - "Maximum number of allowed domain administrators for a new " - "domain. (0 to deny any creation, -1 to allow unlimited " - "creations)" - ), - }), - ("deflt_domain_domain_aliases_limit", { - "label": _("Domain aliases"), - "display": "enable_domain_limits=true", - "help_text": _( - "Maximum number of allowed domain aliases for a new " - "domain. (0 to deny any creation, -1 to allow " - "unlimited creations)" - ), - }), - ("deflt_domain_mailboxes_limit", { - "label": _("Mailboxes"), - "display": "enable_domain_limits=true", - "help_text": _( - "Maximum number of allowed mailboxes for a new domain. " - "(0 to deny any creation, -1 to allow unlimited creations)" - ), - }), - ("deflt_domain_mailbox_aliases_limit", { - "label": _("Mailbox aliases"), - "display": "enable_domain_limits=true", - "help_text": _( - "Maximum number of allowed aliases for a new domain. " - "(0 to deny any creation, -1 to allow unlimited creations)" - ), - }) - ]) - }) -]) + }, + ), + ] +) diff --git a/modoboa/limits/apps.py b/modoboa/limits/apps.py index 2f204b0e3..6181a9ea9 100644 --- a/modoboa/limits/apps.py +++ b/modoboa/limits/apps.py @@ -12,8 +12,11 @@ def load_limits_settings(): param_tools.registry.add("global", app_settings.ParametersForm, _("Limits")) param_tools.registry.add2( - "global", "limits", _("Limits"), app_settings.GLOBAL_PARAMETERS_STRUCT, - serializers.LimitsGlobalParemetersSerializer + "global", + "limits", + _("Limits"), + app_settings.GLOBAL_PARAMETERS_STRUCT, + serializers.LimitsGlobalParemetersSerializer, ) diff --git a/modoboa/limits/constants.py b/modoboa/limits/constants.py index 970d9eef7..41de5c11b 100644 --- a/modoboa/limits/constants.py +++ b/modoboa/limits/constants.py @@ -4,54 +4,103 @@ import collections from django.utils.translation import gettext_lazy as _ -DEFAULT_USER_LIMITS = collections.OrderedDict(( - ("domains", { - "content_type": "admin.domain", "label": _("Domains"), - "help": _("Maximum number of domains this user can create"), - "required_role": "Resellers"}), - ("domain_aliases", { - "content_type": "admin.domainalias", "label": _("Domain aliases"), - "help": _("Maximum number of domain aliases this user can create"), - "required_role": "Resellers"}), - ("mailboxes", { - "content_type": "admin.mailbox", "label": _("Mailboxes"), - "help": _("Maximum number of mailboxes this user can create")}), - ("mailbox_aliases", { - "content_type": "admin.alias", "label": _("Mailbox aliases"), - "help": _("Maximum number of mailbox aliases this user " - "can create")}), - ("domain_admins", { - "content_type": "core.user", "label": _("Domain admins"), - "help": _("Maximum number of domain administrators this user " - "can create"), - "required_role": "Resellers", - "extra_filters": {"groups__name": "DomainAdmins"}}), - ("quota", { - "content_type": "admin.domain", "label": _("Quota"), - "help": _("Quota shared between domains of this reseller"), - "required_role": "Resellers", - "type": "sum", - "field": "quota" - }) -)) +DEFAULT_USER_LIMITS = collections.OrderedDict( + ( + ( + "domains", + { + "content_type": "admin.domain", + "label": _("Domains"), + "help": _("Maximum number of domains this user can create"), + "required_role": "Resellers", + }, + ), + ( + "domain_aliases", + { + "content_type": "admin.domainalias", + "label": _("Domain aliases"), + "help": _("Maximum number of domain aliases this user can create"), + "required_role": "Resellers", + }, + ), + ( + "mailboxes", + { + "content_type": "admin.mailbox", + "label": _("Mailboxes"), + "help": _("Maximum number of mailboxes this user can create"), + }, + ), + ( + "mailbox_aliases", + { + "content_type": "admin.alias", + "label": _("Mailbox aliases"), + "help": _("Maximum number of mailbox aliases this user " "can create"), + }, + ), + ( + "domain_admins", + { + "content_type": "core.user", + "label": _("Domain admins"), + "help": _( + "Maximum number of domain administrators this user " "can create" + ), + "required_role": "Resellers", + "extra_filters": {"groups__name": "DomainAdmins"}, + }, + ), + ( + "quota", + { + "content_type": "admin.domain", + "label": _("Quota"), + "help": _("Quota shared between domains of this reseller"), + "required_role": "Resellers", + "type": "sum", + "field": "quota", + }, + ), + ) +) -DEFAULT_DOMAIN_LIMITS = collections.OrderedDict(( - ("domain_aliases", { - "relation": "domainalias_set", "label": _("Domain aliases"), - "help": _("Maximum number of domain aliases allowed for this domain.") - }), - ("mailboxes", { - "relation": "mailbox_set", "label": _("Mailboxes"), - "help": _("Maximum number of mailboxes allowed for this domain.")}), - ("mailbox_aliases", { - "relation": "alias_set", "label": _("Mailbox aliases"), - "help": _( - "Maximum number of mailbox aliases allowed for this domain."), - "extra_filters": {"internal": False} - }), - ("domain_admins", { - "relation": "admins", "label": _("Domain admins"), - "help": _("Maximum number of domain admins allowed for this domain."), - }) -)) +DEFAULT_DOMAIN_LIMITS = collections.OrderedDict( + ( + ( + "domain_aliases", + { + "relation": "domainalias_set", + "label": _("Domain aliases"), + "help": _("Maximum number of domain aliases allowed for this domain."), + }, + ), + ( + "mailboxes", + { + "relation": "mailbox_set", + "label": _("Mailboxes"), + "help": _("Maximum number of mailboxes allowed for this domain."), + }, + ), + ( + "mailbox_aliases", + { + "relation": "alias_set", + "label": _("Mailbox aliases"), + "help": _("Maximum number of mailbox aliases allowed for this domain."), + "extra_filters": {"internal": False}, + }, + ), + ( + "domain_admins", + { + "relation": "admins", + "label": _("Domain admins"), + "help": _("Maximum number of domain admins allowed for this domain."), + }, + ), + ) +) diff --git a/modoboa/limits/forms.py b/modoboa/limits/forms.py index ddaf69f1d..6c226b842 100644 --- a/modoboa/limits/forms.py +++ b/modoboa/limits/forms.py @@ -17,8 +17,8 @@ class ResourcePoolForm(forms.Form): for name, tpl in utils.get_user_limit_templates(): if "required_role" in tpl: condition = ( - self.account is not None and - self.account.role != tpl["required_role"] + self.account is not None + and self.account.role != tpl["required_role"] ) if condition: continue @@ -42,8 +42,9 @@ class ResourcePoolForm(forms.Form): """Load limit values from given user.""" for fieldname in list(self.fields.keys()): lname = fieldname.replace("_limit", "") - self.fields[fieldname].initial = ( - user.userobjectlimit_set.get(name=lname).max_value) + self.fields[fieldname].initial = user.userobjectlimit_set.get( + name=lname + ).max_value def save(self): owner = get_object_owner(self.account) @@ -53,8 +54,7 @@ class ResourcePoolForm(forms.Form): continue limit = self.account.userobjectlimit_set.get(name=name) if not owner.is_superuser: - allocate_resources_from_user( - limit, owner, self.cleaned_data[fieldname]) + allocate_resources_from_user(limit, owner, self.cleaned_data[fieldname]) limit.max_value = self.cleaned_data[fieldname] limit.save(update_fields=["max_value"]) @@ -74,8 +74,9 @@ class DomainLimitsForm(forms.Form): return for fieldname in list(self.fields.keys()): lname = fieldname.replace("_limit", "") - self.fields[fieldname].initial = ( - self.domain.domainobjectlimit_set.get(name=lname).max_value) + self.fields[fieldname].initial = self.domain.domainobjectlimit_set.get( + name=lname + ).max_value def clean(self): """Ensure limit values are correct.""" diff --git a/modoboa/limits/handlers.py b/modoboa/limits/handlers.py index a2992db20..6b3bc325c 100644 --- a/modoboa/limits/handlers.py +++ b/modoboa/limits/handlers.py @@ -46,13 +46,15 @@ def create_user_limits(sender, instance, **kwargs): global_params = dict(param_tools.get_global_parameters("limits")) for name, definition in utils.get_user_limit_templates(): ct = ContentType.objects.get_by_natural_key( - *definition["content_type"].split(".")) + *definition["content_type"].split(".") + ) max_value = 0 # creator can be None if user was created by a factory if not creator or creator.is_superuser: max_value = global_params["deflt_user_{0}_limit".format(name)] models.UserObjectLimit.objects.create( - user=instance, name=name, content_type=ct, max_value=max_value) + user=instance, name=name, content_type=ct, max_value=max_value + ) @receiver(signals.post_save, sender=admin_models.Domain) @@ -64,7 +66,8 @@ def create_domain_limits(sender, instance, **kwargs): for name, _definition in utils.get_domain_limit_templates(): max_value = global_params["deflt_domain_{0}_limit".format(name)] models.DomainObjectLimit.objects.create( - domain=instance, name=name, max_value=max_value) + domain=instance, name=name, max_value=max_value + ) @receiver(admin_signals.extra_domain_dashboard_widgets) @@ -72,32 +75,32 @@ def display_domain_limits(sender, user, domain, **kwargs): """Display resources usage for domain.""" if not param_tools.get_global_parameter("enable_domain_limits"): return [] - return [{ - "column": "right", - "template": "limits/resources_widget.html", - "context": { - "limits": domain.domainobjectlimit_set.all() + return [ + { + "column": "right", + "template": "limits/resources_widget.html", + "context": {"limits": domain.domainobjectlimit_set.all()}, } - }] + ] @receiver(admin_signals.extra_account_dashboard_widgets) def display_admin_limits(sender, user, account, **kwargs): """Display resources usage for admin.""" - condition = ( - param_tools.get_global_parameter("enable_admin_limits") and - account.role in ["DomainAdmins", "Resellers"] - ) + condition = param_tools.get_global_parameter( + "enable_admin_limits" + ) and account.role in ["DomainAdmins", "Resellers"] if not condition: return [] - return [{ - "column": "right", - "template": "limits/resources_widget.html", - "context": { - "limits": ( - account.userobjectlimit_set.select_related("content_type")) + return [ + { + "column": "right", + "template": "limits/resources_widget.html", + "context": { + "limits": (account.userobjectlimit_set.select_related("content_type")) + }, } - }] + ] @receiver(admin_signals.extra_account_forms) @@ -108,16 +111,11 @@ def extra_account_form(sender, user, account=None, **kwargs): if user.role not in ["SuperAdmins", "Resellers"]: return [] condition = ( - (account is not None and - account.role not in ["Resellers", "DomainAdmins"]) or - user == account - ) + account is not None and account.role not in ["Resellers", "DomainAdmins"] + ) or user == account if condition: return [] - return [{ - "id": "resources", "title": _("Resources"), - "cls": forms.ResourcePoolForm - }] + return [{"id": "resources", "title": _("Resources"), "cls": forms.ResourcePoolForm}] @receiver(admin_signals.check_extra_account_form) @@ -134,9 +132,9 @@ def check_form_access(sender, account, form, **kwargs): def fill_account_instances(sender, user, account, **kwargs): """Set account instance for resources form.""" condition = ( - not param_tools.get_global_parameter("enable_admin_limits") or - (not user.is_superuser and user.role != "Resellers") or - account.role not in ["Resellers", "DomainAdmins"] + not param_tools.get_global_parameter("enable_admin_limits") + or (not user.is_superuser and user.role != "Resellers") + or account.role not in ["Resellers", "DomainAdmins"] ) if condition: return {} @@ -150,19 +148,15 @@ def extra_domain_form(sender, user, domain, **kwargs): return [] if not user.has_perm("admin.change_domain"): return [] - return [{ - "id": "resources", "title": _("Resources"), - "cls": forms.DomainLimitsForm - }] + return [{"id": "resources", "title": _("Resources"), "cls": forms.DomainLimitsForm}] @receiver(admin_signals.get_domain_form_instances) def fill_domain_instances(sender, user, domain, **kwargs): """Set domain instance for resources form.""" - condition = ( - not param_tools.get_global_parameter("enable_domain_limits") or - not user.has_perm("admin.change_domain") - ) + condition = not param_tools.get_global_parameter( + "enable_domain_limits" + ) or not user.has_perm("admin.change_domain") if condition: return {} return {"resources": domain} @@ -190,14 +184,14 @@ def user_can_set_role(sender, user, role, account=None, **kwargs): :param ``User`` account: account modified (None on creation) """ condition = ( - not param_tools.get_global_parameter("enable_admin_limits") or - role != "DomainAdmins") + not param_tools.get_global_parameter("enable_admin_limits") + or role != "DomainAdmins" + ) if condition: return True lname = "domain_admins" condition = ( - user.is_superuser or - not user.userobjectlimit_set.get(name=lname).is_exceeded() + user.is_superuser or not user.userobjectlimit_set.get(name=lname).is_exceeded() ) if condition: return True @@ -210,9 +204,9 @@ def user_can_set_role(sender, user, role, account=None, **kwargs): def get_static_content(sender, caller, st_type, user, **kwargs): """Add extra static content.""" condition = ( - not param_tools.get_global_parameter("enable_admin_limits") or - caller not in ["domains", "identities"] or - user.role in ["SuperAdmins", "SimpleUsers"] + not param_tools.get_global_parameter("enable_admin_limits") + or caller not in ["domains", "identities"] + or user.role in ["SuperAdmins", "SimpleUsers"] ) if condition: return "" @@ -244,8 +238,10 @@ $(document).ready(function() { def display_pool_usage(sender, user, location, currentpage, **kwargs): """Display current usage.""" condition = ( - not param_tools.get_global_parameter("enable_admin_limits") or - location != "leftcol" or user.is_superuser) + not param_tools.get_global_parameter("enable_admin_limits") + or location != "leftcol" + or user.is_superuser + ) if condition: return [] if currentpage == "identities": @@ -255,19 +251,16 @@ def display_pool_usage(sender, user, location, currentpage, **kwargs): else: exceptions = ["domain_admins", "mailboxes", "mailbox_aliases"] names = [ - name for name, tpl in utils.get_user_limit_templates() - if name not in exceptions and - ("required_role" not in tpl or - tpl["required_role"] == user.role) + name + for name, tpl in utils.get_user_limit_templates() + if name not in exceptions + and ("required_role" not in tpl or tpl["required_role"] == user.role) ] limits = user.userobjectlimit_set.filter(name__in=names, max_value__gt=0) if len(limits) == 0: return [] - return [ - render_to_string("limits/poolusage.html", - {"limits": limits}) - ] + return [render_to_string("limits/poolusage.html", {"limits": limits})] @receiver(core_signals.account_role_changed) diff --git a/modoboa/limits/lib.py b/modoboa/limits/lib.py index f186bc07f..22663c1ea 100644 --- a/modoboa/limits/lib.py +++ b/modoboa/limits/lib.py @@ -43,9 +43,7 @@ def allocate_resources_from_user(limit, user, value): """ ol = user.userobjectlimit_set.get(name=limit.name) if value == -1 and ol.max_value != -1: - raise BadLimitValue( - _("You're not allowed to define unlimited values") - ) + raise BadLimitValue(_("You're not allowed to define unlimited values")) if limit.max_value > -1: value -= limit.max_value diff --git a/modoboa/limits/migrations/0001_initial.py b/modoboa/limits/migrations/0001_initial.py index c40f69b92..986bbdea6 100644 --- a/modoboa/limits/migrations/0001_initial.py +++ b/modoboa/limits/migrations/0001_initial.py @@ -10,37 +10,58 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Limit', + name="Limit", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=255)), - ('curvalue', models.IntegerField(default=0)), - ('maxvalue', models.IntegerField(default=-2)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=255)), + ("curvalue", models.IntegerField(default=0)), + ("maxvalue", models.IntegerField(default=-2)), ], options={ - 'db_table': 'limits_limit', + "db_table": "limits_limit", }, bases=(models.Model,), ), migrations.CreateModel( - name='LimitsPool', + name="LimitsPool", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "user", + models.OneToOneField( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), + ), ], options={ - 'db_table': 'limits_limitspool', + "db_table": "limits_limitspool", }, bases=(models.Model,), ), migrations.AddField( - model_name='limit', - name='pool', - field=models.ForeignKey(to='limits.LimitsPool', on_delete=models.CASCADE), + model_name="limit", + name="pool", + field=models.ForeignKey(to="limits.LimitsPool", on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterUniqueTogether( - name='limit', - unique_together=set([('name', 'pool')]), + name="limit", + unique_together=set([("name", "pool")]), ), ] diff --git a/modoboa/limits/migrations/0001_squashed_0004_auto_20160413_1312.py b/modoboa/limits/migrations/0001_squashed_0004_auto_20160413_1312.py index 0e7692443..78a2f789d 100644 --- a/modoboa/limits/migrations/0001_squashed_0004_auto_20160413_1312.py +++ b/modoboa/limits/migrations/0001_squashed_0004_auto_20160413_1312.py @@ -10,47 +10,78 @@ import modoboa.limits.models class Migration(migrations.Migration): replaces = [ - ('limits', '0001_initial'), - ('limits', '0002_auto_20151114_1518'), - ('limits', '0003_auto_20160413_1046'), - ('limits', '0004_auto_20160413_1312'), + ("limits", "0001_initial"), + ("limits", "0002_auto_20151114_1518"), + ("limits", "0003_auto_20160413_1046"), + ("limits", "0004_auto_20160413_1312"), ] initial = True dependencies = [ - ('admin', '0001_initial'), - ('contenttypes', '0002_remove_content_type_name'), + ("admin", "0001_initial"), + ("contenttypes", "0002_remove_content_type_name"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='UserObjectLimit', + name="UserObjectLimit", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=254)), - ('max_value', models.IntegerField(default=0)), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=254)), + ("max_value", models.IntegerField(default=0)), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="contenttypes.ContentType", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.AlterUniqueTogether( - name='userobjectlimit', - unique_together=set([('user', 'name')]), + name="userobjectlimit", + unique_together=set([("user", "name")]), ), migrations.CreateModel( - name='DomainObjectLimit', + name="DomainObjectLimit", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=254)), - ('max_value', models.IntegerField(default=0)), - ('domain', models.ForeignKey(to='admin.Domain', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=254)), + ("max_value", models.IntegerField(default=0)), + ( + "domain", + models.ForeignKey(to="admin.Domain", on_delete=models.CASCADE), + ), ], bases=(modoboa.limits.models.ObjectLimitMixin, models.Model), ), migrations.AlterUniqueTogether( - name='domainobjectlimit', - unique_together=set([('domain', 'name')]), + name="domainobjectlimit", + unique_together=set([("domain", "name")]), ), ] diff --git a/modoboa/limits/migrations/0002_auto_20151114_1518.py b/modoboa/limits/migrations/0002_auto_20151114_1518.py index 7982519e0..f54808265 100644 --- a/modoboa/limits/migrations/0002_auto_20151114_1518.py +++ b/modoboa/limits/migrations/0002_auto_20151114_1518.py @@ -4,16 +4,16 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('limits', '0001_initial'), + ("limits", "0001_initial"), ] operations = [ migrations.AlterModelTable( - name='limit', + name="limit", table=None, ), migrations.AlterModelTable( - name='limitspool', + name="limitspool", table=None, ), ] diff --git a/modoboa/limits/migrations/0003_auto_20160413_1046.py b/modoboa/limits/migrations/0003_auto_20160413_1046.py index 941802bb6..f33519bfc 100644 --- a/modoboa/limits/migrations/0003_auto_20160413_1046.py +++ b/modoboa/limits/migrations/0003_auto_20160413_1046.py @@ -13,28 +13,31 @@ def move_limits_to_user(apps, schema_editor): return name_to_content_type = { - "domains_limit": ( - ContentType.objects.get(app_label="admin", model="domain")), + "domains_limit": (ContentType.objects.get(app_label="admin", model="domain")), "domain_aliases_limit": ( - ContentType.objects.get(app_label="admin", model="domainalias")), + ContentType.objects.get(app_label="admin", model="domainalias") + ), "mailboxes_limit": ( - ContentType.objects.get(app_label="admin", model="mailbox")), + ContentType.objects.get(app_label="admin", model="mailbox") + ), "mailbox_aliases_limit": ( - ContentType.objects.get(app_label="admin", model="alias")), + ContentType.objects.get(app_label="admin", model="alias") + ), "domain_admins_limit": ( - ContentType.objects.get(app_label="core", model="user")), + ContentType.objects.get(app_label="core", model="user") + ), } to_create = [] - qset = ( - LimitsPool.objects.select_related("user") - .prefetch_related("limit_set")) + qset = LimitsPool.objects.select_related("user").prefetch_related("limit_set") for pool in qset: for limit in pool.limit_set.all(): objlimit = UserObjectLimit( - user=pool.user, max_value=limit.maxvalue, + user=pool.user, + max_value=limit.maxvalue, name=limit.name.replace("_limit", ""), - content_type=name_to_content_type[limit.name]) + content_type=name_to_content_type[limit.name], + ) to_create.append(objlimit) UserObjectLimit.objects.bulk_create(to_create) @@ -42,26 +45,44 @@ def move_limits_to_user(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), + ("contenttypes", "0002_remove_content_type_name"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('limits', '0002_auto_20151114_1518'), - ('admin', '0001_initial'), + ("limits", "0002_auto_20151114_1518"), + ("admin", "0001_initial"), ] operations = [ migrations.CreateModel( - name='UserObjectLimit', + name="UserObjectLimit", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=254)), - ('max_value', models.IntegerField(default=-2)), - ('content_type', models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE)), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=254)), + ("max_value", models.IntegerField(default=-2)), + ( + "content_type", + models.ForeignKey( + to="contenttypes.ContentType", on_delete=models.CASCADE + ), + ), + ( + "user", + models.ForeignKey( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), + ), ], ), migrations.AlterUniqueTogether( - name='userobjectlimit', - unique_together=set([('user', 'name')]), + name="userobjectlimit", + unique_together=set([("user", "name")]), ), - migrations.RunPython(move_limits_to_user) + migrations.RunPython(move_limits_to_user), ] diff --git a/modoboa/limits/migrations/0004_auto_20160413_1312.py b/modoboa/limits/migrations/0004_auto_20160413_1312.py index 1ec5502f7..cc5468074 100644 --- a/modoboa/limits/migrations/0004_auto_20160413_1312.py +++ b/modoboa/limits/migrations/0004_auto_20160413_1312.py @@ -4,26 +4,26 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('limits', '0003_auto_20160413_1046'), + ("limits", "0003_auto_20160413_1046"), ] operations = [ migrations.AlterUniqueTogether( - name='limit', + name="limit", unique_together=set([]), ), migrations.RemoveField( - model_name='limit', - name='pool', + model_name="limit", + name="pool", ), migrations.RemoveField( - model_name='limitspool', - name='user', + model_name="limitspool", + name="user", ), migrations.DeleteModel( - name='Limit', + name="Limit", ), migrations.DeleteModel( - name='LimitsPool', + name="LimitsPool", ), ] diff --git a/modoboa/limits/migrations/0005_auto_20160415_1654.py b/modoboa/limits/migrations/0005_auto_20160415_1654.py index 563b84e4f..b16f91d1d 100644 --- a/modoboa/limits/migrations/0005_auto_20160415_1654.py +++ b/modoboa/limits/migrations/0005_auto_20160415_1654.py @@ -10,18 +10,15 @@ def create_limits(apps, schema_editor): to_create = [] for domain in Domain.objects.all(): for name, tpl in utils.get_domain_limit_templates(): - to_create.append( - DomainObjectLimit(domain=domain, name=name, max_value=-1)) + to_create.append(DomainObjectLimit(domain=domain, name=name, max_value=-1)) DomainObjectLimit.objects.bulk_create(to_create) class Migration(migrations.Migration): dependencies = [ - ('admin', '0003_auto_20151118_1215'), - ('limits', '0004_auto_20160413_1312'), + ("admin", "0003_auto_20151118_1215"), + ("limits", "0004_auto_20160413_1312"), ] - operations = [ - migrations.RunPython(create_limits) - ] + operations = [migrations.RunPython(create_limits)] diff --git a/modoboa/limits/migrations/0006_auto_20170216_1112.py b/modoboa/limits/migrations/0006_auto_20170216_1112.py index a2ac72b32..d6314a98a 100644 --- a/modoboa/limits/migrations/0006_auto_20170216_1112.py +++ b/modoboa/limits/migrations/0006_auto_20170216_1112.py @@ -21,9 +21,7 @@ def create_quota_limits(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('limits', '0005_auto_20160415_1654'), + ("limits", "0005_auto_20160415_1654"), ] - operations = [ - migrations.RunPython(create_quota_limits) - ] + operations = [migrations.RunPython(create_quota_limits)] diff --git a/modoboa/limits/models.py b/modoboa/limits/models.py index 2f12d8285..e0f59b227 100644 --- a/modoboa/limits/models.py +++ b/modoboa/limits/models.py @@ -43,8 +43,7 @@ class ObjectLimitMixin(object): value = getattr(instance, field) if value == 0: # Do not allow unlimited value - raise lib.BadLimitValue( - _("You're not allowed to define unlimited values")) + raise lib.BadLimitValue(_("You're not allowed to define unlimited values")) try: old_value = instance._loaded_values[field] except (AttributeError, KeyError): @@ -63,12 +62,13 @@ class UserObjectLimit(ObjectLimitMixin, models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) name = models.CharField(max_length=254) - content_type = models.ForeignKey("contenttypes.ContentType", - on_delete=models.CASCADE) + content_type = models.ForeignKey( + "contenttypes.ContentType", on_delete=models.CASCADE + ) max_value = models.IntegerField(default=0) class Meta(object): - unique_together = (("user", "name"), ) + unique_together = (("user", "name"),) @property def definition(self): @@ -81,25 +81,23 @@ class UserObjectLimit(ObjectLimitMixin, models.Model): @property def current_value(self) -> int: """Return the current number of objects.""" - condition = ( - self.type == "count" and - "extra_filters" not in self.definition) + condition = self.type == "count" and "extra_filters" not in self.definition if condition: return core_models.ObjectAccess.objects.filter( - user=self.user, is_owner=True, - content_type=self.content_type).count() + user=self.user, is_owner=True, content_type=self.content_type + ).count() id_list = core_models.ObjectAccess.objects.filter( - user=self.user, is_owner=True, - content_type=self.content_type).values_list("object_id", flat=True) + user=self.user, is_owner=True, content_type=self.content_type + ).values_list("object_id", flat=True) model_class = self.content_type.model_class() if self.type == "count": return model_class.objects.filter( - pk__in=id_list, **self.definition["extra_filters"]).count() + pk__in=id_list, **self.definition["extra_filters"] + ).count() qset = model_class.objects.filter(pk__in=id_list) if not qset.exists(): return 0 - return qset.aggregate( - total=models.Sum(self.definition["field"]))["total"] + return qset.aggregate(total=models.Sum(self.definition["field"]))["total"] @property def label(self): @@ -130,7 +128,7 @@ class DomainObjectLimit(ObjectLimitMixin, models.Model): max_value = models.IntegerField(default=0) class Meta(object): - unique_together = (("domain", "name"), ) + unique_together = (("domain", "name"),) @property def definition(self): diff --git a/modoboa/limits/tests/test_domain_limits.py b/modoboa/limits/tests/test_domain_limits.py index 545105f0b..b34a4a072 100644 --- a/modoboa/limits/tests/test_domain_limits.py +++ b/modoboa/limits/tests/test_domain_limits.py @@ -17,13 +17,13 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): def setUpTestData(cls): # NOQA:N802 """Create test data.""" super(DomainLimitsTestCase, cls).setUpTestData() - cls.localconfig.parameters.set_values({ - "enable_admin_limits": False, - "enable_domain_limits": True - }) + cls.localconfig.parameters.set_values( + {"enable_admin_limits": False, "enable_domain_limits": True} + ) for name, _definition in utils.get_domain_limit_templates(): cls.localconfig.parameters.set_value( - "deflt_domain_{0}_limit".format(name), 2) + "deflt_domain_{0}_limit".format(name), 2 + ) cls.localconfig.save() admin_factories.populate_database() @@ -31,28 +31,30 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): """Try to set limits for a given domain.""" domain = Domain.objects.get(name="test.com") values = { - "name": domain.name, "quota": domain.quota, + "name": domain.name, + "quota": domain.quota, "default_mailbox_quota": domain.default_mailbox_quota, - "enabled": domain.enabled, "type": "domain", - "mailboxes_limit": 3, "mailbox_aliases_limit": 3, - "domain_aliases_limit": 3, "domain_admins_limit": 3 + "enabled": domain.enabled, + "type": "domain", + "mailboxes_limit": 3, + "mailbox_aliases_limit": 3, + "domain_aliases_limit": 3, + "domain_admins_limit": 3, } - self.ajax_post( - reverse("admin:domain_change", args=[domain.id]), - values - ) + self.ajax_post(reverse("admin:domain_change", args=[domain.id]), values) domain.refresh_from_db() self.assertEqual( - domain.domainobjectlimit_set.get(name="mailboxes").max_value, 3) + domain.domainobjectlimit_set.get(name="mailboxes").max_value, 3 + ) self.assertEqual( - domain.domainobjectlimit_set.get( - name="mailbox_aliases").max_value, 3) + domain.domainobjectlimit_set.get(name="mailbox_aliases").max_value, 3 + ) self.assertEqual( - domain.domainobjectlimit_set.get( - name="domain_aliases").max_value, 3) + domain.domainobjectlimit_set.get(name="domain_aliases").max_value, 3 + ) self.assertEqual( - domain.domainobjectlimit_set.get( - name="domain_admins").max_value, 3) + domain.domainobjectlimit_set.get(name="domain_admins").max_value, 3 + ) def test_domain_aliases_limit(self): """Try to exceed defined limit.""" @@ -60,23 +62,22 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): limit = domain.domainobjectlimit_set.get(name="domain_aliases") self.assertFalse(limit.is_exceeded()) values = { - "name": domain.name, "quota": domain.quota, + "name": domain.name, + "quota": domain.quota, "default_mailbox_quota": domain.default_mailbox_quota, - "enabled": domain.enabled, "type": "domain", - "mailboxes_limit": 2, "mailbox_aliases_limit": 2, - "domain_aliases_limit": 2, "domain_admins_limit": 2, - "aliases": "alias1.com", "aliases_1": "alias2.com" + "enabled": domain.enabled, + "type": "domain", + "mailboxes_limit": 2, + "mailbox_aliases_limit": 2, + "domain_aliases_limit": 2, + "domain_admins_limit": 2, + "aliases": "alias1.com", + "aliases_1": "alias2.com", } - self.ajax_post( - reverse("admin:domain_change", args=[domain.id]), - values - ) + self.ajax_post(reverse("admin:domain_change", args=[domain.id]), values) self.assertTrue(limit.is_exceeded()) values["aliases_2"] = "alias3.com" - self.ajax_post( - reverse("admin:domain_change", args=[domain.id]), - values, 403 - ) + self.ajax_post(reverse("admin:domain_change", args=[domain.id]), values, 403) def test_domain_admins_limit(self): """Try to exceed defined limit.""" @@ -85,37 +86,40 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): self.assertFalse(limit.is_exceeded()) user = User.objects.get(username="admin@test2.com") values = { - "username": user.username, "role": user.role, - "is_active": user.is_active, "email": user.email, - "quota_act": True, "domains": "test2.com", - "domains_1": "test.com", "language": "en" + "username": user.username, + "role": user.role, + "is_active": user.is_active, + "email": user.email, + "quota_act": True, + "domains": "test2.com", + "domains_1": "test.com", + "language": "en", } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) + self.ajax_post(reverse("admin:account_change", args=[user.id]), values) self.assertTrue(limit.is_exceeded()) user = core_factories.UserFactory( - username="admin1000@test.com", groups=("DomainAdmins", )) - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values, 400 + username="admin1000@test.com", groups=("DomainAdmins",) ) + self.ajax_post(reverse("admin:account_change", args=[user.id]), values, 400) def test_mailboxes_limit(self): """Try to exceed defined limits.""" domain = Domain.objects.get(name="test.com") - domain.domainobjectlimit_set.filter(name="mailboxes").update( - max_value=3) + domain.domainobjectlimit_set.filter(name="mailboxes").update(max_value=3) limit = domain.domainobjectlimit_set.get(name="mailboxes") self.assertFalse(limit.is_exceeded()) username = "toto@test.com" values = { "username": "toto@test.com", - "first_name": "Tester", "last_name": "Toto", - "password1": "Toto1234", "password2": "Toto1234", - "role": "SimpleUsers", "quota_act": True, - "is_active": True, "email": username, "stepid": "step2", + "first_name": "Tester", + "last_name": "Toto", + "password1": "Toto1234", + "password2": "Toto1234", + "role": "SimpleUsers", + "quota_act": True, + "is_active": True, + "email": username, + "stepid": "step2", } self.ajax_post(reverse("admin:account_add"), values, 200) self.assertTrue(limit.is_exceeded()) @@ -139,29 +143,27 @@ class DomainLimitsTestCase(lib_tests.ModoTestCase): limit.save() self.assertFalse(limit.is_exceeded()) values = { - "username": user.username, "role": user.role, - "is_active": user.is_active, "email": user.email, + "username": user.username, + "role": user.role, + "is_active": user.is_active, + "email": user.email, "quota_act": True, - "aliases": "alias@test.com", "aliases_1": "alias1@test.com", - "language": "en" + "aliases": "alias@test.com", + "aliases_1": "alias1@test.com", + "language": "en", } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) + self.ajax_post(reverse("admin:account_change", args=[user.id]), values) self.assertTrue(limit.is_exceeded()) values["aliases_2"] = "alias2@test.com" - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values, 403 - ) + self.ajax_post(reverse("admin:account_change", args=[user.id]), values, 403) limit.max_value = 5 limit.save() values = { - "address": "forward2@test.com", "recipients": "user@test.com", - "enabled": True + "address": "forward2@test.com", + "recipients": "user@test.com", + "enabled": True, } self.ajax_post(reverse("admin:alias_add"), values) self.assertTrue(limit.is_exceeded()) diff --git a/modoboa/limits/tests/test_import.py b/modoboa/limits/tests/test_import.py index a1d868026..08b4a4edd 100644 --- a/modoboa/limits/tests/test_import.py +++ b/modoboa/limits/tests/test_import.py @@ -17,7 +17,8 @@ class LimitImportTestCase(ModoTestCase): super(LimitImportTestCase, cls).setUpTestData() admin_factories.populate_database() cls.reseller = core_factories.UserFactory( - username="reseller", groups=("Resellers", )) + username="reseller", groups=("Resellers",) + ) cls.dadmin = core_models.User.objects.get(username="admin@test.com") cls.domain = admin_models.Domain.objects.get(name="test.com") @@ -25,24 +26,22 @@ class LimitImportTestCase(ModoTestCase): """Check domain aliases limit.""" self.client.force_login(self.reseller) self.assertFalse(limit.is_exceeded()) - f = ContentFile("""domainalias; domalias1.com; {domain}; True + f = ContentFile( + """domainalias; domalias1.com; {domain}; True domainalias; domalias2.com; {domain}; True -""".format(domain=self.domain), name="domains.csv") - self.client.post( - reverse("admin:domain_import"), { - "sourcefile": f - } +""".format( + domain=self.domain + ), + name="domains.csv", ) + self.client.post(reverse("admin:domain_import"), {"sourcefile": f}) self.assertTrue(limit.is_exceeded()) f = ContentFile( "domainalias; domalias3.com; {}; True".format(self.domain), - name="domains.csv") - response = self.client.post( - reverse("admin:domain_import"), { - "sourcefile": f - } + name="domains.csv", ) + response = self.client.post(reverse("admin:domain_import"), {"sourcefile": f}) self.assertContains(response, "Domain aliases: limit reached") def _test_domain_admins_import(self, limit, initial_count=0): @@ -53,24 +52,22 @@ domainalias; domalias2.com; {domain}; True for cpt in range(initial_count, 2): content += ( "account; admin{cpt}@{domain}; toto; User; One; True; " - "DomainAdmins; user{cpt}@{domain}; 5; {domain}\n" - .format(domain=self.domain, cpt=cpt) + "DomainAdmins; user{cpt}@{domain}; 5; {domain}\n".format( + domain=self.domain, cpt=cpt + ) ) f = ContentFile(content, name="domain_admins.csv") - response = self.client.post( - reverse("admin:identity_import"), { - "sourcefile": f - } - ) + response = self.client.post(reverse("admin:identity_import"), {"sourcefile": f}) self.assertTrue(limit.is_exceeded()) - f = ContentFile("""account; admin3@{domain}; toto; User; One; True; DomainAdmins; admin3@{domain}; 5; {domain} -""".format(domain=self.domain), name="domain_admins.csv") # NOQA:E501 - response = self.client.post( - reverse("admin:identity_import"), { - "sourcefile": f - } - ) + f = ContentFile( + """account; admin3@{domain}; toto; User; One; True; DomainAdmins; admin3@{domain}; 5; {domain} +""".format( + domain=self.domain + ), + name="domain_admins.csv", + ) # NOQA:E501 + response = self.client.post(reverse("admin:identity_import"), {"sourcefile": f}) self.assertContains(response, "Permission denied") def _test_mailboxes_import(self, limit): @@ -82,48 +79,47 @@ domainalias; domalias2.com; {domain}; True "user1@{domain}; 5\r\n" "account; truc@{domain}; toto; René; Truc; True; SimpleUsers; " "truc@{domain}; 5\r\n".format(domain=self.domain), - name="domains.csv" - ) - response = self.client.post( - reverse("admin:identity_import"), { - "sourcefile": f - } + name="domains.csv", ) + response = self.client.post(reverse("admin:identity_import"), {"sourcefile": f}) self.assertTrue(limit.is_exceeded()) - f = ContentFile(""" + f = ContentFile( + """ account; user3@{domain}; toto; User; One; True; SimpleUsers; user3@{domain}; 5 -""".format(domain=self.domain), name="domains.csv") - response = self.client.post( - reverse("admin:identity_import"), { - "sourcefile": f - } +""".format( + domain=self.domain + ), + name="domains.csv", ) + response = self.client.post(reverse("admin:identity_import"), {"sourcefile": f}) self.assertContains(response, "Mailboxes: limit reached") def _test_mailbox_aliases_import(self, limit): """Check mailbox aliases limit.""" self.client.force_login(self.dadmin) self.assertFalse(limit.is_exceeded()) - f = ContentFile("""alias; alias1@{domain}; True; user@{domain} + f = ContentFile( + """alias; alias1@{domain}; True; user@{domain} alias; alias2@{domain}; True; user@{domain} -""".format(domain=self.domain), name="aliases.csv") - self.client.post( - reverse("admin:identity_import"), { - "sourcefile": f - } +""".format( + domain=self.domain + ), + name="aliases.csv", ) + self.client.post(reverse("admin:identity_import"), {"sourcefile": f}) self.assertTrue(limit.is_exceeded()) - f = ContentFile(""" + f = ContentFile( + """ alias; alias3@{domain}; True; user@{domain} -""".format(domain=self.domain), name="aliases.csv") +""".format( + domain=self.domain + ), + name="aliases.csv", + ) with self.assertRaises(exceptions.ValidationError): - self.client.post( - reverse("admin:identity_import"), { - "sourcefile": f - } - ) + self.client.post(reverse("admin:identity_import"), {"sourcefile": f}) class UserLimitImportTestCase(LimitImportTestCase): @@ -133,8 +129,7 @@ class UserLimitImportTestCase(LimitImportTestCase): """Create test data.""" localconfig = core_models.LocalConfig.objects.first() for name, _definition in utils.get_user_limit_templates(): - localconfig.parameters.set_value( - "deflt_user_{0}_limit".format(name), 2) + localconfig.parameters.set_value("deflt_user_{0}_limit".format(name), 2) localconfig.save() super(UserLimitImportTestCase, cls).setUpTestData() @@ -144,30 +139,24 @@ class UserLimitImportTestCase(LimitImportTestCase): limit = self.reseller.userobjectlimit_set.get(name="domains") self.assertFalse(limit.is_exceeded()) f = ContentFile("domain; domain1.com; 1; 1; True", name="domains.csv") - self.client.post( - reverse("admin:domain_import"), {"sourcefile": f} - ) + self.client.post(reverse("admin:domain_import"), {"sourcefile": f}) self.assertFalse(limit.is_exceeded()) f = ContentFile("domain; domain2.com; 0; 1; True", name="domains.csv") - response = self.client.post( - reverse("admin:domain_import"), {"sourcefile": f} - ) - self.assertContains( - response, "not allowed to define unlimited values") + response = self.client.post(reverse("admin:domain_import"), {"sourcefile": f}) + self.assertContains(response, "not allowed to define unlimited values") f = ContentFile("domain; domain2.com; 2; 1; True", name="domains.csv") - response = self.client.post( - reverse("admin:domain_import"), {"sourcefile": f} - ) + response = self.client.post(reverse("admin:domain_import"), {"sourcefile": f}) self.assertContains(response, "Quota: limit reached") - f = ContentFile(b"""domain; domain2.com; 1; 1; True + f = ContentFile( + b"""domain; domain2.com; 1; 1; True domain; domain3.com; 1000; 100; True -""", name="domains.csv") - response = self.client.post( - reverse("admin:domain_import"), {"sourcefile": f} +""", + name="domains.csv", ) + response = self.client.post(reverse("admin:domain_import"), {"sourcefile": f}) self.assertContains(response, "Domains: limit reached") def test_domain_alias_import(self): @@ -198,18 +187,16 @@ class DomainLimitImportTestCase(LimitImportTestCase): def setUpTestData(cls): # NOQA:N802 """Create test data.""" localconfig = core_models.LocalConfig.objects.first() - localconfig.parameters.set_values({ - "enable_domain_limits": True, - "enable_admin_limits": False - }) + localconfig.parameters.set_values( + {"enable_domain_limits": True, "enable_admin_limits": False} + ) for name, _definition in utils.get_domain_limit_templates(): - localconfig.parameters.set_value( - "deflt_domain_{0}_limit".format(name), 2) + localconfig.parameters.set_value("deflt_domain_{0}_limit".format(name), 2) localconfig.save() super(DomainLimitImportTestCase, cls).setUpTestData() mb = admin_factories.MailboxFactory( - user__username="user@test4.com", - domain__name="test4.com", address="user") + user__username="user@test4.com", domain__name="test4.com", address="user" + ) cls.domain = mb.domain def test_domain_alias_import(self): diff --git a/modoboa/limits/tests/test_user_limits.py b/modoboa/limits/tests/test_user_limits.py index 79f72a719..7349c6429 100644 --- a/modoboa/limits/tests/test_user_limits.py +++ b/modoboa/limits/tests/test_user_limits.py @@ -24,18 +24,22 @@ class PermissionsTestCase(lib_tests.ModoTestCase): Expected result: no. """ values = { - "username": "reseller@test.com", "first_name": "Reseller", - "last_name": "", "password1": "Toto1234", "password2": "Toto1234", - "role": "Resellers", "is_active": True, - "email": "reseller@test.com", "stepid": "step2" + "username": "reseller@test.com", + "first_name": "Reseller", + "last_name": "", + "password1": "Toto1234", + "password2": "Toto1234", + "role": "Resellers", + "is_active": True, + "email": "reseller@test.com", + "stepid": "step2", } self.ajax_post(reverse("admin:account_add"), values) account = User.objects.get(username="reseller@test.com") admin = User.objects.get(username="admin@test.com") self.client.force_login(admin) resp = self.ajax_post( - reverse("admin:account_delete", args=[account.id]), - {}, status=403 + reverse("admin:account_delete", args=[account.id]), {}, status=403 ) self.assertEqual(resp, "Permission denied") @@ -46,59 +50,63 @@ class ResourceTestCase(lib_tests.ModoTestCase): def setUpTestData(cls): # NOQA:N802 """Custom setUpTestData method.""" super(ResourceTestCase, cls).setUpTestData() - cls.localconfig.parameters.set_values({ - "enable_admin_limits": True, - "enable_domain_limits": False - }) + cls.localconfig.parameters.set_values( + {"enable_admin_limits": True, "enable_domain_limits": False} + ) for name, _definition in utils.get_user_limit_templates(): - cls.localconfig.parameters.set_value( - "deflt_user_{0}_limit".format(name), 2) + cls.localconfig.parameters.set_value("deflt_user_{0}_limit".format(name), 2) cls.localconfig.save() populate_database() - def _create_account( - self, username, role="SimpleUsers", status=200, **kwargs): + def _create_account(self, username, role="SimpleUsers", status=200, **kwargs): values = { - "username": username, "first_name": "Tester", "last_name": "Toto", - "password1": "Toto1234", "password2": "Toto1234", "role": role, - "quota_act": True, "is_active": True, "email": username, - "stepid": "step2" + "username": username, + "first_name": "Tester", + "last_name": "Toto", + "password1": "Toto1234", + "password2": "Toto1234", + "role": role, + "quota_act": True, + "is_active": True, + "email": username, + "stepid": "step2", } values.update(kwargs) - return self.ajax_post( - reverse("admin:account_add"), values, status - ) + return self.ajax_post(reverse("admin:account_add"), values, status) def _create_alias(self, email, rcpt="user@test.com", status=200): - values = { - "address": email, "recipients": rcpt, "enabled": True - } - return self.ajax_post( - reverse("admin:alias_add"), values, status - ) + values = {"address": email, "recipients": rcpt, "enabled": True} + return self.ajax_post(reverse("admin:alias_add"), values, status) def _create_domain(self, name, status=200, withtpl=False, **kwargs): values = { - "name": name, "quota": 100, "default_mailbox_quota": 10, - "create_dom_admin": False, "with_mailbox": True, - "create_aliases": False, "stepid": "step3", "type": "domain" + "name": name, + "quota": 100, + "default_mailbox_quota": 10, + "create_dom_admin": False, + "with_mailbox": True, + "create_aliases": False, + "stepid": "step3", + "type": "domain", } if withtpl: - values.update({ - "create_dom_admin": True, - "dom_admin_username": "admin", - "create_aliases": True - }) + values.update( + { + "create_dom_admin": True, + "dom_admin_username": "admin", + "create_aliases": True, + } + ) values.update(kwargs) - response = self.ajax_post( - reverse("admin:domain_add"), values, status - ) + response = self.ajax_post(reverse("admin:domain_add"), values, status) return response def _domain_alias_operation(self, optype, domain, name, status=200): dom = Domain.objects.get(name=domain) values = { - "name": dom.name, "quota": dom.quota, "enabled": dom.enabled, + "name": dom.name, + "quota": dom.quota, + "enabled": dom.enabled, "type": "domain", "default_mailbox_quota": dom.default_mailbox_quota, } @@ -110,10 +118,7 @@ class ResourceTestCase(lib_tests.ModoTestCase): for cpt, alias in enumerate(aliases): fname = "aliases" if not cpt else "aliases_%d" % cpt values[fname] = alias - self.ajax_post( - reverse("admin:domain_change", args=[dom.id]), - values, status - ) + self.ajax_post(reverse("admin:domain_change", args=[dom.id]), values, status) def _check_limit(self, name, curvalue, maxvalue): limit = self.user.userobjectlimit_set.get(name=name) @@ -129,7 +134,8 @@ class DomainAdminTestCase(ResourceTestCase): super(DomainAdminTestCase, cls).setUpTestData() cls.user = User.objects.get(username="admin@test.com") cls.user.userobjectlimit_set.filter( - name__in=["mailboxes", "mailbox_aliases"]).update(max_value=2) + name__in=["mailboxes", "mailbox_aliases"] + ).update(max_value=2) def setUp(self): """Test initialization.""" @@ -144,9 +150,11 @@ class DomainAdminTestCase(ResourceTestCase): self._create_account("tester3@test.com", status=403) self._check_limit("mailboxes", 2, 2) self.ajax_post( - reverse("admin:account_delete", - args=[User.objects.get(username="tester2@test.com").id]), - {} + reverse( + "admin:account_delete", + args=[User.objects.get(username="tester2@test.com").id], + ), + {}, ) self._check_limit("mailboxes", 1, 2) @@ -159,27 +167,29 @@ class DomainAdminTestCase(ResourceTestCase): self._check_limit("mailbox_aliases", 2, 2) # Set unlimited value self.user.userobjectlimit_set.filter(name="mailbox_aliases").update( - max_value=-1) + max_value=-1 + ) self._create_alias("alias3@test.com") self._check_limit("mailbox_aliases", 3, -1) self.ajax_delete( - reverse("admin:alias_delete") + "?selection=%d" - % Alias.objects.get(address="alias2@test.com").id + reverse("admin:alias_delete") + + "?selection=%d" % Alias.objects.get(address="alias2@test.com").id ) self._check_limit("mailbox_aliases", 2, -1) def test_aliases_limit_through_account_form(self): user = User.objects.get(username="user@test.com") values = { - "username": user.username, "role": user.role, - "is_active": user.is_active, "email": user.email, "quota_act": True, - "aliases": "alias1@test.com", "aliases_1": "alias2@test.com", - "language": "en" + "username": user.username, + "role": user.role, + "is_active": user.is_active, + "email": user.email, + "quota_act": True, + "aliases": "alias1@test.com", + "aliases_1": "alias2@test.com", + "language": "en", } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) + self.ajax_post(reverse("admin:account_change", args=[user.id]), values) Alias.objects.get(address="alias1@test.com") self._check_limit("mailbox_aliases", 2, 2) @@ -192,9 +202,7 @@ class ResellerTestCase(ResourceTestCase): super(ResellerTestCase, cls).setUpTestData() cls.localconfig.parameters.set_value("deflt_user_quota_limit", 1000) cls.localconfig.save() - cls.user = UserFactory( - username="reseller", groups=("Resellers",) - ) + cls.user = UserFactory(username="reseller", groups=("Resellers",)) def setUp(self): """Test initialization.""" @@ -212,9 +220,10 @@ class ResellerTestCase(ResourceTestCase): self._create_domain("domain3.tld", 403) self._check_limit("domains", 2, 2) self.ajax_post( - reverse("admin:domain_delete", - args=[Domain.objects.get(name="domain2.tld").id]), - {} + reverse( + "admin:domain_delete", args=[Domain.objects.get(name="domain2.tld").id] + ), + {}, ) self._check_limit("domains", 1, 2) @@ -224,12 +233,9 @@ class ResellerTestCase(ResourceTestCase): self._check_limit("domain_aliases", 1, 2) self._domain_alias_operation("add", "pouet.com", "domain-alias2.tld") self._check_limit("domain_aliases", 2, 2) - self._domain_alias_operation( - "add", "pouet.com", "domain-alias3.tld", 403 - ) + self._domain_alias_operation("add", "pouet.com", "domain-alias3.tld", 403) self._check_limit("domain_aliases", 2, 2) - self._domain_alias_operation( - "delete", "pouet.com", "domain-alias2.tld") + self._domain_alias_operation("delete", "pouet.com", "domain-alias2.tld") self._check_limit("domain_aliases", 1, 2) def test_domain_admins_limit(self): @@ -244,33 +250,33 @@ class ResellerTestCase(ResourceTestCase): self._create_account("admin2@domain.tld", role="DomainAdmins") self._check_limit("domain_admins", 2, 2) resp = self._create_account( - "admin3@domain.tld", - role="DomainAdmins", - status=400) + "admin3@domain.tld", role="DomainAdmins", status=400 + ) self.assertEqual( resp["form_errors"]["role"][0], "Select a valid choice. DomainAdmins is not one of the available " - "choices." + "choices.", ) self._check_limit("domain_admins", 2, 2) - self.user.userobjectlimit_set.filter( - name="mailboxes").update(max_value=3) + self.user.userobjectlimit_set.filter(name="mailboxes").update(max_value=3) self._create_account("user1@domain.tld") user = User.objects.get(username="user1@domain.tld") values = { - "username": user.username, "role": "DomainAdmins", - "quota_act": True, "is_active": user.is_active, - "email": user.email, "language": "en" + "username": user.username, + "role": "DomainAdmins", + "quota_act": True, + "is_active": user.is_active, + "email": user.email, + "language": "en", } resp = self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values, status=400 + reverse("admin:account_change", args=[user.id]), values, status=400 ) self.assertEqual( resp["form_errors"]["role"][0], "Select a valid choice. DomainAdmins is not one of the available " - "choices." + "choices.", ) self._check_limit("domain_admins", 2, 2) @@ -279,13 +285,10 @@ class ResellerTestCase(ResourceTestCase): self._create_account("admin1@domain.tld", role="DomainAdmins") domadmin = User.objects.get(username="admin1@domain.tld") for l in ["mailboxes", "mailbox_aliases"]: - self.assertEqual( - domadmin.userobjectlimit_set.get(name=l).max_value, 0 - ) + self.assertEqual(domadmin.userobjectlimit_set.get(name=l).max_value, 0) def test_domain_admins_limit_from_domain_tpl(self): - self.user.userobjectlimit_set.filter( - name="domains").update(max_value=3) + self.user.userobjectlimit_set.filter(name="domains").update(max_value=3) self._create_domain("domain1.tld", withtpl=True) self._create_domain("domain2.tld", withtpl=True) self._check_limit("domain_admins", 2, 2) @@ -298,14 +301,17 @@ class ResellerTestCase(ResourceTestCase): """Check quota resource.""" self._create_domain("domain1.tld", withtpl=True, quota=1000) response = self._create_domain( - "domain2.tld", status=403, withtpl=True, quota=1000) + "domain2.tld", status=403, withtpl=True, quota=1000 + ) self.assertEqual(response, "Quota: limit reached") dom1 = Domain.objects.get(name="domain1.tld") url = reverse("admin:domain_change", args=[dom1.pk]) values = { - "name": dom1.name, "type": dom1.type, "enabled": dom1.enabled, + "name": dom1.name, + "type": dom1.type, + "enabled": dom1.enabled, "default_mailbox_quota": dom1.default_mailbox_quota, - "quota": 500 + "quota": 500, } self.ajax_post(url, values) @@ -313,13 +319,13 @@ class ResellerTestCase(ResourceTestCase): """Check reseller can't define unlimited quota.""" response = self._create_domain("domain1.tld", 400, quota=0) self.assertEqual( - response["form_errors"]["quota"][0], - "You can't define an unlimited quota.") - response = self._create_domain( - "domain1.tld", 400, default_mailbox_quota=0) + response["form_errors"]["quota"][0], "You can't define an unlimited quota." + ) + response = self._create_domain("domain1.tld", 400, default_mailbox_quota=0) self.assertEqual( response["form_errors"]["default_mailbox_quota"][0], - "You can't define an unlimited quota.") + "You can't define an unlimited quota.", + ) self.user.userobjectlimit_set.filter(name="quota").update(max_value=0) response = self._create_domain("domain2.tld", quota=0) @@ -330,31 +336,33 @@ class ResellerTestCase(ResourceTestCase): # See https://github.com/modoboa/modoboa/issues/1223 self._create_domain("domain1.tld") self._create_account( - "admin@domain1.tld", role="DomainAdmins", - quota_act=False, quota=0, status=400) + "admin@domain1.tld", + role="DomainAdmins", + quota_act=False, + quota=0, + status=400, + ) self._create_domain("domain2.tld", withtpl=True) admin = User.objects.get(username="admin@domain2.tld") values = { - "username": admin.username, "role": admin.role, - "is_active": admin.is_active, "email": admin.email, - "mailboxes_limit": 1, "mailbox_aliases_limit": 2, - "language": "en", "quota_act": False, "quota": 0 + "username": admin.username, + "role": admin.role, + "is_active": admin.is_active, + "email": admin.email, + "mailboxes_limit": 1, + "mailbox_aliases_limit": 2, + "language": "en", + "quota_act": False, + "quota": 0, } - self.ajax_post( - reverse("admin:account_change", args=[admin.pk]), - values, - 400 - ) + self.ajax_post(reverse("admin:account_change", args=[admin.pk]), values, 400) def test_reseller_deletes_domain(self): """Check if all resources are restored after the deletion.""" self._create_domain("domain.tld", withtpl=True) dom = Domain.objects.get(name="domain.tld") - self.ajax_post( - reverse("admin:domain_delete", args=[dom.id]), - {} - ) + self.ajax_post(reverse("admin:domain_delete", args=[dom.id]), {}) self._check_limit("domains", 0, 2) self._check_limit("domain_admins", 1, 2) self._check_limit("mailboxes", 0, 2) @@ -367,9 +375,9 @@ class ResellerTestCase(ResourceTestCase): self.client.login(username="admin", password="password") self.ajax_delete( "{0}?domid={1}&daid={2}".format( - reverse("admin:permission_remove"), - dom.id, self.user.id - ), {} + reverse("admin:permission_remove"), dom.id, self.user.id + ), + {}, ) self._check_limit("domains", 0, 2) self._check_limit("domain_admins", 0, 2) @@ -383,24 +391,22 @@ class ResellerTestCase(ResourceTestCase): # Give 1 mailbox and 2 aliases to the admin -> should work values = { - "username": user.username, "role": user.role, "quota_act": True, - "is_active": user.is_active, "email": user.email, - "mailboxes_limit": 1, "mailbox_aliases_limit": 2, - "language": "en" + "username": user.username, + "role": user.role, + "quota_act": True, + "is_active": user.is_active, + "email": user.email, + "mailboxes_limit": 1, + "mailbox_aliases_limit": 2, + "language": "en", } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) + self.ajax_post(reverse("admin:account_change", args=[user.id]), values) self._check_limit("mailboxes", 1, 1) self._check_limit("mailbox_aliases", 0, 0) # Delete the admin -> resources should go back to the # reseller's pool - self.ajax_post( - reverse("admin:account_delete", args=[user.id]), - {} - ) + self.ajax_post(reverse("admin:account_delete", args=[user.id]), {}) self._check_limit("mailboxes", 0, 2) self._check_limit("mailbox_aliases", 0, 2) @@ -411,15 +417,16 @@ class ResellerTestCase(ResourceTestCase): self._create_account("admin1@domain.tld", role="DomainAdmins") user = User.objects.get(username="admin1@domain.tld") values = { - "username": user.username, "role": user.role, "quota_act": True, - "is_active": user.is_active, "email": user.email, - "mailboxes_limit": 1, "mailbox_aliases_limit": 2, - "language": "en" + "username": user.username, + "role": user.role, + "quota_act": True, + "is_active": user.is_active, + "email": user.email, + "mailboxes_limit": 1, + "mailbox_aliases_limit": 2, + "language": "en", } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) + self.ajax_post(reverse("admin:account_change", args=[user.id]), values) dom.add_admin(user) self.client.logout() self.client.login(username="admin1@domain.tld", password="Toto1234") @@ -430,10 +437,7 @@ class ResellerTestCase(ResourceTestCase): self.client.login(username="reseller", password="toto") # Delete the admin -> resources should go back to the # reseller's pool - self.ajax_post( - reverse("admin:account_delete", args=[user.id]), - {} - ) + self.ajax_post(reverse("admin:account_delete", args=[user.id]), {}) self._check_limit("mailboxes", 1, 2) self._check_limit("mailbox_aliases", 2, 2) @@ -444,30 +448,30 @@ class ResellerTestCase(ResourceTestCase): # Give 1 mailbox and 2 aliases to the admin -> should work values = { - "username": user.username, "role": user.role, "quota_act": True, - "is_active": user.is_active, "email": user.email, - "mailboxes_limit": 1, "mailbox_aliases_limit": 2, - "language": "en" + "username": user.username, + "role": user.role, + "quota_act": True, + "is_active": user.is_active, + "email": user.email, + "mailboxes_limit": 1, + "mailbox_aliases_limit": 2, + "language": "en", } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) + self.ajax_post(reverse("admin:account_change", args=[user.id]), values) self._check_limit("mailboxes", 1, 1) self._check_limit("mailbox_aliases", 0, 0) # Change admin role to SimpleUser -> resources should go back # to the reseller. values = { - "username": user.username, "role": "SimpleUsers", + "username": user.username, + "role": "SimpleUsers", "quota_act": True, - "is_active": user.is_active, "email": user.email, + "is_active": user.is_active, + "email": user.email, "language": "en", } - self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values - ) + self.ajax_post(reverse("admin:account_change", args=[user.id]), values) self._check_limit("mailboxes", 1, 2) self._check_limit("mailbox_aliases", 0, 2) @@ -478,14 +482,17 @@ class ResellerTestCase(ResourceTestCase): # Give 2 mailboxes and 3 aliases to the admin -> should fail. values = { - "username": user.username, "role": user.role, "quota_act": True, - "is_active": user.is_active, "email": user.email, - "mailboxes_limit": 2, "mailbox_aliases_limit": 3, - "language": "en" + "username": user.username, + "role": user.role, + "quota_act": True, + "is_active": user.is_active, + "email": user.email, + "mailboxes_limit": 2, + "mailbox_aliases_limit": 3, + "language": "en", } resp = self.ajax_post( - reverse("admin:account_change", args=[user.id]), - values, 424 + reverse("admin:account_change", args=[user.id]), values, 424 ) self.assertEqual(resp, "Not enough resources") self._check_limit("mailboxes", 1, 2) diff --git a/modoboa/maillog/api/v2/serializers.py b/modoboa/maillog/api/v2/serializers.py index d61601a93..c7e68546a 100644 --- a/modoboa/maillog/api/v2/serializers.py +++ b/modoboa/maillog/api/v2/serializers.py @@ -12,7 +12,7 @@ PERIODS = [ ("week", "Week"), ("month", "Month"), ("year", "Year"), - ("custom", "Custom") + ("custom", "Custom"), ] @@ -27,9 +27,8 @@ class StatisticsInputSerializer(serializers.Serializer): end = serializers.DateField(required=False) def validate(self, data): - condition = ( - data["period"] == "custom" and - (not data.get("start") or not data.get("end")) + condition = data["period"] == "custom" and ( + not data.get("start") or not data.get("end") ) if condition: raise serializers.ValidationError( @@ -71,8 +70,14 @@ class MaillogSerializer(serializers.ModelSerializer): class Meta: fields = ( - "id", "queue_id", "date", "sender", "rcpt", "original_rcpt", - "size", "status" + "id", + "queue_id", + "date", + "sender", + "rcpt", + "original_rcpt", + "size", + "status", ) model = models.Maillog diff --git a/modoboa/maillog/api/v2/tests.py b/modoboa/maillog/api/v2/tests.py index 767716a3a..073b1aea8 100644 --- a/modoboa/maillog/api/v2/tests.py +++ b/modoboa/maillog/api/v2/tests.py @@ -12,15 +12,11 @@ class StatisticsViewSetTestCase(ModoAPITestCase): resp = self.client.get(url + "?gset=mailtraffic&period=day") self.assertEqual(resp.status_code, 200) - resp = self.client.get( - url + - "?gset=mailtraffic&period=custom&start=2021-05-01" - ) + resp = self.client.get(url + "?gset=mailtraffic&period=custom&start=2021-05-01") self.assertEqual(resp.status_code, 400) resp = self.client.get( - url + - "?gset=mailtraffic&period=custom&start=2021-05-01&end=2021-05-02" + url + "?gset=mailtraffic&period=custom&start=2021-05-01&end=2021-05-02" ) self.assertEqual(resp.status_code, 200) diff --git a/modoboa/maillog/api/v2/urls.py b/modoboa/maillog/api/v2/urls.py index 3fd2217c2..41e27834c 100644 --- a/modoboa/maillog/api/v2/urls.py +++ b/modoboa/maillog/api/v2/urls.py @@ -5,9 +5,7 @@ from rest_framework import routers from . import viewsets router = routers.SimpleRouter() -router.register( - r"statistics", viewsets.StatisticsViewSet, basename="statistics") -router.register( - r"logs/messages", viewsets.MaillogViewSet, basename="maillog") +router.register(r"statistics", viewsets.StatisticsViewSet, basename="statistics") +router.register(r"logs/messages", viewsets.MaillogViewSet, basename="maillog") urlpatterns = router.urls diff --git a/modoboa/maillog/api/v2/viewsets.py b/modoboa/maillog/api/v2/viewsets.py index 89d1d3c5a..58a60546d 100644 --- a/modoboa/maillog/api/v2/viewsets.py +++ b/modoboa/maillog/api/v2/viewsets.py @@ -16,33 +16,29 @@ from ... import signals from . import serializers -class StatisticsViewSet(GetThrottleViewsetMixin,viewsets.ViewSet): +class StatisticsViewSet(GetThrottleViewsetMixin, viewsets.ViewSet): """A viewset to provide extra route related to mail statistics.""" - permission_classes = (permissions.IsAuthenticated, ) + permission_classes = (permissions.IsAuthenticated,) @extend_schema( parameters=[serializers.StatisticsInputSerializer], - responses={200: serializers.StatisticsSerializer} + responses={200: serializers.StatisticsSerializer}, ) def list(self, request, **kwargs): serializer = serializers.StatisticsInputSerializer(data=request.GET) serializer.is_valid(raise_exception=True) graph_sets = {} - for result in signals.get_graph_sets.send( - sender="index", user=request.user): + for result in signals.get_graph_sets.send(sender="index", user=request.user): graph_sets.update(result[1]) gset = serializer.validated_data["gset"] fname = graph_sets[gset].get_file_name( - request.user, serializer.validated_data.get("searchquery")) + request.user, serializer.validated_data.get("searchquery") + ) period = serializer.validated_data["period"] if period == "custom": - start = int( - time.mktime(serializer.validated_data["start"].timetuple()) - ) - end = int( - time.mktime(serializer.validated_data["end"].timetuple()) - ) + start = int(time.mktime(serializer.validated_data["start"].timetuple())) + end = int(time.mktime(serializer.validated_data["end"].timetuple())) else: end = int(time.mktime(time.localtime())) start = "-1{}".format(period) @@ -59,7 +55,7 @@ class MaillogViewSet(GetThrottleViewsetMixin, viewsets.ReadOnlyModelViewSet): ordering = ["-date"] ordering_fields = "__all__" pagination_class = pagination.CustomPageNumberPagination - permissions = (permissions.IsAuthenticated, ) + permissions = (permissions.IsAuthenticated,) search_fields = ["queue_id", "sender", "rcpt", "original_rcpt", "status"] serializer_class = serializers.MaillogSerializer diff --git a/modoboa/maillog/apps.py b/modoboa/maillog/apps.py index bcb8215d4..c264e4e2a 100644 --- a/modoboa/maillog/apps.py +++ b/modoboa/maillog/apps.py @@ -6,25 +6,47 @@ from django.apps import AppConfig from django.utils.translation import gettext, gettext_lazy as _ -GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([ - ("general", { - "label": _("General"), - "params": collections.OrderedDict([ - ("logfile", { - "label": _("Path to the log file"), - "help_text": _("Path to log file used to collect statistics") - }), - ("rrd_rootdir", { - "label": _("Directory to store RRD files"), - "help_text": _("Path to directory where RRD files are stored") - }), - ("greylist", { - "label": _("Show greylisted messages"), - "help_text": _("Differentiate between hard and soft rejects (greylisting)") - }) - ]) - }) -]) +GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict( + [ + ( + "general", + { + "label": _("General"), + "params": collections.OrderedDict( + [ + ( + "logfile", + { + "label": _("Path to the log file"), + "help_text": _( + "Path to log file used to collect statistics" + ), + }, + ), + ( + "rrd_rootdir", + { + "label": _("Directory to store RRD files"), + "help_text": _( + "Path to directory where RRD files are stored" + ), + }, + ), + ( + "greylist", + { + "label": _("Show greylisted messages"), + "help_text": _( + "Differentiate between hard and soft rejects (greylisting)" + ), + }, + ), + ] + ), + }, + ) + ] +) def load_maillog_settings(): @@ -33,11 +55,13 @@ def load_maillog_settings(): from modoboa.maillog.api.v2 import serializers from modoboa.parameters import tools as param_tools - param_tools.registry.add( - "global", forms.ParametersForm, gettext("Statistics")) + param_tools.registry.add("global", forms.ParametersForm, gettext("Statistics")) param_tools.registry.add2( - "global", "maillog", gettext("Statistics"), GLOBAL_PARAMETERS_STRUCT, - serializers.MaillogGlobalParemetersSerializer + "global", + "maillog", + gettext("Statistics"), + GLOBAL_PARAMETERS_STRUCT, + serializers.MaillogGlobalParemetersSerializer, ) diff --git a/modoboa/maillog/forms.py b/modoboa/maillog/forms.py index a7f386d05..7851ff8ec 100644 --- a/modoboa/maillog/forms.py +++ b/modoboa/maillog/forms.py @@ -23,22 +23,22 @@ class ParametersForm(param_forms.AdminParametersForm): label=gettext_lazy("Path to the log file"), initial="/var/log/mail.log", help_text=gettext_lazy("Path to log file used to collect statistics"), - widget=forms.TextInput(attrs={"class": "form-control"}) + widget=forms.TextInput(attrs={"class": "form-control"}), ) rrd_rootdir = forms.CharField( label=gettext_lazy("Directory to store RRD files"), initial="/tmp/modoboa", - help_text=gettext_lazy( - "Path to directory where RRD files are stored"), - widget=forms.TextInput(attrs={"class": "form-control"}) + help_text=gettext_lazy("Path to directory where RRD files are stored"), + widget=forms.TextInput(attrs={"class": "form-control"}), ) greylist = form_utils.YesNoField( label=gettext_lazy("Show greylisted messages"), initial=False, help_text=gettext_lazy( - "Differentiate between hard and soft rejects (greylisting)") + "Differentiate between hard and soft rejects (greylisting)" + ), ) def __init__(self, *args, **kwargs): diff --git a/modoboa/maillog/graphics.py b/modoboa/maillog/graphics.py index 2bac4c098..8b438153d 100644 --- a/modoboa/maillog/graphics.py +++ b/modoboa/maillog/graphics.py @@ -23,8 +23,7 @@ class Curve: """ def __init__(self, dsname, color, legend, cfunc="AVERAGE"): - """Constructor. - """ + """Constructor.""" self.dsname = dsname self.color = color self.legend = legend @@ -40,10 +39,9 @@ class Curve: param_tools.get_global_parameter("rrd_rootdir"), "%s.rrd" % rrdfile ) return [ - 'DEF:%s=%s:%s:%s' % - (self.dsname, rrdfile, self.dsname, self.cfunc), - 'CDEF:%(ds)spm=%(ds)s,UN,0,%(ds)s,IF,60,*' % {"ds": self.dsname}, - 'XPORT:%spm:"%s"' % (self.dsname, self.legend) + "DEF:%s=%s:%s:%s" % (self.dsname, rrdfile, self.dsname, self.cfunc), + "CDEF:%(ds)spm=%(ds)s,UN,0,%(ds)s,IF,60,*" % {"ds": self.dsname}, + 'XPORT:%spm:"%s"' % (self.dsname, self.legend), ] @@ -82,15 +80,15 @@ class Graphic: dpath = output.strip() else: known_paths = getattr( - settings, "RRDTOOL_LOOKUP_PATH", - ("/usr/bin/rrdtool", "/usr/local/bin/rrdtool") + settings, + "RRDTOOL_LOOKUP_PATH", + ("/usr/bin/rrdtool", "/usr/local/bin/rrdtool"), ) for fpath in known_paths: if os.path.isfile(fpath) and os.access(fpath, os.X_OK): dpath = fpath if dpath is None: - raise exceptions.InternalError( - _("Failed to locate rrdtool binary.")) + raise exceptions.InternalError(_("Failed to locate rrdtool binary.")) return smart_str(dpath) def export(self, rrdfile, start, end): @@ -98,27 +96,27 @@ class Graphic: result = [] cmdargs = [] for curve in self._curves: - result += [{ - "name": str(curve.legend), - "backgroundColor": curve.color, "data": [] - }] + result += [ + {"name": str(curve.legend), "backgroundColor": curve.color, "data": []} + ] cmdargs += curve.to_rrd_command_args(rrdfile) code = 0 cmd = "{} xport --json -t --start {} --end {} ".format( - self.rrdtool_binary, str(start), str(end)) + self.rrdtool_binary, str(start), str(end) + ) cmd += " ".join(cmdargs) code, output = exec_cmd(smart_bytes(cmd)) if code: return [] xport = json.loads(output) - for row in xport['data']: + for row in xport["data"]: timestamp = int(row[0]) - date = datetime.datetime.fromtimestamp(timestamp).isoformat(sep=' ') - for (vindex, value) in enumerate(row[1:]): - result[vindex]['data'].append( - {'x': date, 'y': value, 'timestamp': timestamp} + date = datetime.datetime.fromtimestamp(timestamp).isoformat(sep=" ") + for vindex, value in enumerate(row[1:]): + result[vindex]["data"].append( + {"x": date, "y": value, "timestamp": timestamp} ) return result @@ -159,7 +157,7 @@ class GraphicSet(object): if graphic is None or graphic == graph.display_name: result[graph.display_name] = { "title": str(graph.title), - "series": graph.export(rrdfile, start, end) + "series": graph.export(rrdfile, start, end), } return result @@ -167,7 +165,7 @@ class GraphicSet(object): class AverageTraffic(Graphic): """Average traffic.""" - title = gettext_lazy('Average traffic (msgs/min)') + title = gettext_lazy("Average traffic (msgs/min)") # Curve definitions sent = Curve("sent", "lawngreen", gettext_lazy("sent messages")) @@ -182,10 +180,16 @@ class AverageTraffic(Graphic): def __init__(self, greylist=False): if greylist: self.greylist = Curve( - "greylist", "dimgrey", gettext_lazy("greylisted messages")) + "greylist", "dimgrey", gettext_lazy("greylisted messages") + ) self.order = [ - "reject", "greylist", "bounced", "recv", "sent", "virus", - "spam" + "reject", + "greylist", + "bounced", + "recv", + "sent", + "virus", + "spam", ] super().__init__() @@ -193,13 +197,11 @@ class AverageTraffic(Graphic): class AverageTrafficSize(Graphic): """Average traffic size.""" - title = gettext_lazy('Average normal traffic size (bytes/min)') + title = gettext_lazy("Average normal traffic size (bytes/min)") # Curve definitions size_recv = Curve("size_recv", "orange", gettext_lazy("received size")) - size_sent = Curve( - "size_sent", "mediumturquoise", gettext_lazy("sent size") - ) + size_sent = Curve("size_sent", "mediumturquoise", gettext_lazy("sent size")) class MailTraffic(GraphicSet): @@ -232,8 +234,7 @@ class MailTraffic(GraphicSet): results = list( chain( admin_models.Domain.objects.filter(name__startswith=pattern), - admin_models.DomainAlias.objects.filter( - name__startswith=pattern) + admin_models.DomainAlias.objects.filter(name__startswith=pattern), ) ) if len(results) != 1: @@ -252,8 +253,7 @@ class AccountCreationGraphic(Graphic): title = gettext_lazy("Average account creation (account/hour)") - accounts = Curve( - "new_accounts", "steelblue", gettext_lazy("New accounts")) + accounts = Curve("new_accounts", "steelblue", gettext_lazy("New accounts")) class AccountGraphicSet(GraphicSet): diff --git a/modoboa/maillog/handlers.py b/modoboa/maillog/handlers.py index 99ff0c906..57286d2af 100644 --- a/modoboa/maillog/handlers.py +++ b/modoboa/maillog/handlers.py @@ -18,9 +18,7 @@ def menu(sender, location, user, **kwargs): if location != "top_menu" or user.role == "SimpleUsers": return [] return [ - {"name": "stats", - "label": _("Statistics"), - "url": reverse('maillog:fullindex')} + {"name": "stats", "label": _("Statistics"), "url": reverse("maillog:fullindex")} ] @@ -28,10 +26,9 @@ def menu(sender, location, user, **kwargs): def get_default_graphic_sets(sender, **kwargs): """Return graphic set.""" mail_traffic_gset = graphics.MailTraffic( - param_tools.get_global_parameter("greylist", raise_exception=False)) - result = { - mail_traffic_gset.html_id: mail_traffic_gset - } + param_tools.get_global_parameter("greylist", raise_exception=False) + ) + result = {mail_traffic_gset.html_id: mail_traffic_gset} if kwargs.get("user").is_superuser: account_gset = graphics.AccountGraphicSet() result.update({account_gset.html_id: account_gset}) diff --git a/modoboa/maillog/lib.py b/modoboa/maillog/lib.py index 5417ba542..fa5e04184 100644 --- a/modoboa/maillog/lib.py +++ b/modoboa/maillog/lib.py @@ -12,11 +12,9 @@ def date_to_timestamp(timetuple): :return: an integer """ date = " ".join( - [("%d" % elem) if isinstance(elem, int) else elem - for elem in timetuple] + [("%d" % elem) if isinstance(elem, int) else elem for elem in timetuple] ) - fmt = "%Y %m %d %H %M %S" \ - if timetuple[1].isdigit() else "%Y %b %d %H %M %S" + fmt = "%Y %m %d %H %M %S" if timetuple[1].isdigit() else "%Y %b %d %H %M %S" try: local = time.strptime(date, fmt) except ValueError: diff --git a/modoboa/maillog/management/commands/logparser.py b/modoboa/maillog/management/commands/logparser.py index 734722cfb..5212963c1 100644 --- a/modoboa/maillog/management/commands/logparser.py +++ b/modoboa/maillog/management/commands/logparser.py @@ -43,8 +43,16 @@ from ... import models rrdstep = 60 xpoints = 540 points_per_sample = 3 -variables = ["sent", "recv", "bounced", "reject", "spam", "virus", - "size_sent", "size_recv"] +variables = [ + "sent", + "recv", + "bounced", + "reject", + "spam", + "virus", + "size_sent", + "size_recv", +] class LogParser: @@ -73,10 +81,9 @@ class LogParser: # set up regular expression self._date_expressions = [ r"(?P\w+)\s+(?P\d+)\s+(?P\d+):(?P\d+):(?P\d+)(?P.*)", # noqa - r"(?P\d+)-(?P\d+)-(?P\d+)T(?P\d+):(?P\d+):(?P\d+)\.\d+\+\d+:\d+(?P.*)" # noqa + r"(?P\d+)-(?P\d+)-(?P\d+)T(?P\d+):(?P\d+):(?P\d+)\.\d+\+\d+:\d+(?P.*)", # noqa ] - self._date_expressions = [ - re.compile(v) for v in self._date_expressions] + self._date_expressions = [re.compile(v) for v in self._date_expressions] self.date_expr = None self._regex = { "line": r"\s+([-\w\.]+)\s+(\w+)/?(\w*)[[](\d+)[]]:\s+(.*)", @@ -91,14 +98,13 @@ class LogParser: "rmilter_msg_done": r"msg done: queue_id: <(?P[^>]+)>; message id: <(?P[^>]+)>.*; from: <(?P[^>]+)>; rcpt: <(?P[^>]+)>.*; spam scan: (?P[^;]+); virus scan:", # noqa "rmilter_spam": r"mlfi_eom: (rejecting spam|add spam header to message according to spamd action)", # noqa "rmilter_virus": r"mlfi_eom:.* virus found", - "rmilter_greylist": ( - r"GREYLIST\([0-9]+\.[0-9]{2}\)\[greylisted[^\]]*\]") + "rmilter_greylist": (r"GREYLIST\([0-9]+\.[0-9]{2}\)\[greylisted[^\]]*\]"), } self._regex = {k: re.compile(v) for k, v in self._regex.items()} self._srs_regex = { "detect_srs": "^SRS[01][+-=]", "reverse_srs0": r"^SRS0[+-=]\S+=\S{2}=(\S+)=(\S+)\@\S+$", - "reverse_srs1": r"^SRS1[+-=]\S+=\S+==\S+=\S{2}=(\S+)=(\S+)\@\S+$" + "reverse_srs1": r"^SRS1[+-=]\S+=\S+==\S+=\S{2}=(\S+)=(\S+)\@\S+$", } self._srs_regex = { k: re.compile(v, re.IGNORECASE) for k, v in self._srs_regex.items() @@ -192,7 +198,7 @@ class LogParser: """ ds_type = "ABSOLUTE" rows = xpoints / points_per_sample - realrows = int(rows * 1.1) # ensure that the full range is covered + realrows = int(rows * 1.1) # ensure that the full range is covered day_steps = int(3600 * 24 / (rrdstep * rows)) week_steps = day_steps * 7 month_steps = week_steps * 5 @@ -209,10 +215,7 @@ class LogParser: params += ["RRA:%s:0.5:%s:%s" % (cf, step, realrows)] # With those setup, we can now created the RRD - rrdtool.create(str(fname), - "--start", str(m), - "--step", str(rrdstep), - *params) + rrdtool.create(str(fname), "--start", str(m), "--step", str(rrdstep), *params) return m def add_datasource_to_rrd(self, fname, dsname): @@ -326,14 +329,13 @@ class LogParser: :param month: the month of the current record beeing parsed :return: an integer """ - month = time.strptime(month, "%b").tm_mon if not month.isdigit() \ - else int(month) + month = time.strptime(month, "%b").tm_mon if not month.isdigit() else int(month) if self.curmonth == 1 and month != self.curmonth: return self.__year - 1 return self.__year def is_srs_forward(self, mail_address): - """ Check if mail address has been mangled by SRS + """Check if mail address has been mangled by SRS Sender Rewriting Scheme (SRS) modifies mail adresses so that they always end in a local domain. @@ -344,7 +346,7 @@ class LogParser: return self._srs_regex["detect_srs"].match(mail_address) is not None def reverse_srs(self, mail_address): - """ Try to unwind a mail address rewritten by SRS + """Try to unwind a mail address rewritten by SRS Sender Rewriting Scheme (SRS) modifies mail adresses so that they always end in a local domain. Common Postfix implementations of SRS @@ -354,8 +356,7 @@ class LogParser: :return a str """ m = self._srs_regex["reverse_srs0"].match(mail_address) - m = self._srs_regex["reverse_srs1"].match(mail_address) \ - if m is None else m + m = self._srs_regex["reverse_srs1"].match(mail_address) if m is None else m if m is not None: return "%s@%s" % m.group(2, 1) @@ -363,7 +364,7 @@ class LogParser: return mail_address def _parse_amavis(self, log, host, pid, subprog): - """ Parse an Amavis log entry. + """Parse an Amavis log entry. :param str log: logged message :param str host: hostname @@ -384,7 +385,7 @@ class LogParser: return False def _parse_rmilter(self, log, host, pid, subprog): - """ Parse an Rmilter log entry. + """Parse an Rmilter log entry. :param str log: logged message :param str host: hostname @@ -400,24 +401,18 @@ class LogParser: # Virus check must come before spam check due to pattern similarity. m = self._regex["rmilter_virus"].match(msg) if m is not None: - self.workdict[workdict_key] = { - "action": "virus" - } + self.workdict[workdict_key] = {"action": "virus"} return True m = self._regex["rmilter_spam"].match(msg) if m is not None: - self.workdict[workdict_key] = { - "action": "spam" - } + self.workdict[workdict_key] = {"action": "spam"} return True # Greylisting if self.greylist: m = self._regex["rmilter_greylist"].search(msg) if m is not None: - self.workdict[workdict_key] = { - "action": "greylist" - } + self.workdict[workdict_key] = {"action": "greylist"} return True # Gather information about message sender and queue ID @@ -434,7 +429,7 @@ class LogParser: return False def _parse_postfix(self, log, host, pid, subprog): - """ Parse a log entry generated by Postfix. + """Parse a log entry generated by Postfix. :param str log: logged message :param str host: hostname @@ -452,10 +447,8 @@ class LogParser: m = self._regex["reject"].match(msg) dom = m.group(1) if m is not None else None if dom in self.domains: - condition = ( - self.greylist and ( - "Greylisted" in msg or - (subprog == "postscreen" and " 450 " in msg)) + condition = self.greylist and ( + "Greylisted" in msg or (subprog == "postscreen" and " 450 " in msg) ) if condition: self.inc_counter(dom, "greylist") @@ -474,7 +467,7 @@ class LogParser: if m is not None: self.workdict[queue_id] = { "from": self.reverse_srs(m.group(1)), - "size": int(m.group(2)) + "size": int(m.group(2)), } return True @@ -484,12 +477,12 @@ class LogParser: return False (msg_to, msg_status) = m.groups() if queue_id not in self.workdict: - self._dprint("[parser] inconsistent mail (%s: %s), skipping" - % (queue_id, msg_to)) + self._dprint( + "[parser] inconsistent mail (%s: %s), skipping" % (queue_id, msg_to) + ) return True if msg_status not in variables: - self._dprint("[parser] unsupported status %s, skipping" - % msg_status) + self._dprint("[parser] unsupported status %s, skipping" % msg_status) return True # orig_to is optional. @@ -500,15 +493,11 @@ class LogParser: from_domain = split_mailbox(self.workdict[queue_id]["from"])[1] if from_domain is not None and from_domain in self.domains: self.inc_counter(from_domain, "sent") - self.inc_counter(from_domain, "size_sent", - self.workdict[queue_id]["size"]) + self.inc_counter(from_domain, "size_sent", self.workdict[queue_id]["size"]) # Handle local "to" domains. to_domain = None - condition = ( - msg_orig_to is not None and - not self.is_srs_forward(msg_orig_to) - ) + condition = msg_orig_to is not None and not self.is_srs_forward(msg_orig_to) if condition: to_domain = split_mailbox(msg_orig_to)[1] if to_domain is None: @@ -516,8 +505,7 @@ class LogParser: if msg_status == "sent": self.inc_counter(to_domain, "recv") - self.inc_counter(to_domain, "size_recv", - self.workdict[queue_id]["size"]) + self.inc_counter(to_domain, "size_recv", self.workdict[queue_id]["size"]) else: self.inc_counter(to_domain, msg_status) @@ -525,26 +513,25 @@ class LogParser: cur_dt = datetime.fromtimestamp(self.orig_ts) tz = timezone.get_current_timezone() cur_dt = cur_dt.replace(tzinfo=tz) - condition = ( - last_message and ( - cur_dt < last_message.date or - (cur_dt == last_message.date and - models.Maillog.objects.filter( - date=cur_dt, queue_id=queue_id).exists()) + condition = last_message and ( + cur_dt < last_message.date + or ( + cur_dt == last_message.date + and models.Maillog.objects.filter( + date=cur_dt, queue_id=queue_id + ).exists() ) ) if not condition: if from_domain in self.domains: from_domain = Domain.objects.filter( - Q(name=from_domain) | - Q(domainalias__name=from_domain) + Q(name=from_domain) | Q(domainalias__name=from_domain) ).first() else: from_domain = None if to_domain in self.domains: to_domain = Domain.objects.filter( - Q(name=to_domain) | - Q(domainalias__name=to_domain) + Q(name=to_domain) | Q(domainalias__name=to_domain) ).first() else: to_domain = None @@ -559,7 +546,7 @@ class LogParser: size=self.workdict[queue_id]["size"], status=msg_status, from_domain=from_domain, - to_domain=to_domain + to_domain=to_domain, ) return True @@ -582,8 +569,7 @@ class LogParser: if not parser(log, host, pid, subprog): self._dprint("[parser] ignoring %r log: %r" % (prog, log)) except AttributeError: - self._dprint( - "[parser] no log handler for \"{}\": {}".format(prog, log)) + self._dprint('[parser] no log handler for "{}": {}'.format(prog, log)) def process(self): """Process the log file. @@ -611,14 +597,21 @@ class Command(BaseCommand): def add_arguments(self, parser): """Add extra arguments to command line.""" parser.add_argument( - "--logfile", default=None, - help="postfix log in syslog format", metavar="FILE") + "--logfile", + default=None, + help="postfix log in syslog format", + metavar="FILE", + ) parser.add_argument( - "--verbose", default=False, action="store_true", - dest="verbose", help="Set verbose mode") + "--verbose", + default=False, + action="store_true", + dest="verbose", + help="Set verbose mode", + ) parser.add_argument( - "--debug", default=False, action="store_true", - help="Set debug mode") + "--debug", default=False, action="store_true", help="Set debug mode" + ) def can_start(self): """Check if another process is already running or not.""" @@ -643,11 +636,10 @@ class Command(BaseCommand): sys.exit(2) if options["logfile"] is None: options["logfile"] = param_tools.get_global_parameter( - "logfile", app="maillog") - greylist = param_tools.get_global_parameter( - "greylist", raise_exception=False) + "logfile", app="maillog" + ) + greylist = param_tools.get_global_parameter("greylist", raise_exception=False) p = LogParser( - options, param_tools.get_global_parameter("rrd_rootdir"), - None, greylist + options, param_tools.get_global_parameter("rrd_rootdir"), None, greylist ) p.process() diff --git a/modoboa/maillog/management/commands/update_statistics.py b/modoboa/maillog/management/commands/update_statistics.py index d828acab9..031ddbc51 100644 --- a/modoboa/maillog/management/commands/update_statistics.py +++ b/modoboa/maillog/management/commands/update_statistics.py @@ -20,8 +20,10 @@ class Command(BaseCommand): def add_arguments(self, parser): """Add extra arguments to command line.""" parser.add_argument( - "--rebuild", action="store_true", - help="Rebuild statistics from the begining") + "--rebuild", + action="store_true", + help="Rebuild statistics from the begining", + ) def _create_new_accounts_rrd_file(self, fname, start): """Create RRD file.""" @@ -29,15 +31,12 @@ class Command(BaseCommand): ds_name = "new_accounts" params = ["DS:{}:ABSOLUTE:{}:0:U".format(ds_name, step * 2)] params += [ - "RRA:AVERAGE:0.5:1:48", # 48 hours with a 1h granularity - "RRA:AVERAGE:0.5:24:31", # 31 days with a 1d granularity - "RRA:AVERAGE:0.5:168:52", # 52 weeks with a 1w granularity + "RRA:AVERAGE:0.5:1:48", # 48 hours with a 1h granularity + "RRA:AVERAGE:0.5:24:31", # 31 days with a 1d granularity + "RRA:AVERAGE:0.5:168:52", # 52 weeks with a 1w granularity "RRA:AVERAGE:0.5:5208:24", # 24 months with a 1m granularity ] - rrdtool.create( - str(fname), "--start", str(start), "--step", str(step), - *params - ) + rrdtool.create(str(fname), "--start", str(start), "--step", str(step), *params) def update_account_creation_stats(self, rebuild=False): """Look for newly created accounts.""" @@ -47,15 +46,16 @@ class Command(BaseCommand): end = timezone.now().replace(minute=0, second=0, microsecond=0) start = end - relativedelta(hours=1) new_accounts = core_models.User.objects.filter( - date_joined__gte=start, date_joined__lt=end).count() - data.append( - "{}:{}".format(int(end.strftime("%s")), new_accounts * 60)) + date_joined__gte=start, date_joined__lt=end + ).count() + data.append("{}:{}".format(int(end.strftime("%s")), new_accounts * 60)) else: existing_stats = {} start = None for user in core_models.User.objects.all().order_by("date_joined"): hour = user.date_joined.replace( - minute=0, second=0, microsecond=0) + relativedelta(hours=1) + minute=0, second=0, microsecond=0 + ) + relativedelta(hours=1) if start is None: start = hour hour = int(hour.strftime("%s")) @@ -63,14 +63,14 @@ class Command(BaseCommand): existing_stats[hour] = 0 existing_stats[hour] += 1 end = timezone.now().replace( - minute=0, second=0, microsecond=0) + relativedelta(hours=1) + minute=0, second=0, microsecond=0 + ) + relativedelta(hours=1) if os.path.exists(db_path): os.unlink(db_path) hour = start while hour <= end: new_accounts = existing_stats.get(int(hour.strftime("%s")), 0) - data.append("{}:{}".format( - int(hour.strftime("%s")), new_accounts * 60)) + data.append("{}:{}".format(int(hour.strftime("%s")), new_accounts * 60)) hour += relativedelta(hours=1) if not os.path.exists(db_path): self._create_new_accounts_rrd_file( diff --git a/modoboa/maillog/migrations/0001_initial.py b/modoboa/maillog/migrations/0001_initial.py index 92e1122eb..6ff2e93c4 100644 --- a/modoboa/maillog/migrations/0001_initial.py +++ b/modoboa/maillog/migrations/0001_initial.py @@ -9,26 +9,50 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('admin', '0017_alarm'), + ("admin", "0017_alarm"), ] operations = [ migrations.CreateModel( - name='Maillog', + name="Maillog", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('queue_id', models.CharField(max_length=50)), - ('date', models.DateTimeField()), - ('sender', models.EmailField(max_length=254)), - ('rcpt', models.EmailField(max_length=254)), - ('original_rcpt', models.EmailField(max_length=254, null=True)), - ('size', models.PositiveIntegerField()), - ('status', models.CharField(max_length=15)), - ('from_domain', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent_messages_log', to='admin.Domain')), - ('to_domain', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='recv_messages_log', to='admin.Domain')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("queue_id", models.CharField(max_length=50)), + ("date", models.DateTimeField()), + ("sender", models.EmailField(max_length=254)), + ("rcpt", models.EmailField(max_length=254)), + ("original_rcpt", models.EmailField(max_length=254, null=True)), + ("size", models.PositiveIntegerField()), + ("status", models.CharField(max_length=15)), + ( + "from_domain", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="sent_messages_log", + to="admin.Domain", + ), + ), + ( + "to_domain", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="recv_messages_log", + to="admin.Domain", + ), + ), ], options={ - 'ordering': ['date'], + "ordering": ["date"], }, ), ] diff --git a/modoboa/maillog/migrations/0002_auto_20200916_1346.py b/modoboa/maillog/migrations/0002_auto_20200916_1346.py index 7d6f6a821..34d451c6f 100644 --- a/modoboa/maillog/migrations/0002_auto_20200916_1346.py +++ b/modoboa/maillog/migrations/0002_auto_20200916_1346.py @@ -28,7 +28,7 @@ def backwards(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('maillog', '0001_initial'), + ("maillog", "0001_initial"), ] operations = [ diff --git a/modoboa/maillog/migrations/0003_auto_20211108_1652.py b/modoboa/maillog/migrations/0003_auto_20211108_1652.py index ba7f3a474..a36dc60dc 100644 --- a/modoboa/maillog/migrations/0003_auto_20211108_1652.py +++ b/modoboa/maillog/migrations/0003_auto_20211108_1652.py @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('maillog', '0002_auto_20200916_1346'), + ("maillog", "0002_auto_20200916_1346"), ] operations = [ migrations.AlterField( - model_name='maillog', - name='date', + model_name="maillog", + name="date", field=models.DateTimeField(db_index=True), ), ] diff --git a/modoboa/maillog/models.py b/modoboa/maillog/models.py index 689add82d..339a5fe45 100644 --- a/modoboa/maillog/models.py +++ b/modoboa/maillog/models.py @@ -15,13 +15,17 @@ class Maillog(models.Model): status = models.CharField(max_length=15) from_domain = models.ForeignKey( - "admin.Domain", on_delete=models.SET_NULL, null=True, - related_name="sent_messages_log" + "admin.Domain", + on_delete=models.SET_NULL, + null=True, + related_name="sent_messages_log", ) to_domain = models.ForeignKey( - "admin.Domain", on_delete=models.SET_NULL, null=True, - related_name="recv_messages_log" + "admin.Domain", + on_delete=models.SET_NULL, + null=True, + related_name="recv_messages_log", ) class Meta: - ordering = ['date'] + ordering = ["date"] diff --git a/modoboa/maillog/parser.py b/modoboa/maillog/parser.py index 4541247ec..093b91e56 100644 --- a/modoboa/maillog/parser.py +++ b/modoboa/maillog/parser.py @@ -33,10 +33,9 @@ class MaillogParser: # set up regular expression self._date_expressions = [ r"(?P\w+)\s+(?P\d+)\s+(?P\d+):(?P\d+):(?P\d+)(?P.*)", # noqa - r"(?P\d+)-(?P\d+)-(?P\d+)T(?P\d+):(?P\d+):(?P\d+)\.\d+\+\d+:\d+(?P.*)" # noqa + r"(?P\d+)-(?P\d+)-(?P\d+)T(?P\d+):(?P\d+):(?P\d+)\.\d+\+\d+:\d+(?P.*)", # noqa ] - self._date_expressions = [ - re.compile(v) for v in self._date_expressions] + self._date_expressions = [re.compile(v) for v in self._date_expressions] self.date_expr = None self._regex = { "line": r"\s+([-\w\.]+)\s+(\w+)/?(\w*)[[](\d+)[]]:\s+(.*)", @@ -51,7 +50,7 @@ class MaillogParser: self._srs_regex = { "detect_srs": "^SRS[01][+-=]", "reverse_srs0": r"^SRS0[+-=]\S+=\S{2}=(\S+)=(\S+)\@\S+$", - "reverse_srs1": r"^SRS1[+-=]\S+=\S+==\S+=\S{2}=(\S+)=(\S+)\@\S+$" + "reverse_srs1": r"^SRS1[+-=]\S+=\S+==\S+=\S{2}=(\S+)=(\S+)\@\S+$", } self._srs_regex = { k: re.compile(v, re.IGNORECASE) for k, v in self._srs_regex.items() @@ -97,10 +96,7 @@ class MaillogParser: :param month: the month of the current record beeing parsed :return: an integer """ - month = ( - time.strptime(month, "%b").tm_mon if not month.isdigit() - else int(month) - ) + month = time.strptime(month, "%b").tm_mon if not month.isdigit() else int(month) if self.curmonth == 1 and month != self.curmonth: return self.__year - 1 return self.__year @@ -143,7 +139,7 @@ class MaillogParser: return match.group("eol") def is_srs_forward(self, mail_address): - """ Check if mail address has been mangled by SRS + """Check if mail address has been mangled by SRS Sender Rewriting Scheme (SRS) modifies mail adresses so that they always end in a local domain. @@ -154,7 +150,7 @@ class MaillogParser: return self._srs_regex["detect_srs"].match(mail_address) is not None def reverse_srs(self, mail_address): - """ Try to unwind a mail address rewritten by SRS + """Try to unwind a mail address rewritten by SRS Sender Rewriting Scheme (SRS) modifies mail adresses so that they always end in a local domain. Common Postfix implementations of SRS @@ -164,8 +160,7 @@ class MaillogParser: :return a str """ m = self._srs_regex["reverse_srs0"].match(mail_address) - m = self._srs_regex["reverse_srs1"].match(mail_address) \ - if m is None else m + m = self._srs_regex["reverse_srs1"].match(mail_address) if m is None else m if m is not None: return "%s@%s" % m.group(2, 1) @@ -176,13 +171,13 @@ class MaillogParser: pass def new_message_processed( - self, queue_id, msg_status, from_domain, to_domain, msg_to, - msg_orig_to): + self, queue_id, msg_status, from_domain, to_domain, msg_to, msg_orig_to + ): """Store new message in local database.""" pass def _parse_postfix(self, log, host, pid, subprog): - """ Parse a log entry generated by Postfix. + """Parse a log entry generated by Postfix. :param str log: logged message :param str host: hostname @@ -200,13 +195,10 @@ class MaillogParser: m = self._regex["reject"].match(msg) dom = m.group(1) if m is not None else None if dom in self.domains: - condition = ( - self.greylist and ( - "Greylisted" in msg or - (subprog == "postscreen" and " 450 " in msg)) + condition = self.greylist and ( + "Greylisted" in msg or (subprog == "postscreen" and " 450 " in msg) ) - self.new_domain_event( - dom, "greylist" if condition else "reject") + self.new_domain_event(dom, "greylist" if condition else "reject") return True # Message acknowledged. @@ -220,7 +212,7 @@ class MaillogParser: if m is not None: self.workdict[queue_id] = { "from": self.reverse_srs(m.group(1)), - "size": int(m.group(2)) + "size": int(m.group(2)), } return True @@ -230,8 +222,9 @@ class MaillogParser: return False (msg_to, msg_status) = m.groups() if queue_id not in self.workdict: - self._dprint("[parser] inconsistent mail (%s: %s), skipping" - % (queue_id, msg_to)) + self._dprint( + "[parser] inconsistent mail (%s: %s), skipping" % (queue_id, msg_to) + ) return True # orig_to is optional. @@ -241,15 +234,11 @@ class MaillogParser: # Handle local "from" domains. from_domain = split_mailbox(self.workdict[queue_id]["from"])[1] if from_domain is not None and from_domain in self.domains: - self.new_domain_event( - from_domain, "sent", self.workdict[queue_id]["size"]) + self.new_domain_event(from_domain, "sent", self.workdict[queue_id]["size"]) # Handle local "to" domains. to_domain = None - condition = ( - msg_orig_to is not None and - not self.is_srs_forward(msg_orig_to) - ) + condition = msg_orig_to is not None and not self.is_srs_forward(msg_orig_to) if condition: to_domain = split_mailbox(msg_orig_to)[1] if to_domain is None: @@ -257,12 +246,12 @@ class MaillogParser: if msg_status == "sent": msg_status = "recv" - self.new_domain_event( - to_domain, "recv", self.workdict[queue_id]["size"]) + self.new_domain_event(to_domain, "recv", self.workdict[queue_id]["size"]) # Store log entry self.new_message_processed( - queue_id, msg_status, from_domain, to_domain, msg_to, msg_orig_to) + queue_id, msg_status, from_domain, to_domain, msg_to, msg_orig_to + ) return True @@ -284,8 +273,7 @@ class MaillogParser: if not parser(log, host, pid, subprog): self._dprint("[parser] ignoring %r log: %r" % (prog, log)) except AttributeError: - self._dprint( - "[parser] no log handler for \"%r\": %r".format(prog, log)) + self._dprint('[parser] no log handler for "%r": %r'.format(prog, log)) def parse(self, logfile): """Process the log file.""" diff --git a/modoboa/maillog/signals.py b/modoboa/maillog/signals.py index 28b476cae..227f0295b 100644 --- a/modoboa/maillog/signals.py +++ b/modoboa/maillog/signals.py @@ -2,4 +2,4 @@ import django.dispatch -get_graph_sets = django.dispatch.Signal() # Provides user +get_graph_sets = django.dispatch.Signal() # Provides user diff --git a/modoboa/maillog/tests/test_views.py b/modoboa/maillog/tests/test_views.py index 1610bb724..4da688cc9 100644 --- a/modoboa/maillog/tests/test_views.py +++ b/modoboa/maillog/tests/test_views.py @@ -31,11 +31,9 @@ class RunCommandsMixin(object): def run_logparser(self): """Run logparser command.""" - path = os.path.join( - os.path.dirname(__file__), "mail.log") + path = os.path.join(os.path.dirname(__file__), "mail.log") with open(path) as fp: - content = fp.read() % { - "day": datetime.date.today().strftime("%b %d")} + content = fp.read() % {"day": datetime.date.today().strftime("%b %d")} path = os.path.join(self.workdir, "mail.log") with open(path, "w") as fp: fp.write(content) @@ -84,7 +82,8 @@ class ViewsTestCase(RunCommandsMixin, ModoTestCase): self.assertIn("averagetraffic", response["graphs"]) for period in ["week", "month", "year"]: response = self.ajax_get( - "{}?gset=mailtraffic&period={}".format(url, period)) + "{}?gset=mailtraffic&period={}".format(url, period) + ) self.assertIn("averagetraffic", response["graphs"]) self.assertEqual(response["period_name"], period) @@ -93,15 +92,14 @@ class ViewsTestCase(RunCommandsMixin, ModoTestCase): start = "{} 11:00:00".format(today) end = "{} 11:40:00".format(today) response = self.ajax_get( - "{}?gset=mailtraffic&period=custom&start={}&end={}".format( - url, start, end) + "{}?gset=mailtraffic&period=custom&start={}&end={}".format(url, start, end) ) self.assertIn("averagetraffic", response["graphs"]) # unknown domain response = self.ajax_get( - "{}?gset=mailtraffic&searchquery=unknown.com".format(url), - status=400) + "{}?gset=mailtraffic&searchquery=unknown.com".format(url), status=400 + ) # check with greylist enabled self.set_global_parameter("greylist", True) @@ -113,8 +111,7 @@ class ViewsTestCase(RunCommandsMixin, ModoTestCase): self.run_update_statistics(rebuild=True) url = reverse("maillog:graph_list") response = self.ajax_get("{}?gset=accountgraphicset".format(url)) - data = ( - response["graphs"]["accountcreationgraphic"]["series"][0]["data"]) + data = response["graphs"]["accountcreationgraphic"]["series"][0]["data"] self.assertEqual(data[-1]["y"], 5.0) def test_graphs_as_domainadmin(self): @@ -128,8 +125,7 @@ class ViewsTestCase(RunCommandsMixin, ModoTestCase): response = self.ajax_get("{}&searchquery=test.com".format(url)) self.assertIn("averagetraffic", response["graphs"]) - response = self.ajax_get( - "{}&searchquery=test2.com".format(url), status=403) + response = self.ajax_get("{}&searchquery=test2.com".format(url), status=403) def test_get_domain_list(self): """Test get_domain_list view.""" diff --git a/modoboa/maillog/views.py b/modoboa/maillog/views.py index b2dcedb0e..15264d9b6 100644 --- a/modoboa/maillog/views.py +++ b/modoboa/maillog/views.py @@ -8,14 +8,14 @@ import time from django.shortcuts import render from django.utils.translation import gettext as _ from django.contrib.auth.decorators import ( - login_required, user_passes_test, permission_required + login_required, + user_passes_test, + permission_required, ) from modoboa.admin.models import Domain from modoboa.lib.exceptions import BadRequest, NotFound -from modoboa.lib.web_utils import ( - render_to_json_response -) +from modoboa.lib.web_utils import render_to_json_response from . import signals from .lib import date_to_timestamp @@ -33,22 +33,25 @@ def index(request): raise NotFound(_("No statistics available")) graph_sets = {} - for result in signals.get_graph_sets.send( - sender="index", user=request.user): + for result in signals.get_graph_sets.send(sender="index", user=request.user): graph_sets.update(result[1]) periods = [ {"name": "day", "label": _("Day")}, {"name": "week", "label": _("Week")}, {"name": "month", "label": _("Month")}, {"name": "year", "label": _("Year")}, - {"name": "custom", "label": _("Custom")} + {"name": "custom", "label": _("Custom")}, ] - return render(request, 'maillog/index.html', { - "periods": periods, - "selection": "stats", - "deflocation": deflocation, - "graph_sets": graph_sets - }) + return render( + request, + "maillog/index.html", + { + "periods": periods, + "selection": "stats", + "deflocation": deflocation, + "graph_sets": graph_sets, + }, + ) @login_required @@ -57,8 +60,7 @@ def graphs(request): gset = request.GET.get("gset", None) searchq = request.GET.get("searchquery", None) graph_sets = {} - for result in signals.get_graph_sets.send( - sender="index", user=request.user): + for result in signals.get_graph_sets.send(sender="index", user=request.user): graph_sets.update(result[1]) if gset not in graph_sets: raise NotFound(_("Unknown graphic set")) @@ -74,8 +76,8 @@ def graphs(request): raise BadRequest(_("Bad custom period")) start = request.GET["start"] end = request.GET["end"] - expr = re.compile(r'[:\- ]') - period_name = "%s_%s" % (expr.sub('', start), expr.sub('', end)) + expr = re.compile(r"[:\- ]") + period_name = "%s_%s" % (expr.sub("", start), expr.sub("", end)) start = date_to_timestamp(expr.split(start)) end = date_to_timestamp(expr.split(end)) else: diff --git a/modoboa/parameters/api/v2/tests.py b/modoboa/parameters/api/v2/tests.py index 34806feb1..e43efd549 100644 --- a/modoboa/parameters/api/v2/tests.py +++ b/modoboa/parameters/api/v2/tests.py @@ -43,7 +43,7 @@ class ParametersAPITestCase(ModoAPITestCase): "deflt_domain_domain_admins_limit": 0, "deflt_domain_domain_aliases_limit": 0, "deflt_domain_mailboxes_limit": 0, - "deflt_domain_mailbox_aliases_limit": 0 + "deflt_domain_mailbox_aliases_limit": 0, } url = reverse("v2:parameter-detail", args=["limits"]) resp = self.client.put(url, data, format="json") @@ -55,7 +55,7 @@ class ParametersAPITestCase(ModoAPITestCase): "default_mailbox_quota": -1, "valid_mxs": "mail.test.com", "custom_dns_server": "", - "dkim_keys_storage_dir": "/wrong" + "dkim_keys_storage_dir": "/wrong", } url = reverse("v2:parameter-detail", args=["admin"]) resp = self.client.put(url, data, format="json") @@ -63,40 +63,32 @@ class ParametersAPITestCase(ModoAPITestCase): errors = resp.json() self.assertEqual( errors["valid_mxs"], - ["This field only allows valid IP addresses (or networks)"] - ) - self.assertEqual( - errors["default_domain_quota"], ["Must be a positive integer"] + ["This field only allows valid IP addresses (or networks)"], ) + self.assertEqual(errors["default_domain_quota"], ["Must be a positive integer"]) self.assertEqual( errors["default_mailbox_quota"], ["Must be a positive integer"] ) - self.assertEqual( - errors["dkim_keys_storage_dir"], ["Directory not found."] - ) + self.assertEqual(errors["dkim_keys_storage_dir"], ["Directory not found."]) # Try to set dkim storage dir to a non-writable directory. - data = { - "dkim_keys_storage_dir": "/root" - } + data = {"dkim_keys_storage_dir": "/root"} url = reverse("v2:parameter-detail", args=["admin"]) resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 400) errors = resp.json() - self.assertEqual( - errors["dkim_keys_storage_dir"], ["Directory non-writable"] - ) + self.assertEqual(errors["dkim_keys_storage_dir"], ["Directory non-writable"]) data = { "enable_mx_checks": True, "domains_must_have_authorized_mx": True, "valid_mxs": "", "custom_dns_server": "", - "dkim_keys_storage_dir": "" + "dkim_keys_storage_dir": "", } resp = self.client.put(url, data, format="json") self.assertEqual(resp.status_code, 400) self.assertEqual( resp.json()["valid_mxs"], - ["Define at least one authorized network / address"] + ["Define at least one authorized network / address"], ) diff --git a/modoboa/parameters/api/v2/urls.py b/modoboa/parameters/api/v2/urls.py index 25ec41c04..418037966 100644 --- a/modoboa/parameters/api/v2/urls.py +++ b/modoboa/parameters/api/v2/urls.py @@ -6,6 +6,5 @@ from . import viewsets router = routers.SimpleRouter() -router.register( - r"parameters", viewsets.ParametersViewSet, basename="parameter") +router.register(r"parameters", viewsets.ParametersViewSet, basename="parameter") urlpatterns = router.urls diff --git a/modoboa/parameters/api/v2/viewsets.py b/modoboa/parameters/api/v2/viewsets.py index 3a670f02f..cb2d785fa 100644 --- a/modoboa/parameters/api/v2/viewsets.py +++ b/modoboa/parameters/api/v2/viewsets.py @@ -36,39 +36,41 @@ class ParametersViewSet(GetThrottleViewsetMixin, viewsets.ViewSet): @extend_schema( parameters=[ OpenApiParameter( - name='id', location=OpenApiParameter.PATH, - description='A registered application name', - type=str, required=True + name="id", + location=OpenApiParameter.PATH, + description="A registered application name", + type=str, + required=True, ), ], - responses=serializers.AppParametersSerializer + responses=serializers.AppParametersSerializer, ) def retrieve(self, request, pk: str): """Return all parameters for given app.""" parameters = request.localconfig.parameters.get_values_dict(pk) - serializer = tools.registry.get_serializer_class("global", pk)( - parameters) - result = serializers.AppParametersSerializer({ - "label": tools.registry.get_label("global", pk), - "params": serializer.data - }) + serializer = tools.registry.get_serializer_class("global", pk)(parameters) + result = serializers.AppParametersSerializer( + {"label": tools.registry.get_label("global", pk), "params": serializer.data} + ) return response.Response(result.data) @extend_schema( parameters=[ OpenApiParameter( - name='id', location=OpenApiParameter.PATH, - description='A registered application name', - type=str, required=True + name="id", + location=OpenApiParameter.PATH, + description="A registered application name", + type=str, + required=True, ), ] ) def update(self, request, pk: str): """Save parameters for given app.""" serializer = tools.registry.get_serializer_class("global", pk)( - data=request.data) + data=request.data + ) serializer.is_valid(raise_exception=True) - request.localconfig.parameters.set_values( - serializer.validated_data, app=pk) + request.localconfig.parameters.set_values(serializer.validated_data, app=pk) request.localconfig.save(update_fields=["_parameters"]) return response.Response() diff --git a/modoboa/parameters/forms.py b/modoboa/parameters/forms.py index f35949b25..49a40eba4 100644 --- a/modoboa/parameters/forms.py +++ b/modoboa/parameters/forms.py @@ -10,6 +10,7 @@ class GenericParametersForm(forms.Form): Each extension has the possibility to define global parameters. """ + app = None visibility_rules = None @@ -35,9 +36,7 @@ class GenericParametersForm(forms.Form): """Add visibility rules to this form.""" for key, rule in list(rules.items()): field, value = rule.split("=") - visibility = { - "field": "id_%s-%s" % (self.app, field), "value": value - } + visibility = {"field": "id_%s-%s" % (self.app, field), "value": value} self.visirules["%s-%s" % (self.app, key)] = visibility def _add_dynamic_fields(self): @@ -66,8 +65,8 @@ class AdminParametersForm(GenericParametersForm): def _load_initial_values(self): """Load form initial values from database.""" condition = ( - not db_utils.db_table_exists("core_localconfig") or - not self.localconfig) + not db_utils.db_table_exists("core_localconfig") or not self.localconfig + ) if condition: return values = self.localconfig.parameters.get_values(app=self.app) diff --git a/modoboa/parameters/tools.py b/modoboa/parameters/tools.py index ed9e99667..69629fbc5 100644 --- a/modoboa/parameters/tools.py +++ b/modoboa/parameters/tools.py @@ -18,8 +18,7 @@ class NotDefined(exceptions.ModoboaException): def __str__(self): if self.name is None: return "Application {} not registered".format(self.app) - return "Parameter {} not defined for app {}".format( - self.name, self.app) + return "Parameter {} not defined for app {}".format(self.name, self.app) class Registry(object): @@ -27,28 +26,24 @@ class Registry(object): def __init__(self): """Constructor.""" - self._registry = { - "global": {}, - "user": {} - } - self._registry2 = { - "global": {}, - "user": {} - } + self._registry = {"global": {}, "user": {}} + self._registry2 = {"global": {}, "user": {}} def add(self, level, formclass, label): """Add a new class containing parameters.""" self._registry[level][formclass.app] = { - "label": label, "formclass": formclass, "defaults": {} + "label": label, + "formclass": formclass, + "defaults": {}, } - def add2(self, level, app, label, structure, serializer_class, - is_extension=False): + def add2(self, level, app, label, structure, serializer_class, is_extension=False): """Add new parameters for given app and level.""" self._registry2[level][app] = { - "label": label, "structure": structure, + "label": label, + "structure": structure, "serializer_class": serializer_class, - "is_extension": is_extension + "is_extension": is_extension, } def _load_default_values(self, level): @@ -62,9 +57,14 @@ class Registry(object): def get_applications(self, level): """Return all applications registered for level.""" - result = [{"name": key, "label": value["label"], - "is_extension": value["is_extension"]} - for key, value in self._registry2[level].items()] + result = [ + { + "name": key, + "label": value["label"], + "is_extension": value["is_extension"], + } + for key, value in self._registry2[level].items() + ] return result def get_label(self, level, app): @@ -77,22 +77,21 @@ class Registry(object): if first_app in self._registry[level]: sorted_apps.append(first_app) sorted_apps += sorted( - ( - a for a in self._registry[level] - if a != first_app - ), - key=lambda a: self._registry[level][a]["label"] + (a for a in self._registry[level] if a != first_app), + key=lambda a: self._registry[level][a]["label"], ) result = [] for app in sorted_apps: data = self._registry[level][app] if not data["formclass"].has_access(**kwargs): continue - result.append({ - "app": app, - "label": data["label"], - "form": data["formclass"](*args, **kwargs) - }) + result.append( + { + "app": app, + "label": data["label"], + "form": data["formclass"](*args, **kwargs), + } + ) return result def get_serializer_class(self, level, app): @@ -112,7 +111,7 @@ class Registry(object): item = { "label": sconfig["label"], "display": sconfig.get("display", ""), - "parameters": [] + "parameters": [], } for name, config in list(sconfig["params"].items()): data = { @@ -217,7 +216,8 @@ class Manager: raise NotDefined(app, parameter) if app not in self._parameters: self._parameters[app] = copy.deepcopy( - registry.get_defaults(self._level, app)) + registry.get_defaults(self._level, app) + ) self._parameters[app][parameter] = value def set_values(self, values, app=None): @@ -228,13 +228,15 @@ class Manager: raise NotDefined(app) if app not in self._parameters: self._parameters[app] = copy.deepcopy( - registry.get_defaults(self._level, app)) + registry.get_defaults(self._level, app) + ) self._parameters[app].update(values) def get_localconfig(): """Retrieve current LocalConfig instance.""" from modoboa.core import models as core_models + request = signals.get_request() if request: return request.localconfig diff --git a/modoboa/pdfcredentials/api/v2/serializers.py b/modoboa/pdfcredentials/api/v2/serializers.py index 2832f8ca5..867608c7c 100644 --- a/modoboa/pdfcredentials/api/v2/serializers.py +++ b/modoboa/pdfcredentials/api/v2/serializers.py @@ -31,36 +31,34 @@ class PDFCredentialsSettingsSerializer(serializers.Serializer): title = serializers.CharField(default=_("Personal account information")) webpanel_url = serializers.URLField() custom_message = serializers.CharField( - required=False, allow_blank=True, allow_null=True) + required=False, allow_blank=True, allow_null=True + ) include_connection_settings = serializers.BooleanField(default=False) smtp_server_address = serializers.CharField() smtp_server_port = serializers.IntegerField(default=587) smtp_connection_security = serializers.ChoiceField( - choices=CONNECTION_SECURITY_MODES, default="starttls") + choices=CONNECTION_SECURITY_MODES, default="starttls" + ) imap_server_address = serializers.CharField() imap_server_port = serializers.IntegerField(default=143) imap_connection_security = serializers.ChoiceField( - choices=CONNECTION_SECURITY_MODES, default="starttls") + choices=CONNECTION_SECURITY_MODES, default="starttls" + ) def validate(self, data): """Check that directory exists.""" enabled_pdfcredentials = data.get("enabled_pdfcredentials", None) - condition = (enabled_pdfcredentials or - (enabled_pdfcredentials is None and - param_tools.get_global_parameter("enabled_pdfcredentials") - ) - ) + condition = enabled_pdfcredentials or ( + enabled_pdfcredentials is None + and param_tools.get_global_parameter("enabled_pdfcredentials") + ) if condition: storage_dir = data.get("storage_dir", None) if storage_dir is not None: if not os.path.isdir(storage_dir): - raise serializers.ValidationError( - _("Directory not found.") - ) + raise serializers.ValidationError(_("Directory not found.")) if not os.access(storage_dir, os.W_OK): - raise serializers.ValidationError( - _("Directory is not writable") - ) + raise serializers.ValidationError(_("Directory is not writable")) return data @@ -81,8 +79,7 @@ class GetAccountCredentialsSerializer(serializers.Serializer): fname = get_creds_filename(self.context["account"]) if not os.path.exists(fname): raise APIException( - _("No document available for this user"), - status.HTTP_400_BAD_REQUEST + _("No document available for this user"), status.HTTP_400_BAD_REQUEST ) self.context["content"] = decrypt_file(fname) if param_tools.get_global_parameter("delete_first_dl"): diff --git a/modoboa/pdfcredentials/api/v2/tests.py b/modoboa/pdfcredentials/api/v2/tests.py index 88cb981c3..c35c1b765 100644 --- a/modoboa/pdfcredentials/api/v2/tests.py +++ b/modoboa/pdfcredentials/api/v2/tests.py @@ -28,9 +28,7 @@ class PDFCredentialViewTestCase(ModoAPITestCase): data = { "username": f"{username}", "role": "SimpleUsers", - "mailbox": { - "use_domain_quota": True - }, + "mailbox": {"use_domain_quota": True}, "password": "Toto12345", "language": "fr", } @@ -78,9 +76,7 @@ class PDFCredentialViewTestCase(ModoAPITestCase): self.assertTrue(assert_action_in_response) # Try to download the file - response = self.client.get( - reverse("v2:get-credentials", - args=[account.pk])) + response = self.client.get(reverse("v2:get-credentials", args=[account.pk])) self.assertEqual(response.status_code, 200) self.assertEqual(response["Content-Type"], "application/pdf") @@ -88,22 +84,21 @@ class PDFCredentialViewTestCase(ModoAPITestCase): self.assertFalse(os.path.exists(fname)) # Try to download a second time - response = self.client.get( - reverse("v2:get-credentials", - args=[account.pk])) - self.assertEqual(response.json()["detail"], - "No document available for this user") + response = self.client.get(reverse("v2:get-credentials", args=[account.pk])) + self.assertEqual( + response.json()["detail"], "No document available for this user" + ) # Update account values.update({"language": "en"}) self.client.patch( - reverse("v2:account-detail", args=[account.pk]), values, format='json' + reverse("v2:account-detail", args=[account.pk]), values, format="json" ) self.assertFalse(os.path.exists(fname)) self.set_global_parameter("generate_at_creation", False) self.client.patch( - reverse("v2:account-detail", args=[account.pk]), values, format='json' + reverse("v2:account-detail", args=[account.pk]), values, format="json" ) self.assertTrue(os.path.exists(fname)) @@ -146,9 +141,7 @@ class PDFCredentialViewTestCase(ModoAPITestCase): account = core_models.User.objects.get(username=values["username"]) # Try to download the file - response = self.client.get( - reverse("v2:get-credentials", - args=[account.pk])) + response = self.client.get(reverse("v2:get-credentials", args=[account.pk])) self.assertEqual(response.status_code, 200) self.assertEqual(response["Content-Type"], "application/pdf") @@ -171,10 +164,10 @@ class PDFCredentialViewTestCase(ModoAPITestCase): self.set_global_parameter("enabled_pdfcredentials", False) url = reverse("v2:parameter-detail", args=["pdfcredentials"]) data = { - "webpanel_url": "http://localhost", - "smtp_server_address": "mail.localhost", - "imap_server_address": "mail.localhost", - } + "webpanel_url": "http://localhost", + "smtp_server_address": "mail.localhost", + "imap_server_address": "mail.localhost", + } # Test that we can't activate it without providing a valid storage dir. data["enabled_pdfcredentials"] = True diff --git a/modoboa/pdfcredentials/api/v2/urls.py b/modoboa/pdfcredentials/api/v2/urls.py index 4b62a3ea0..6fb68a803 100644 --- a/modoboa/pdfcredentials/api/v2/urls.py +++ b/modoboa/pdfcredentials/api/v2/urls.py @@ -6,6 +6,9 @@ from django.urls import path from . import views urlpatterns = [ - path('credentials//', views.PDFCredentialView.as_view(), - name="get-credentials"), + path( + "credentials//", + views.PDFCredentialView.as_view(), + name="get-credentials", + ), ] diff --git a/modoboa/pdfcredentials/api/v2/views.py b/modoboa/pdfcredentials/api/v2/views.py index d7ddc1f03..15f7fc581 100644 --- a/modoboa/pdfcredentials/api/v2/views.py +++ b/modoboa/pdfcredentials/api/v2/views.py @@ -19,7 +19,8 @@ from ...lib import rfc_6266_content_disposition class PDFCredentialView(APIView): permission_classes = ( - permissions.IsAuthenticated, permissions.DjangoModelPermissions, + permissions.IsAuthenticated, + permissions.DjangoModelPermissions, ) serializer_class = GetAccountCredentialsSerializer @@ -33,7 +34,8 @@ class PDFCredentialView(APIView): """View to download a document.""" data = {"account_id": kwargs["account_id"]} serializer = GetAccountCredentialsSerializer( - data=data, context={'request': request}) + data=data, context={"request": request} + ) serializer.is_valid(raise_exception=True) serializer.save() diff --git a/modoboa/pdfcredentials/app_settings.py b/modoboa/pdfcredentials/app_settings.py index 54ec06f10..e6f81a745 100644 --- a/modoboa/pdfcredentials/app_settings.py +++ b/modoboa/pdfcredentials/app_settings.py @@ -15,104 +15,176 @@ from modoboa.parameters import forms as param_forms from . import constants -PDF_CREDENTIALS_PARAMETERS_STRUCT = collections.OrderedDict([ - ("general", { - "label": _("General"), - "params": collections.OrderedDict([ - ("enabled_pdfcredentials", { - "label": _("Enable PDF credentials"), - "help_text": _( - "Enabled PDF credentials document generation on account creation."), - }) - ]) - }), - ("docstore", { - "label": _("Documents storage"), - "params": collections.OrderedDict([ - ("storage_dir", { - "label": _("Directory to save documents into"), - "help_text": _( - "Path to a directory where PDF documents will be saved"), - }), - ]) - }), - ("security", { - "label": _("Security options"), - "params": collections.OrderedDict([ - ("delete_first_dl", { - "label": _("Delete documents after the first download"), - "help_text": _( - "Automatically delete a document just after its first download " - "from this interface" - ) - }), - ("generate_at_creation", { - "label": _("Generate documents only at account creation"), - "help_text": _( - "Generate a new document only when a new account is created. " - "If set to no, a new document will be created each time a " - "password is updated." - ) - }) - ]) - }), - ("customization", { - "label": _("Customization options"), - "params": collections.OrderedDict([ - ("title", { - "label": _("Title"), - "help_text": _("The document's title") - }), - ("webpanel_url", { - "label": _("Web panel url"), - "help_text": _("URL of the Modoboa web panel") - }), - ("custom_message", { - "label": _("Custom message"), - "help_text": _( - "A custom message that will appear at the end of documents" - ) - }), - ("include_connection_settings", { - "label": _("Include mail client connection settings"), - "help_text": _( - "Include required SMTP and IMAP connection information to " - "configure a mail client, a tablet or a phone" - ) - }), - ("smtp_server_address", { - "label": _("SMTP server address"), - "display": "include_connection_settings=True", - "help_text": _("Address of the SMTP server (hostname or IP)") - }), - ("smtp_server_port", { - "label": _("SMTP server port"), - "display": "include_connection_settings=true", - "help_text": _("Listening port of the SMTP server") - }), - ("smtp_connection_security", { - "label": _("SMTP connection security"), - "display": "include_connection_settings=true", - "help_text": _("Connection security mechanism") - }), - ("imap_server_address", { - "label": _("IMAP server address"), - "display": "include_connection_settings=true", - "help_text": _("Address of the IMAP server (hostname or IP)") - }), - ("imap_server_port", { - "label": _("IMAP server port"), - "display": "include_connection_settings=true", - "help_text": _("Listening port of the IMAP server") - }), - ("imap_connection_security", { - "label": _("IMAP connection security"), - "display": "include_connection_settings=true", - "help_text": _("Connection security mechanism") - }) - ]) - }) -]) +PDF_CREDENTIALS_PARAMETERS_STRUCT = collections.OrderedDict( + [ + ( + "general", + { + "label": _("General"), + "params": collections.OrderedDict( + [ + ( + "enabled_pdfcredentials", + { + "label": _("Enable PDF credentials"), + "help_text": _( + "Enabled PDF credentials document generation on account creation." + ), + }, + ) + ] + ), + }, + ), + ( + "docstore", + { + "label": _("Documents storage"), + "params": collections.OrderedDict( + [ + ( + "storage_dir", + { + "label": _("Directory to save documents into"), + "help_text": _( + "Path to a directory where PDF documents will be saved" + ), + }, + ), + ] + ), + }, + ), + ( + "security", + { + "label": _("Security options"), + "params": collections.OrderedDict( + [ + ( + "delete_first_dl", + { + "label": _("Delete documents after the first download"), + "help_text": _( + "Automatically delete a document just after its first download " + "from this interface" + ), + }, + ), + ( + "generate_at_creation", + { + "label": _( + "Generate documents only at account creation" + ), + "help_text": _( + "Generate a new document only when a new account is created. " + "If set to no, a new document will be created each time a " + "password is updated." + ), + }, + ), + ] + ), + }, + ), + ( + "customization", + { + "label": _("Customization options"), + "params": collections.OrderedDict( + [ + ( + "title", + { + "label": _("Title"), + "help_text": _("The document's title"), + }, + ), + ( + "webpanel_url", + { + "label": _("Web panel url"), + "help_text": _("URL of the Modoboa web panel"), + }, + ), + ( + "custom_message", + { + "label": _("Custom message"), + "help_text": _( + "A custom message that will appear at the end of documents" + ), + }, + ), + ( + "include_connection_settings", + { + "label": _("Include mail client connection settings"), + "help_text": _( + "Include required SMTP and IMAP connection information to " + "configure a mail client, a tablet or a phone" + ), + }, + ), + ( + "smtp_server_address", + { + "label": _("SMTP server address"), + "display": "include_connection_settings=True", + "help_text": _( + "Address of the SMTP server (hostname or IP)" + ), + }, + ), + ( + "smtp_server_port", + { + "label": _("SMTP server port"), + "display": "include_connection_settings=true", + "help_text": _("Listening port of the SMTP server"), + }, + ), + ( + "smtp_connection_security", + { + "label": _("SMTP connection security"), + "display": "include_connection_settings=true", + "help_text": _("Connection security mechanism"), + }, + ), + ( + "imap_server_address", + { + "label": _("IMAP server address"), + "display": "include_connection_settings=true", + "help_text": _( + "Address of the IMAP server (hostname or IP)" + ), + }, + ), + ( + "imap_server_port", + { + "label": _("IMAP server port"), + "display": "include_connection_settings=true", + "help_text": _("Listening port of the IMAP server"), + }, + ), + ( + "imap_connection_security", + { + "label": _("IMAP connection security"), + "display": "include_connection_settings=true", + "help_text": _("Connection security mechanism"), + }, + ), + ] + ), + }, + ), + ] +) class ParametersForm(param_forms.AdminParametersForm): @@ -125,7 +197,7 @@ class ParametersForm(param_forms.AdminParametersForm): enabled_pdfcredentials = form_utils.YesNoField( label=_("Enable PDF credentials"), initial=True, - help_text=_("Enabled PDF credentials document generation on account creation.") + help_text=_("Enabled PDF credentials document generation on account creation."), ) docstore = form_utils.SeparatorField(label=_("Documents storage")) @@ -144,7 +216,7 @@ class ParametersForm(param_forms.AdminParametersForm): help_text=_( "Automatically delete a document just after its first download " "from this interface" - ) + ), ) generate_at_creation = form_utils.YesNoField( @@ -154,7 +226,7 @@ class ParametersForm(param_forms.AdminParametersForm): "Generate a new document only when a new account is created. " "If set to no, a new document will be created each time a " "password is updated." - ) + ), ) customization = form_utils.SeparatorField(label=_("Customization options")) @@ -162,19 +234,17 @@ class ParametersForm(param_forms.AdminParametersForm): title = forms.CharField( label=_("Title"), initial=_("Personal account information"), - help_text=_("The document's title") + help_text=_("The document's title"), ) webpanel_url = forms.URLField( - label=_("Web panel url"), - help_text=_("URL of the Modoboa web panel") + label=_("Web panel url"), help_text=_("URL of the Modoboa web panel") ) custom_message = forms.CharField( label=_("Custom message"), - help_text=_( - "A custom message that will appear at the end of documents"), - required=False + help_text=_("A custom message that will appear at the end of documents"), + required=False, ) include_connection_settings = form_utils.YesNoField( @@ -183,43 +253,43 @@ class ParametersForm(param_forms.AdminParametersForm): help_text=_( "Include required SMTP and IMAP connection information to " "configure a mail client, a tablet or a phone" - ) + ), ) smtp_server_address = forms.CharField( label=_("SMTP server address"), - help_text=_("Address of the SMTP server (hostname or IP)") + help_text=_("Address of the SMTP server (hostname or IP)"), ) smtp_server_port = forms.IntegerField( label=_("SMTP server port"), initial=587, - help_text=_("Listening port of the SMTP server") + help_text=_("Listening port of the SMTP server"), ) smtp_connection_security = forms.ChoiceField( label=_("SMTP connection security"), choices=constants.CONNECTION_SECURITY_MODES, initial="starttls", - help_text=_("Connection security mechanism") + help_text=_("Connection security mechanism"), ) imap_server_address = forms.CharField( label=_("IMAP server address"), - help_text=_("Address of the IMAP server (hostname or IP)") + help_text=_("Address of the IMAP server (hostname or IP)"), ) imap_server_port = forms.IntegerField( label=_("IMAP server port"), initial=143, - help_text=_("Listening port of the IMAP server") + help_text=_("Listening port of the IMAP server"), ) imap_connection_security = forms.ChoiceField( label=_("IMAP connection security"), choices=constants.CONNECTION_SECURITY_MODES, initial="starttls", - help_text=_("Connection security mechanism") + help_text=_("Connection security mechanism"), ) visibility_rules = { diff --git a/modoboa/pdfcredentials/apps.py b/modoboa/pdfcredentials/apps.py index cb47c6bb0..45439ef86 100644 --- a/modoboa/pdfcredentials/apps.py +++ b/modoboa/pdfcredentials/apps.py @@ -16,12 +16,16 @@ def load_pdfcredential_settings(): from .api.v2 import serializers param_tools.registry.add( - "global", app_settings.ParametersForm, gettext_lazy("PDF credentials")) + "global", app_settings.ParametersForm, gettext_lazy("PDF credentials") + ) param_tools.registry.add2( - "global", "pdfcredentials", gettext_lazy("PDF Credentials"), + "global", + "pdfcredentials", + gettext_lazy("PDF Credentials"), app_settings.PDF_CREDENTIALS_PARAMETERS_STRUCT, serializers.PDFCredentialsSettingsSerializer, - True) + True, + ) class PDFCredentialsConfig(AppConfig): diff --git a/modoboa/pdfcredentials/constants.py b/modoboa/pdfcredentials/constants.py index 0dd979ccc..e3e7511b6 100644 --- a/modoboa/pdfcredentials/constants.py +++ b/modoboa/pdfcredentials/constants.py @@ -6,5 +6,5 @@ from django.utils.translation import gettext_lazy CONNECTION_SECURITY_MODES = [ ("none", gettext_lazy("None")), ("starttls", "STARTTLS"), - ("tls", "TLS") + ("tls", "TLS"), ] diff --git a/modoboa/pdfcredentials/documents.py b/modoboa/pdfcredentials/documents.py index edc2bc973..6a35f608e 100644 --- a/modoboa/pdfcredentials/documents.py +++ b/modoboa/pdfcredentials/documents.py @@ -8,8 +8,13 @@ from reportlab.lib.units import cm from reportlab.lib.enums import TA_CENTER from reportlab.lib import utils from reportlab.platypus import ( - SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Frame, - Image + SimpleDocTemplate, + Paragraph, + Spacer, + Table, + TableStyle, + Frame, + Image, ) from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle @@ -20,25 +25,28 @@ from modoboa.parameters import tools as param_tools from . import lib styles = getSampleStyleSheet() -styles.add(ParagraphStyle( - name="Footer", alignment=TA_CENTER, fontName="Helvetica-Oblique", - textColor=colors.lightgrey -)) -styles.add(ParagraphStyle( - name="Warning", fontName="Helvetica-Oblique", - textColor=colors.red, fontSize=12 -)) -styles.add(ParagraphStyle( - name="Link", - textColor=colors.blue -)) -styles.add(ParagraphStyle( - name="Greeting", alignment=TA_CENTER, fontName="Helvetica-Oblique", - fontSize=14 -)) +styles.add( + ParagraphStyle( + name="Footer", + alignment=TA_CENTER, + fontName="Helvetica-Oblique", + textColor=colors.lightgrey, + ) +) +styles.add( + ParagraphStyle( + name="Warning", fontName="Helvetica-Oblique", textColor=colors.red, fontSize=12 + ) +) +styles.add(ParagraphStyle(name="Link", textColor=colors.blue)) +styles.add( + ParagraphStyle( + name="Greeting", alignment=TA_CENTER, fontName="Helvetica-Oblique", fontSize=14 + ) +) -def resized_image(path, width=1*cm): +def resized_image(path, width=1 * cm): img = utils.ImageReader(path) iw, ih = img.getSize() aspect = ih / float(iw) @@ -52,56 +60,74 @@ def credentials(account, password): canvas.setTitle(_("Personal account information")) canvas.setAuthor(account.fullname) canvas.setCreator("Modoboa") - footer = [Paragraph(_("Powered by Modoboa - Mail hosting made simple"), - styles["Footer"])] + footer = [ + Paragraph( + _("Powered by Modoboa - Mail hosting made simple"), styles["Footer"] + ) + ] Frame(0, 0, 21 * cm, 2 * cm).addFromList(footer, canvas) - conf = dict( - param_tools.get_global_parameters("pdfcredentials")) + conf = dict(param_tools.get_global_parameters("pdfcredentials")) filename = lib.get_creds_filename(account) buff = BytesIO() doc = SimpleDocTemplate(buff, pagesize=A4) story = [] logo = lib.get_document_logo() if logo is not None: - story.append(resized_image(logo, 8*cm)) + story.append(resized_image(logo, 8 * cm)) story.append(Spacer(1, 1 * cm)) story.append(Paragraph(conf["title"], styles["Title"])) story.append(Spacer(1, 1 * cm)) - story.append(Paragraph(_(""" + story.append( + Paragraph( + _( + """ Dear %s, this document contains the credentials you will need to connect to Modoboa. Learn the content and destroy the document as soon as possible. -""") % account.fullname, styles["Normal"])) +""" + ) + % account.fullname, + styles["Normal"], + ) + ) story.append(Spacer(1, 0.2 * cm)) story.append(Paragraph(_("Web panel:"), styles["h3"])) data = [ ["URL", conf["webpanel_url"]], [_("Username"), account.username], - [_("Password"), password] + [_("Password"), password], ] table = Table(data) - table.setStyle(TableStyle([ - ('TEXTCOLOR', (1, 0), (1, 0), colors.blue), - ('GRID', (0, 0), (-1, -1), 1, colors.black), - ('BACKGROUND', (0, 0), (0, -1), colors.lightgrey), - ])) + table.setStyle( + TableStyle( + [ + ("TEXTCOLOR", (1, 0), (1, 0), colors.blue), + ("GRID", (0, 0), (-1, -1), 1, colors.black), + ("BACKGROUND", (0, 0), (0, -1), colors.lightgrey), + ] + ) + ) story.append(table) story.append(Spacer(1, 0.5 * cm)) - story.append(Paragraph(_(""" + story.append( + Paragraph( + _( + """ Here you can view your emails anytime online, create filters or manage your contacts. -"""), styles["Normal"])) +""" + ), + styles["Normal"], + ) + ) story.append(Spacer(1, 0.2 * cm)) - story.append(Paragraph( - _("Please change your password!"), - styles["Warning"])) + story.append(Paragraph(_("Please change your password!"), styles["Warning"])) if conf["include_connection_settings"]: story.append(Spacer(1, 1 * cm)) - story.append(Paragraph( - _("PC/Tablet/Smartphone configuration:"), styles["h3"])) + story.append(Paragraph(_("PC/Tablet/Smartphone configuration:"), styles["h3"])) story.append(Spacer(1, 0.2 * cm)) data = [ [_("SMTP server address"), conf["smtp_server_address"]], @@ -112,15 +138,21 @@ your contacts. [_("IMAP connection security"), conf["imap_connection_security"]], ] table = Table(data) - table.setStyle(TableStyle([ - ('GRID', (0, 0), (-1, -1), 1, colors.black), - ('BACKGROUND', (0, 0), (0, -1), colors.lightgrey), - ])) + table.setStyle( + TableStyle( + [ + ("GRID", (0, 0), (-1, -1), 1, colors.black), + ("BACKGROUND", (0, 0), (0, -1), colors.lightgrey), + ] + ) + ) story.append(table) story.append(Spacer(1, 0.5 * cm)) - story.append(Paragraph( - _("Use those settings for your computer, tablet or phone."), - styles["Normal"]) + story.append( + Paragraph( + _("Use those settings for your computer, tablet or phone."), + styles["Normal"], + ) ) if conf["custom_message"]: diff --git a/modoboa/pdfcredentials/handlers.py b/modoboa/pdfcredentials/handlers.py index cf8dfb360..c67ca2ec4 100644 --- a/modoboa/pdfcredentials/handlers.py +++ b/modoboa/pdfcredentials/handlers.py @@ -23,8 +23,7 @@ def password_updated(sender, account, password, created, **kwargs): """Create or update document.""" if not param_tools.get_global_parameter("enabled_pdfcredentials"): return - generate_at_creation = param_tools.get_global_parameter( - "generate_at_creation") + generate_at_creation = param_tools.get_global_parameter("generate_at_creation") if (generate_at_creation and not created) or account.is_superuser: return try: @@ -32,8 +31,11 @@ def password_updated(sender, account, password, created, **kwargs): except InternalError as e: logger = logging.getLogger("modoboa.admin") logger.error( - _("Failed to create PDF_credentials directory. " - "Please check the permissions or the path.")) + _( + "Failed to create PDF_credentials directory. " + "Please check the permissions or the path." + ) + ) credentials(account, password) @@ -51,13 +53,14 @@ def extra_account_actions(sender, account, **kwargs): fname = get_creds_filename(account) if not os.path.exists(fname): return [] - return [{ - "name": "get_credentials", - "url": reverse("pdfcredentials:account_credentials", - args=[account.id]), - "img": "fa fa-download", - "title": _("Retrieve user's credentials as a PDF document") - }] + return [ + { + "name": "get_credentials", + "url": reverse("pdfcredentials:account_credentials", args=[account.id]), + "img": "fa fa-download", + "title": _("Retrieve user's credentials as a PDF document"), + } + ] @receiver(admin_signals.extra_account_identities_actions) @@ -80,5 +83,5 @@ def extra_identities_actions(sender, account, **kwargs): "type": "download", "content_type": "application/pdf", "url": url, - "filename": os.path.basename(fname) + "filename": os.path.basename(fname), } diff --git a/modoboa/pdfcredentials/lib.py b/modoboa/pdfcredentials/lib.py index 895e202ca..72897fac9 100644 --- a/modoboa/pdfcredentials/lib.py +++ b/modoboa/pdfcredentials/lib.py @@ -25,15 +25,14 @@ def init_storage_dir(): return elif not os.access(storage_dir, os.W_OK) and os.path.exists(storage_dir): raise InternalError( - _("Can't write to selected directory to store PDF " - "documents") - ) + _("Can't write to selected directory to store PDF " "documents") + ) try: os.mkdir(storage_dir) except (OSError, IOError) as inst: raise InternalError( - _("Failed to create the directory that will contain " - "PDF documents (%s)") % inst + _("Failed to create the directory that will contain " "PDF documents (%s)") + % inst ) @@ -55,14 +54,10 @@ def _get_cipher(iv): """Return ready-to-user Cipher.""" key = smart_bytes(settings.SECRET_KEY[:32]) backend = default_backend() - return Cipher( - algorithms.AES(force_bytes(key)), - modes.CBC(iv), - backend=backend - ) + return Cipher(algorithms.AES(force_bytes(key)), modes.CBC(iv), backend=backend) -def crypt_and_save_to_file(content, filename, length, chunksize=64*512): +def crypt_and_save_to_file(content, filename, length, chunksize=64 * 512): """Crypt content and save it to a file.""" iv = os.urandom(16) cipher = _get_cipher(iv) @@ -82,11 +77,14 @@ def crypt_and_save_to_file(content, filename, length, chunksize=64*512): except FileNotFoundError as e: logger = logging.getLogger("modoboa.admin") logger.error( - _("Failed to create PDF_credentials file. " - "Please check the permissions or the path.")) + _( + "Failed to create PDF_credentials file. " + "Please check the permissions or the path." + ) + ) -def decrypt_file(filename, chunksize=24*1024): +def decrypt_file(filename, chunksize=24 * 1024): """Decrypt the content of a file and return it.""" buff = BytesIO() with open(filename, "rb") as fp: diff --git a/modoboa/pdfcredentials/tests/test_lib.py b/modoboa/pdfcredentials/tests/test_lib.py index 87366d542..c66aea3e2 100644 --- a/modoboa/pdfcredentials/tests/test_lib.py +++ b/modoboa/pdfcredentials/tests/test_lib.py @@ -34,9 +34,7 @@ class PDFCredentialsLibTestCase(ModoAPITestCase): data = { "username": f"{username}", "role": "SimpleUsers", - "mailbox": { - "use_domain_quota": True - }, + "mailbox": {"use_domain_quota": True}, "password": "Toto12345", "language": "fr", } @@ -62,16 +60,15 @@ class PDFCredentialsLibTestCase(ModoAPITestCase): fname = os.path.join(self.workdir, "{}.pdf".format(username)) self.assertTrue(os.path.exists(fname)) filebuff = lib.decrypt_file(fname) - self.assertIn("application/pdf", - Popen("/usr/bin/file -b --mime -", - shell=True, - stdout=PIPE, - stdin=PIPE) - .communicate(filebuff)[0].strip().decode() - ) + self.assertIn( + "application/pdf", + Popen("/usr/bin/file -b --mime -", shell=True, stdout=PIPE, stdin=PIPE) + .communicate(filebuff)[0] + .strip() + .decode(), + ) with open(fname, "r") as file: with self.assertRaises(UnicodeDecodeError): - Popen("/usr/bin/file -b --mime -", - shell=True, - stdout=PIPE, - stdin=PIPE).communicate(file.read(1024))[0].strip() + Popen( + "/usr/bin/file -b --mime -", shell=True, stdout=PIPE, stdin=PIPE + ).communicate(file.read(1024))[0].strip() diff --git a/modoboa/pdfcredentials/tests/test_views.py b/modoboa/pdfcredentials/tests/test_views.py index 48f2d9508..f4a1bdf57 100644 --- a/modoboa/pdfcredentials/tests/test_views.py +++ b/modoboa/pdfcredentials/tests/test_views.py @@ -36,14 +36,18 @@ class EventsTestCase(ModoTestCase): """Create a test account.""" values = { "username": username, - "first_name": "Tester", "last_name": "Toto", - "role": "SimpleUsers", "quota_act": True, - "is_active": True, "email": username, - "random_password": True, "stepid": 2 + "first_name": "Tester", + "last_name": "Toto", + "role": "SimpleUsers", + "quota_act": True, + "is_active": True, + "email": username, + "random_password": True, + "stepid": 2, } response = self.client.post( - reverse("admin:account_add"), values, - HTTP_X_REQUESTED_WITH="XMLHttpRequest") + reverse("admin:account_add"), values, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + ) self.assertEqual(response.status_code, expected_status) return values @@ -60,8 +64,8 @@ class EventsTestCase(ModoTestCase): # Try to download the file response = self.client.get( - reverse("pdfcredentials:account_credentials", - args=[account.pk])) + reverse("pdfcredentials:account_credentials", args=[account.pk]) + ) self.assertEqual(response.status_code, 200) self.assertEqual(response["Content-Type"], "application/pdf") @@ -70,21 +74,17 @@ class EventsTestCase(ModoTestCase): # Try to download a second time response = self.client.get( - reverse("pdfcredentials:account_credentials", - args=[account.pk])) + reverse("pdfcredentials:account_credentials", args=[account.pk]) + ) self.assertContains(response, "No document available for this user") # Update account values.update({"language": "en"}) - self.ajax_post( - reverse("admin:account_change", args=[account.pk]), values - ) + self.ajax_post(reverse("admin:account_change", args=[account.pk]), values) self.assertFalse(os.path.exists(fname)) self.set_global_parameter("generate_at_creation", False) - self.ajax_post( - reverse("admin:account_change", args=[account.pk]), values - ) + self.ajax_post(reverse("admin:account_change", args=[account.pk]), values) self.assertTrue(os.path.exists(fname)) def test_with_connection_settings(self): @@ -107,9 +107,7 @@ class EventsTestCase(ModoTestCase): fname = os.path.join(self.workdir, "{}.pdf".format(values["username"])) self.assertTrue(os.path.exists(fname)) account = core_models.User.objects.get(username=values["username"]) - self.ajax_post( - reverse("admin:account_delete", args=[account.pk]), {} - ) + self.ajax_post(reverse("admin:account_delete", args=[account.pk]), {}) self.assertFalse(os.path.exists(fname)) @override_settings(MODOBOA_LOGO="modoboa.png") @@ -128,8 +126,8 @@ class EventsTestCase(ModoTestCase): # Try to download the file response = self.client.get( - reverse("pdfcredentials:account_credentials", - args=[account.pk])) + reverse("pdfcredentials:account_credentials", args=[account.pk]) + ) self.assertEqual(response.status_code, 200) self.assertEqual(response["Content-Type"], "application/pdf") @@ -137,18 +135,15 @@ class EventsTestCase(ModoTestCase): self.assertFalse(os.path.exists(fname)) # Delete account - self.ajax_post( - reverse("admin:account_delete", args=[account.pk]), {} - ) + self.ajax_post(reverse("admin:account_delete", args=[account.pk]), {}) def test_storage_dir_creation(self): """Test storage directory creation.""" self.set_global_parameter("storage_dir", "/nonexistentdir") - with self.assertLogs(logger='modoboa.admin', level='ERROR') as log: + with self.assertLogs(logger="modoboa.admin", level="ERROR") as log: resp = self._create_account("leon@test.com") self.assertIn( "ERROR:modoboa.admin:Failed to create PDF_credentials directory. " "Please check the permissions or the path.", - log.output + log.output, ) - diff --git a/modoboa/pdfcredentials/urls.py b/modoboa/pdfcredentials/urls.py index 9a13a5b3c..939ed979d 100644 --- a/modoboa/pdfcredentials/urls.py +++ b/modoboa/pdfcredentials/urls.py @@ -5,6 +5,9 @@ from . import views app_name = "pdfcredentials" urlpatterns = [ - path('credentials//', views.get_account_credentials, - name="account_credentials"), + path( + "credentials//", + views.get_account_credentials, + name="account_credentials", + ), ] diff --git a/modoboa/pdfcredentials/views.py b/modoboa/pdfcredentials/views.py index 9ee197585..4ef0257bb 100644 --- a/modoboa/pdfcredentials/views.py +++ b/modoboa/pdfcredentials/views.py @@ -5,9 +5,7 @@ import os from django.http import HttpResponse from django.utils.translation import gettext as _ -from django.contrib.auth.decorators import ( - login_required, permission_required -) +from django.contrib.auth.decorators import login_required, permission_required from modoboa.core.models import User from modoboa.lib.exceptions import ModoboaException, PermDeniedException diff --git a/modoboa/policyd/core.py b/modoboa/policyd/core.py index ed710293a..96a085bf0 100644 --- a/modoboa/policyd/core.py +++ b/modoboa/policyd/core.py @@ -37,6 +37,7 @@ def close_db_connections(func, *args, **kwargs): To use in threads. """ + def _close_db_connections(*args, **kwargs): ret = None try: @@ -45,6 +46,7 @@ def close_db_connections(func, *args, **kwargs): for conn in connections.all(): conn.close() return ret + return _close_db_connections @@ -77,10 +79,7 @@ def get_local_config(): @close_db_connections def get_notification_recipients(): """Return superadmins with a mailbox.""" - return ( - core_models.User.objects - .filter(is_superuser=True, mailbox__isnull=False) - ) + return core_models.User.objects.filter(is_superuser=True, mailbox__isnull=False) @close_db_connections @@ -94,16 +93,18 @@ def create_alarm(ltype, name): else: localpart, domain = split_mailbox(name) mailbox = admin_models.Mailbox.objects.get( - address=localpart, domain__name=domain) + address=localpart, domain__name=domain + ) mailbox.alarms.create( - domain=mailbox.domain, title=title, internal_name=internal_name) + domain=mailbox.domain, title=title, internal_name=internal_name + ) async def notify_limit_reached(ltype, name): """Send a notification to super admins about item.""" ltype_translations = { "account": gettext_lazy("account"), - "domain": gettext_lazy("domain") + "domain": gettext_lazy("domain"), } # We're going to execute sync code so we need an executor executor = concurrent.futures.ThreadPoolExecutor(max_workers=3) @@ -118,9 +119,9 @@ async def notify_limit_reached(ltype, name): for recipient in recipients: with translation.override(recipient.language): content = render_to_string( - "policyd/notifications/limit_reached.html", { - "ltype": ltype_translations[ltype], "name": name - }) + "policyd/notifications/limit_reached.html", + {"ltype": ltype_translations[ltype], "name": name}, + ) subject = _("[modoboa] Sending limit reached") msg = EmailMessage() msg["From"] = sender @@ -143,7 +144,9 @@ async def apply_policies(attributes): sasl_username = attributes.get("sasl_username") if not sasl_username: return SUCCESS_ACTION - rclient = aioredis.from_url(settings.REDIS_URL, encoding="utf-8", decode_responses=True) + rclient = aioredis.from_url( + settings.REDIS_URL, encoding="utf-8", decode_responses=True + ) decr_domain = False decr_user = False localpart, domain = split_mailbox(sasl_username) @@ -155,8 +158,7 @@ async def apply_policies(attributes): decr_domain = True if await rclient.hexists(constants.REDIS_HASHNAME, sasl_username): counter = await rclient.hget(constants.REDIS_HASHNAME, sasl_username) - logger.info("Account {} current counter: {}".format( - sasl_username, counter)) + logger.info("Account {} current counter: {}".format(sasl_username, counter)) if int(counter) <= 0: return FAILURE_ACTION decr_user = True @@ -212,8 +214,7 @@ async def new_connection(reader, writer): def get_next_execution_dt(): """Return next execution date and time.""" - return (timezone.now() + relativedelta(days=1)).replace( - hour=0, minute=0, second=0) + return (timezone.now() + relativedelta(days=1)).replace(hour=0, minute=0, second=0) @sync_to_async @@ -226,11 +227,10 @@ def get_domains_to_reset(): """ qset = admin_models.Domain.objects.filter(message_limit__isnull=False) admin_models.Alarm.objects.filter( - internal_name="limit_reached", domain__in=qset, - status=admin_constants.ALARM_OPENED - ).update( - status=admin_constants.ALARM_CLOSED, closed=timezone.now() - ) + internal_name="limit_reached", + domain__in=qset, + status=admin_constants.ALARM_OPENED, + ).update(status=admin_constants.ALARM_CLOSED, closed=timezone.now()) return list(qset) @@ -242,29 +242,27 @@ def get_mailboxes_to_reset(): We also close all associated alarms. """ - qset = ( - admin_models.Mailbox.objects.filter(message_limit__isnull=False) - .select_related("domain") - ) + qset = admin_models.Mailbox.objects.filter( + message_limit__isnull=False + ).select_related("domain") admin_models.Alarm.objects.filter( - internal_name="limit_reached", mailbox__in=qset, - status=admin_constants.ALARM_OPENED - ).update( - status=admin_constants.ALARM_CLOSED, closed=timezone.now() - ) + internal_name="limit_reached", + mailbox__in=qset, + status=admin_constants.ALARM_OPENED, + ).update(status=admin_constants.ALARM_CLOSED, closed=timezone.now()) return list(qset) async def reset_counters(): """Reset all counters.""" - rclient = aioredis.from_url(settings.REDIS_URL, encoding="utf-8", decode_responses=True) + rclient = aioredis.from_url( + settings.REDIS_URL, encoding="utf-8", decode_responses=True + ) logger.info("Resetting all counters") for domain in await get_domains_to_reset(): - await rclient.hset( - constants.REDIS_HASHNAME, domain.name, domain.message_limit) + await rclient.hset(constants.REDIS_HASHNAME, domain.name, domain.message_limit) for mb in await get_mailboxes_to_reset(): - await rclient.hset( - constants.REDIS_HASHNAME, mb.full_address, mb.message_limit) + await rclient.hset(constants.REDIS_HASHNAME, mb.full_address, mb.message_limit) await rclient.close() # reschedule asyncio.ensure_future(run_at(get_next_execution_dt(), reset_counters)) @@ -273,5 +271,6 @@ async def reset_counters(): def start_reset_counters_coro(): """Start coroutine.""" first_time = (timezone.now() + relativedelta(days=1)).replace( - hour=0, minute=0, second=0) + hour=0, minute=0, second=0 + ) asyncio.ensure_future(run_at(first_time, reset_counters)) diff --git a/modoboa/policyd/handlers.py b/modoboa/policyd/handlers.py index cbdcc366e..d4e05465f 100644 --- a/modoboa/policyd/handlers.py +++ b/modoboa/policyd/handlers.py @@ -17,9 +17,7 @@ def set_message_limit(instance, key): if old_message_limit == instance.message_limit: return rclient = redis.Redis( - host=settings.REDIS_HOST, - port=settings.REDIS_PORT, - db=settings.REDIS_QUOTA_DB + host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_QUOTA_DB ) if instance.message_limit is None: # delete existing key diff --git a/modoboa/policyd/management/commands/policy_daemon.py b/modoboa/policyd/management/commands/policy_daemon.py index 2808a5086..e1abc2948 100644 --- a/modoboa/policyd/management/commands/policy_daemon.py +++ b/modoboa/policyd/management/commands/policy_daemon.py @@ -27,8 +27,7 @@ class Command(BaseCommand): """Add command line arguments.""" parser.add_argument("--host", type=str, default="localhost") parser.add_argument("--port", type=int, default=9999) - parser.add_argument("--debug", action="store_true", - help="Enable debug mode") + parser.add_argument("--debug", action="store_true", help="Enable debug mode") def handle(self, *args, **options): """Entry point.""" @@ -41,17 +40,16 @@ class Command(BaseCommand): # Schedule reset task core.start_reset_counters_coro() - for signame in {'SIGINT', 'SIGTERM'}: + for signame in {"SIGINT", "SIGTERM"}: loop.add_signal_handler( - getattr(signal, signame), - functools.partial(ask_exit, signame, loop) + getattr(signal, signame), functools.partial(ask_exit, signame, loop) ) logger.info("Serving on {}".format(server.sockets[0].getsockname())) if options["debug"]: loop.set_debug(True) - logging.getLogger('asyncio').setLevel(logging.DEBUG) + logging.getLogger("asyncio").setLevel(logging.DEBUG) loop.run_forever() diff --git a/modoboa/policyd/tests.py b/modoboa/policyd/tests.py index d3bd97000..000e541f3 100644 --- a/modoboa/policyd/tests.py +++ b/modoboa/policyd/tests.py @@ -36,14 +36,13 @@ class RedisTestCaseMixin: self.rclient = redis.Redis( host=settings.REDIS_HOST, port=settings.REDIS_PORT, - db=settings.REDIS_QUOTA_DB + db=settings.REDIS_QUOTA_DB, ) self.rclient.set_response_callback("HGET", int) self.rclient.delete(constants.REDIS_HASHNAME) -class PolicyDaemonTestCase( - RedisTestCaseMixin, ParametersMixin, TransactionTestCase): +class PolicyDaemonTestCase(RedisTestCaseMixin, ParametersMixin, TransactionTestCase): """Test cases for policy daemon. A redis instance is required to run those tests. @@ -71,7 +70,7 @@ class PolicyDaemonTestCase( domain.save() self.assertEqual( self.rclient.hget(constants.REDIS_HASHNAME, domain.name), - domain.message_limit + domain.message_limit, ) return domain @@ -82,8 +81,7 @@ class PolicyDaemonTestCase( mb.message_limit = value mb.save() self.assertEqual( - self.rclient.hget(constants.REDIS_HASHNAME, account.email), - mb.message_limit + self.rclient.hget(constants.REDIS_HASHNAME, account.email), mb.message_limit ) return account @@ -108,25 +106,29 @@ class PolicyDaemonTestCase( s = self.connect_to_daemon() # This one will be accepted - s.send(b"""protocol_state=RCPT + s.send( + b"""protocol_state=RCPT sasl_username=user@test.com -""") +""" + ) res = s.recv(1024) self.assertEqual(res, b"action=dunno\n\n") self.assertEqual( self.rclient.hget(constants.REDIS_HASHNAME, domain.name), - domain.message_limit - 1 + domain.message_limit - 1, ) s.close() # This one will be accepted and will trigger a notification # but I can't check it yet s = self.connect_to_daemon() - s.send(b"""protocol_state=RCPT + s.send( + b"""protocol_state=RCPT sasl_username=user@test.com -""") +""" + ) time.sleep(0.1) res = s.recv(1024) self.assertEqual(res, b"action=dunno\n\n") @@ -134,10 +136,12 @@ sasl_username=user@test.com # This one will be denied s = self.connect_to_daemon() - s.send(b"""protocol_state=RCPT + s.send( + b"""protocol_state=RCPT sasl_username=user@test.com -""") +""" + ) res = s.recv(1024) self.assertEqual( res, b"action=defer_if_permit Daily limit reached, retry later\n\n" @@ -150,25 +154,29 @@ sasl_username=user@test.com s = self.connect_to_daemon() # This one will be accepted - s.send(b"""protocol_state=RCPT + s.send( + b"""protocol_state=RCPT sasl_username=user@test.com -""") +""" + ) res = s.recv(1024) self.assertEqual(res, b"action=dunno\n\n") self.assertEqual( self.rclient.hget(constants.REDIS_HASHNAME, account.email), - account.mailbox.message_limit - 1 + account.mailbox.message_limit - 1, ) s.close() # This one will be accepted and will trigger a notification # but I can't check it yet s = self.connect_to_daemon() - s.send(b"""protocol_state=RCPT + s.send( + b"""protocol_state=RCPT sasl_username=user@test.com -""") +""" + ) time.sleep(0.1) res = s.recv(1024) self.assertEqual(res, b"action=dunno\n\n") @@ -176,10 +184,12 @@ sasl_username=user@test.com # This one will be denied s = self.connect_to_daemon() - s.send(b"""protocol_state=RCPT + s.send( + b"""protocol_state=RCPT sasl_username=user@test.com -""") +""" + ) res = s.recv(1024) self.assertEqual( res, b"action=defer_if_permit Daily limit reached, retry later\n\n" @@ -202,10 +212,8 @@ sasl_username=user@test.com event_loop.run_until_complete(run_test()) event_loop.close() - self.assertEqual( - self.rclient.hget(constants.REDIS_HASHNAME, domain.name), 20) - self.assertEqual( - self.rclient.hget(constants.REDIS_HASHNAME, account.email), 10) + self.assertEqual(self.rclient.hget(constants.REDIS_HASHNAME, domain.name), 20) + self.assertEqual(self.rclient.hget(constants.REDIS_HASHNAME, account.email), 10) class ModelsTestCase(RedisTestCaseMixin, ModoTestCase): @@ -241,7 +249,7 @@ class ModelsTestCase(RedisTestCaseMixin, ModoTestCase): domain.save() self.assertEqual( self.rclient.hget(constants.REDIS_HASHNAME, domain.name), - domain.message_limit + domain.message_limit, ) # Force constructor call to fill _loaded_values @@ -250,11 +258,9 @@ class ModelsTestCase(RedisTestCaseMixin, ModoTestCase): domain.save() self.assertEqual( self.rclient.hget(constants.REDIS_HASHNAME, domain.name), - domain.message_limit + domain.message_limit, ) domain.message_limit = None domain.save() - self.assertFalse( - self.rclient.hexists(constants.REDIS_HASHNAME, domain.name) - ) + self.assertFalse(self.rclient.hexists(constants.REDIS_HASHNAME, domain.name)) diff --git a/modoboa/policyd/utils.py b/modoboa/policyd/utils.py index 23872ffe6..ca4b88c17 100644 --- a/modoboa/policyd/utils.py +++ b/modoboa/policyd/utils.py @@ -10,9 +10,7 @@ from . import constants def get_redis_connection(): """Return a client connection to Redis server.""" rclient = redis.Redis( - host=settings.REDIS_HOST, - port=settings.REDIS_PORT, - db=settings.REDIS_QUOTA_DB + host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_QUOTA_DB ) rclient.set_response_callback("HGET", int) return rclient diff --git a/modoboa/relaydomains/api/v1/serializers.py b/modoboa/relaydomains/api/v1/serializers.py index c4eab23a7..4461b45d2 100644 --- a/modoboa/relaydomains/api/v1/serializers.py +++ b/modoboa/relaydomains/api/v1/serializers.py @@ -21,16 +21,17 @@ class TransportSerializer(serializers.ModelSerializer): """Check fields based on backend.""" self.backend = tr_backends.manager.get_backend(data["service"]) if not self.backend: - raise serializers.ValidationError({ - "service": _("Unsupported service") - }) + raise serializers.ValidationError({"service": _("Unsupported service")}) data["_settings"] = json.loads(data["_settings"]) errors = self.backend.clean_fields(data["_settings"]) if errors: - raise serializers.ValidationError({ - "_settings": ",".join( - ["{}: {}".format(error[0], error[1]) for error in errors]) - }) + raise serializers.ValidationError( + { + "_settings": ",".join( + ["{}: {}".format(error[0], error[1]) for error in errors] + ) + } + ) return data @@ -42,10 +43,18 @@ class RelayDomainSerializer(serializers.ModelSerializer): class Meta: model = admin_models.Domain fields = ( - "pk", "name", "enabled", "transport", "enable_dkim", - "dkim_key_selector", "dkim_public_key" + "pk", + "name", + "enabled", + "transport", + "enable_dkim", + "dkim_key_selector", + "dkim_public_key", + ) + read_only_fields = ( + "pk", + "dkim_public_key", ) - read_only_fields = ("pk", "dkim_public_key", ) def create(self, validated_data): """Use backend to serialize data.""" diff --git a/modoboa/relaydomains/api/v1/tests.py b/modoboa/relaydomains/api/v1/tests.py index 55be375a3..7f3a1944b 100644 --- a/modoboa/relaydomains/api/v1/tests.py +++ b/modoboa/relaydomains/api/v1/tests.py @@ -16,23 +16,27 @@ class DataMixin(object): def setUpTestData(cls): # NOQA:N802 super(DataMixin, cls).setUpTestData() transport = tr_factories.TransportFactory( - pattern="test.com", _settings={ + pattern="test.com", + _settings={ "relay_target_host": "external.host.tld", "relay_target_port": 25, - "relay_verify_recipients": False - } + "relay_verify_recipients": False, + }, ) cls.domain1 = admin_factories.DomainFactory( - name="test.com", type="relaydomain", transport=transport) + name="test.com", type="relaydomain", transport=transport + ) transport = tr_factories.TransportFactory( - pattern="domain2.test", _settings={ + pattern="domain2.test", + _settings={ "relay_target_host": "external.host.tld", "relay_target_port": 25, - "relay_verify_recipients": True - } + "relay_verify_recipients": True, + }, ) cls.domain2 = admin_factories.DomainFactory( - name="test2.com", type="relaydomain", transport=transport) + name="test2.com", type="relaydomain", transport=transport + ) class RelayDomainAPITestCase(DataMixin, ModoAPITestCase): @@ -48,21 +52,16 @@ class RelayDomainAPITestCase(DataMixin, ModoAPITestCase): def test_create(self): """Test create service.""" url = reverse("api:relaydomain-list") - settings = { - "relay_target_host": "1.2.3.4" - } + settings = {"relay_target_host": "1.2.3.4"} data = { "name": "test3.com", - "transport": { - "service": "relay", - "_settings": json.dumps(settings) - } + "transport": {"service": "relay", "_settings": json.dumps(settings)}, } response = self.client.post(url, data, format="json") self.assertEqual(response.status_code, 400) self.assertEqual( response.data["transport"]["_settings"], - ["relay_target_port: This field is required"] + ["relay_target_port: This field is required"], ) settings.update({"relay_target_port": 25}) @@ -71,8 +70,10 @@ class RelayDomainAPITestCase(DataMixin, ModoAPITestCase): self.assertEqual(response.status_code, 201) domain = admin_models.Domain.objects.get(name="test3.com") self.assertEqual( - domain.transport.next_hop, "[{}]:{}".format( - settings["relay_target_host"], settings["relay_target_port"]) + domain.transport.next_hop, + "[{}]:{}".format( + settings["relay_target_host"], settings["relay_target_port"] + ), ) def test_update(self): @@ -82,10 +83,7 @@ class RelayDomainAPITestCase(DataMixin, ModoAPITestCase): settings.update({"relay_target_port": 1000}) data = { "name": "test3.com", - "transport": { - "service": "relay", - "_settings": json.dumps(settings) - } + "transport": {"service": "relay", "_settings": json.dumps(settings)}, } response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, 200) @@ -93,8 +91,10 @@ class RelayDomainAPITestCase(DataMixin, ModoAPITestCase): self.domain1.transport.refresh_from_db() self.assertEqual(self.domain1.name, data["name"]) self.assertEqual( - self.domain1.transport.next_hop, "[{}]:{}".format( - settings["relay_target_host"], settings["relay_target_port"]) + self.domain1.transport.next_hop, + "[{}]:{}".format( + settings["relay_target_host"], settings["relay_target_port"] + ), ) def test_delete(self): @@ -105,4 +105,5 @@ class RelayDomainAPITestCase(DataMixin, ModoAPITestCase): with self.assertRaises(admin_models.Domain.DoesNotExist): self.domain1.refresh_from_db() self.assertFalse( - tr_models.Transport.objects.filter(pattern="test.com").exists()) + tr_models.Transport.objects.filter(pattern="test.com").exists() + ) diff --git a/modoboa/relaydomains/api/v1/urls.py b/modoboa/relaydomains/api/v1/urls.py index e11e61e64..38b81e00c 100644 --- a/modoboa/relaydomains/api/v1/urls.py +++ b/modoboa/relaydomains/api/v1/urls.py @@ -5,6 +5,5 @@ from rest_framework import routers from . import viewsets router = routers.SimpleRouter() -router.register( - r"relaydomains", viewsets.RelayDomainViewSet, basename="relaydomain") +router.register(r"relaydomains", viewsets.RelayDomainViewSet, basename="relaydomain") urlpatterns = router.urls diff --git a/modoboa/relaydomains/api/v1/viewsets.py b/modoboa/relaydomains/api/v1/viewsets.py index c47a92feb..c37aa7550 100644 --- a/modoboa/relaydomains/api/v1/viewsets.py +++ b/modoboa/relaydomains/api/v1/viewsets.py @@ -9,17 +9,21 @@ from modoboa.lib.viewsets import RevisionModelMixin from . import serializers -class RelayDomainViewSet(GetThrottleViewsetMixin, RevisionModelMixin, viewsets.ModelViewSet): +class RelayDomainViewSet( + GetThrottleViewsetMixin, RevisionModelMixin, viewsets.ModelViewSet +): """RelayDomain viewset.""" - permission_classes = [IsAuthenticated, DjangoModelPermissions, ] + permission_classes = [ + IsAuthenticated, + DjangoModelPermissions, + ] serializer_class = serializers.RelayDomainSerializer def get_queryset(self): """Filter queryset based on current user.""" - return ( - admin_models.Domain.objects.get_for_admin(self.request.user) - .filter(type="relaydomain") + return admin_models.Domain.objects.get_for_admin(self.request.user).filter( + type="relaydomain" ) def perform_destroy(self, instance): diff --git a/modoboa/relaydomains/forms.py b/modoboa/relaydomains/forms.py index 9b52086b4..34f00c1e4 100644 --- a/modoboa/relaydomains/forms.py +++ b/modoboa/relaydomains/forms.py @@ -43,10 +43,7 @@ class RelayDomainFormGeneral(tr_forms.TransportForm): class Meta: model = tr_models.Transport - exclude = [ - "creation", "pattern", "next_hop", "enabled", - "_settings" - ] + exclude = ["creation", "pattern", "next_hop", "enabled", "_settings"] def save(self, *args, **kwargs): """Custom save method.""" diff --git a/modoboa/relaydomains/handlers.py b/modoboa/relaydomains/handlers.py index a901052a4..96e634c32 100644 --- a/modoboa/relaydomains/handlers.py +++ b/modoboa/relaydomains/handlers.py @@ -19,7 +19,8 @@ def check_relaydomain_alias(sender, recipients, **kwargs): if not qset.exists(): return False qset = admin_models.Mailbox.objects.select_related("domain").filter( - domain__name=domain, address=localpart) + domain__name=domain, address=localpart + ) if qset.exists(): return False return True @@ -45,7 +46,7 @@ def register_postfix_maps(sender, **kwargs): return [ postfix_maps.RelayDomainsMap, postfix_maps.SplitedDomainsTransportMap, - postfix_maps.RelayRecipientVerification + postfix_maps.RelayRecipientVerification, ] @@ -57,34 +58,38 @@ def extra_domain_form(sender, user, **kwargs): domain = kwargs.get("domain") if not domain or domain.type != "relaydomain": return [] - return [{ - "id": "relaydomain", "title": _("Transport settings"), - "cls": forms.RelayDomainFormGeneral, - "formtpl": "transport/_transport_form.html" - }] + return [ + { + "id": "relaydomain", + "title": _("Transport settings"), + "cls": forms.RelayDomainFormGeneral, + "formtpl": "transport/_transport_form.html", + } + ] @receiver(admin_signals.get_domain_form_instances) def fill_domain_instances(sender, user, domain, **kwargs): """Fill the relaydomain form with the right instance.""" condition = ( - not user.has_perm("transport.change_transport") or - domain.type != "relaydomain" + not user.has_perm("transport.change_transport") or domain.type != "relaydomain" ) if condition: return {} - return { - "relaydomain": domain.transport - } + return {"relaydomain": domain.transport} @receiver(admin_signals.extra_domain_wizard_steps) def extra_wizard_step(sender, **kwargs): """Return a step to configure the relay settings.""" - return [forms.RelayDomainWizardStep( - "relay", forms.RelayDomainFormGeneral, _("Transport"), - "transport/_transport_form.html" - )] + return [ + forms.RelayDomainWizardStep( + "relay", + forms.RelayDomainFormGeneral, + _("Transport"), + "transport/_transport_form.html", + ) + ] @receiver(admin_signals.import_object) @@ -102,12 +107,10 @@ def update_recipient_access(sender, instance, **kwargs): return if instance._settings.get("relay_verify_recipients", False): models.RecipientAccess.objects.get_or_create( - pattern=instance.pattern, - defaults={"action": "reject_unverified_recipient"} + pattern=instance.pattern, defaults={"action": "reject_unverified_recipient"} ) else: - models.RecipientAccess.objects.filter( - pattern=instance.pattern).delete() + models.RecipientAccess.objects.filter(pattern=instance.pattern).delete() @receiver(signals.post_delete, sender=tr_models.Transport) diff --git a/modoboa/relaydomains/lib.py b/modoboa/relaydomains/lib.py index aa4f816a8..aecb0d284 100644 --- a/modoboa/relaydomains/lib.py +++ b/modoboa/relaydomains/lib.py @@ -12,17 +12,21 @@ def import_relaydomain(user, row, formopts): if len(row) != 7: raise BadRequest(_("Invalid line")) domain = admin_models.Domain( - name=row[1].strip(), type="relaydomain", quota=0, - enabled=(row[5].strip().lower() in ["true", "1", "yes", "y"]) + name=row[1].strip(), + type="relaydomain", + quota=0, + enabled=(row[5].strip().lower() in ["true", "1", "yes", "y"]), ) domain.save(creator=user) settings = { "relay_target_host": row[2].strip(), "relay_target_port": row[3].strip(), "relay_verify_recipients": ( - row[6].strip().lower() in ["true", "1", "yes", "y"]) + row[6].strip().lower() in ["true", "1", "yes", "y"] + ), } transport = tr_models.Transport( - pattern=domain.name, service="relay", _settings=settings) + pattern=domain.name, service="relay", _settings=settings + ) tr_backends.manager.get_backend("relay").serialize(transport) transport.save() diff --git a/modoboa/relaydomains/migrations/0001_initial.py b/modoboa/relaydomains/migrations/0001_initial.py index cd708a483..e24f8f88a 100644 --- a/modoboa/relaydomains/migrations/0001_initial.py +++ b/modoboa/relaydomains/migrations/0001_initial.py @@ -4,40 +4,88 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('admin', '0002_migrate_from_modoboa_admin'), + ("admin", "0002_migrate_from_modoboa_admin"), ] operations = [ migrations.CreateModel( - name='RelayDomain', + name="RelayDomain", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('target_host', models.CharField(help_text='Remote destination of this domain', max_length=255, verbose_name='target host')), - ('verify_recipients', models.BooleanField(default=False, help_text='Check for valid recipients', verbose_name='verify recipients')), - ('dates', models.ForeignKey(to='admin.ObjectDates', on_delete=models.CASCADE)), - ('domain', models.OneToOneField(null=True, to='admin.Domain', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "target_host", + models.CharField( + help_text="Remote destination of this domain", + max_length=255, + verbose_name="target host", + ), + ), + ( + "verify_recipients", + models.BooleanField( + default=False, + help_text="Check for valid recipients", + verbose_name="verify recipients", + ), + ), + ( + "dates", + models.ForeignKey(to="admin.ObjectDates", on_delete=models.CASCADE), + ), + ( + "domain", + models.OneToOneField( + null=True, to="admin.Domain", on_delete=models.CASCADE + ), + ), ], options={ - 'ordering': ['domain__name'], - 'db_table': 'postfix_relay_domains_relaydomain', + "ordering": ["domain__name"], + "db_table": "postfix_relay_domains_relaydomain", }, bases=(models.Model,), ), migrations.CreateModel( - name='Service', + name="Service", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(help_text='The service name', unique=True, max_length=100, verbose_name='name')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "name", + models.CharField( + help_text="The service name", + unique=True, + max_length=100, + verbose_name="name", + ), + ), ], options={ - 'db_table': 'postfix_relay_domains_service', + "db_table": "postfix_relay_domains_service", }, bases=(models.Model,), ), migrations.AddField( - model_name='relaydomain', - name='service', - field=models.ForeignKey(to='relaydomains.Service', on_delete=models.CASCADE), + model_name="relaydomain", + name="service", + field=models.ForeignKey( + to="relaydomains.Service", on_delete=models.CASCADE + ), preserve_default=True, ), ] diff --git a/modoboa/relaydomains/migrations/0002_migrate_from_modoboa_admin_relaydomains.py b/modoboa/relaydomains/migrations/0002_migrate_from_modoboa_admin_relaydomains.py index 5fd89c6a3..8b28b733d 100644 --- a/modoboa/relaydomains/migrations/0002_migrate_from_modoboa_admin_relaydomains.py +++ b/modoboa/relaydomains/migrations/0002_migrate_from_modoboa_admin_relaydomains.py @@ -4,8 +4,7 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('relaydomains', '0001_initial'), + ("relaydomains", "0001_initial"), ] - operations = [ - ] + operations = [] diff --git a/modoboa/relaydomains/migrations/0003_auto_20170116_1508.py b/modoboa/relaydomains/migrations/0003_auto_20170116_1508.py index bec153bd8..a49c7330b 100644 --- a/modoboa/relaydomains/migrations/0003_auto_20170116_1508.py +++ b/modoboa/relaydomains/migrations/0003_auto_20170116_1508.py @@ -5,18 +5,26 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('relaydomains', '0002_migrate_from_modoboa_admin_relaydomains'), + ("relaydomains", "0002_migrate_from_modoboa_admin_relaydomains"), ] operations = [ migrations.AddField( - model_name='relaydomain', - name='target_port', - field=models.IntegerField(default=25, help_text='Remote port of this domain', verbose_name='target host port'), + model_name="relaydomain", + name="target_port", + field=models.IntegerField( + default=25, + help_text="Remote port of this domain", + verbose_name="target host port", + ), ), migrations.AlterField( - model_name='relaydomain', - name='target_host', - field=models.CharField(help_text='Remote address (hostname or IP) of this domain', max_length=255, verbose_name='target host address'), + model_name="relaydomain", + name="target_host", + field=models.CharField( + help_text="Remote address (hostname or IP) of this domain", + max_length=255, + verbose_name="target host address", + ), ), ] diff --git a/modoboa/relaydomains/migrations/0004_auto_20161105_1424.py b/modoboa/relaydomains/migrations/0004_auto_20161105_1424.py index e426a035d..38ac8f5db 100644 --- a/modoboa/relaydomains/migrations/0004_auto_20161105_1424.py +++ b/modoboa/relaydomains/migrations/0004_auto_20161105_1424.py @@ -16,18 +16,18 @@ def move_dates(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('relaydomains', '0003_auto_20170116_1508'), + ("relaydomains", "0003_auto_20170116_1508"), ] operations = [ migrations.AddField( - model_name='relaydomain', - name='creation', + model_name="relaydomain", + name="creation", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.AddField( - model_name='relaydomain', - name='last_modification', + model_name="relaydomain", + name="last_modification", field=models.DateTimeField(null=True), ), migrations.RunPython(move_dates), diff --git a/modoboa/relaydomains/migrations/0005_auto_20161105_1426.py b/modoboa/relaydomains/migrations/0005_auto_20161105_1426.py index e51a96764..431cd49c6 100644 --- a/modoboa/relaydomains/migrations/0005_auto_20161105_1426.py +++ b/modoboa/relaydomains/migrations/0005_auto_20161105_1426.py @@ -5,12 +5,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('relaydomains', '0004_auto_20161105_1424'), + ("relaydomains", "0004_auto_20161105_1424"), ] operations = [ migrations.RemoveField( - model_name='relaydomain', - name='dates', + model_name="relaydomain", + name="dates", ), ] diff --git a/modoboa/relaydomains/migrations/0006_auto_20170215_0948.py b/modoboa/relaydomains/migrations/0006_auto_20170215_0948.py index 5f7ffd1ca..cdedfdba4 100644 --- a/modoboa/relaydomains/migrations/0006_auto_20170215_0948.py +++ b/modoboa/relaydomains/migrations/0006_auto_20170215_0948.py @@ -6,14 +6,16 @@ import django.utils.timezone class Migration(migrations.Migration): dependencies = [ - ('relaydomains', '0005_auto_20161105_1426'), + ("relaydomains", "0005_auto_20161105_1426"), ] operations = [ migrations.AlterField( - model_name='relaydomain', - name='last_modification', - field=models.DateTimeField(auto_now=True, default=django.utils.timezone.now), + model_name="relaydomain", + name="last_modification", + field=models.DateTimeField( + auto_now=True, default=django.utils.timezone.now + ), preserve_default=False, ), ] diff --git a/modoboa/relaydomains/migrations/0007_recipientaccess.py b/modoboa/relaydomains/migrations/0007_recipientaccess.py index aa2f81198..a14e02c34 100644 --- a/modoboa/relaydomains/migrations/0007_recipientaccess.py +++ b/modoboa/relaydomains/migrations/0007_recipientaccess.py @@ -5,16 +5,24 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('relaydomains', '0006_auto_20170215_0948'), + ("relaydomains", "0006_auto_20170215_0948"), ] operations = [ migrations.CreateModel( - name='RecipientAccess', + name="RecipientAccess", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('pattern', models.CharField(max_length=254, unique=True)), - ('action', models.CharField(max_length=40)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("pattern", models.CharField(max_length=254, unique=True)), + ("action", models.CharField(max_length=40)), ], ), ] diff --git a/modoboa/relaydomains/migrations/0008_auto_20171123_1653.py b/modoboa/relaydomains/migrations/0008_auto_20171123_1653.py index 1bc2fdda2..864f02723 100644 --- a/modoboa/relaydomains/migrations/0008_auto_20171123_1653.py +++ b/modoboa/relaydomains/migrations/0008_auto_20171123_1653.py @@ -17,8 +17,8 @@ def move_relaydomain_to_transport(apps, schema_editor): _settings={ "relay_target_host": rd.target_host, "relay_target_port": rd.target_port, - "relay_verify_recipients": rd.verify_recipients - } + "relay_verify_recipients": rd.verify_recipients, + }, ) rd.domain.transport = tr rd.domain.save(update_fields=["transport"]) @@ -26,7 +26,9 @@ def move_relaydomain_to_transport(apps, schema_editor): continue ra_to_create.append( RecipientAccess( - pattern=rd.domain.name, action="reject_unverified_recipient")) + pattern=rd.domain.name, action="reject_unverified_recipient" + ) + ) RecipientAccess.objects.bulk_create(ra_to_create) @@ -38,11 +40,9 @@ def forward(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('relaydomains', '0007_recipientaccess'), - ('transport', '0001_initial'), - ('admin', '0011_domain_transport'), + ("relaydomains", "0007_recipientaccess"), + ("transport", "0001_initial"), + ("admin", "0011_domain_transport"), ] - operations = [ - migrations.RunPython(move_relaydomain_to_transport, forward) - ] + operations = [migrations.RunPython(move_relaydomain_to_transport, forward)] diff --git a/modoboa/relaydomains/migrations/0009_auto_20171124_1508.py b/modoboa/relaydomains/migrations/0009_auto_20171124_1508.py index 11e03c988..579012af8 100644 --- a/modoboa/relaydomains/migrations/0009_auto_20171124_1508.py +++ b/modoboa/relaydomains/migrations/0009_auto_20171124_1508.py @@ -5,22 +5,22 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('relaydomains', '0008_auto_20171123_1653'), + ("relaydomains", "0008_auto_20171123_1653"), ] operations = [ migrations.RemoveField( - model_name='relaydomain', - name='domain', + model_name="relaydomain", + name="domain", ), migrations.RemoveField( - model_name='relaydomain', - name='service', + model_name="relaydomain", + name="service", ), migrations.DeleteModel( - name='RelayDomain', + name="RelayDomain", ), migrations.DeleteModel( - name='Service', + name="Service", ), ] diff --git a/modoboa/relaydomains/postfix_maps.py b/modoboa/relaydomains/postfix_maps.py index 074ba9e97..00b4e49c3 100644 --- a/modoboa/relaydomains/postfix_maps.py +++ b/modoboa/relaydomains/postfix_maps.py @@ -1,7 +1,7 @@ """Map file definitions for postfix.""" -class RelayDomainsMap(object): +class RelayDomainsMap(object): """Map file to list all relay domains.""" filename = "sql-relaydomains.cf" @@ -20,7 +20,6 @@ class RelayDomainsMap(object): class SplitedDomainsTransportMap(object): - """A transport map for splited domains. (ie. ones with both local and remote mailboxes) @@ -57,16 +56,9 @@ class SplitedDomainsTransportMap(object): class RelayRecipientVerification(object): - """A map file to enable recipient verification.""" filename = "sql-relay-recipient-verification.cf" - mysql = ( - "SELECT action FROM relaydomains_recipientaccess WHERE pattern='%d'" - ) - postgres = ( - "SELECT action FROM relaydomains_recipientaccess WHERE pattern='%d'" - ) - sqlite = ( - "SELECT action FROM relaydomains_recipientaccess WHERE pattern='%d'" - ) + mysql = "SELECT action FROM relaydomains_recipientaccess WHERE pattern='%d'" + postgres = "SELECT action FROM relaydomains_recipientaccess WHERE pattern='%d'" + sqlite = "SELECT action FROM relaydomains_recipientaccess WHERE pattern='%d'" diff --git a/modoboa/relaydomains/tests.py b/modoboa/relaydomains/tests.py index dd00901d4..3e2688c54 100644 --- a/modoboa/relaydomains/tests.py +++ b/modoboa/relaydomains/tests.py @@ -26,13 +26,10 @@ class Operations(object): "enabled": True, "stepid": "step3", "quota": 0, - "default_mailbox_quota": 0 + "default_mailbox_quota": 0, } values.update(kwargs) - return self.ajax_post( - reverse("admin:domain_add"), - values, status - ) + return self.ajax_post(reverse("admin:domain_add"), values, status) def _relay_domain_alias_operation(self, optype, domain, name, status=200): transport = tr_models.Transport.objects.get(pattern=domain.name) @@ -43,7 +40,7 @@ class Operations(object): "relay_target_port": transport._settings["relay_target_port"], "type": "relaydomain", "quota": domain.quota, - "default_mailbox_quota": domain.default_mailbox_quota + "default_mailbox_quota": domain.default_mailbox_quota, } aliases = [alias.name for alias in domain.domainalias_set.all()] if optype == "add": @@ -54,9 +51,7 @@ class Operations(object): fname = "aliases" if not cpt else "aliases_%d" % cpt values[fname] = alias return self.ajax_post( - reverse("admin:domain_change", - args=[domain.id]), - values, status + reverse("admin:domain_change", args=[domain.id]), values, status ) def _check_limit(self, name, curvalue, maxvalue): @@ -73,22 +68,23 @@ class RelayDomainsTestCase(ModoTestCase, Operations): super(RelayDomainsTestCase, cls).setUpTestData() admin_factories.populate_database() cls.transport = tr_factories.TransportFactory( - pattern="relaydomain.tld", service="relay", + pattern="relaydomain.tld", + service="relay", _settings={ "relay_target_host": "external.host.tld", "relay_target_port": "25", - "relay_verify_recipients": False - } + "relay_verify_recipients": False, + }, ) cls.dom = admin_factories.DomainFactory( - name="relaydomain.tld", type="relaydomain", - transport=cls.transport) - admin_factories.DomainAliasFactory( - name="relaydomainalias.tld", target=cls.dom) + name="relaydomain.tld", type="relaydomain", transport=cls.transport + ) + admin_factories.DomainAliasFactory(name="relaydomainalias.tld", target=cls.dom) admin_factories.MailboxFactory( - domain=cls.dom, address="local", + domain=cls.dom, + address="local", user__username="local@relaydomain.tld", - user__groups=("SimpleUsers", ) + user__groups=("SimpleUsers",), ) def test_domain_list_view(self): @@ -110,18 +106,17 @@ class RelayDomainsTestCase(ModoTestCase, Operations): """ self._create_relay_domain("relaydomain1.tld") transport = tr_models.Transport.objects.get(pattern="relaydomain1.tld") - self.assertEqual( - transport._settings["relay_target_host"], "external.host.tld") - self.assertEqual( - transport._settings["relay_verify_recipients"], False) + self.assertEqual(transport._settings["relay_target_host"], "external.host.tld") + self.assertEqual(transport._settings["relay_verify_recipients"], False) resp = self._create_relay_domain("test.com", 400) - self.assertEqual(resp["form_errors"]["name"][0], - "Domain with this Name already exists.") + self.assertEqual( + resp["form_errors"]["name"][0], "Domain with this Name already exists." + ) resp = self._create_relay_domain("relaydomainalias.tld", 400) self.assertEqual( resp["form_errors"]["name"][0], - "A domain alias with this name already exists" + "A domain alias with this name already exists", ) def test_create_relaydomainalias(self): @@ -132,22 +127,16 @@ class RelayDomainsTestCase(ModoTestCase, Operations): FIXME: add a check for domain alias. """ - self._relay_domain_alias_operation( - "add", self.dom, "relaydomainalias1.tld" - ) - resp = self._relay_domain_alias_operation( - "add", self.dom, "test.com", 400 - ) + self._relay_domain_alias_operation("add", self.dom, "relaydomainalias1.tld") + resp = self._relay_domain_alias_operation("add", self.dom, "test.com", 400) self.assertEqual( resp["form_errors"]["aliases_2"][0], - "A domain with this name already exists" - ) - resp = self._relay_domain_alias_operation( - "add", self.dom, self.dom.name, 400 + "A domain with this name already exists", ) + resp = self._relay_domain_alias_operation("add", self.dom, self.dom.name, 400) self.assertEqual( resp["form_errors"]["aliases_2"][0], - "A domain with this name already exists" + "A domain with this name already exists", ) def test_edit_relaydomain(self): @@ -163,26 +152,20 @@ class RelayDomainsTestCase(ModoTestCase, Operations): "relay_verify_recipients": True, "type": "relaydomain", "enabled": True, - "quota": 0, "default_mailbox_quota": 0 + "quota": 0, + "default_mailbox_quota": 0, } - self.ajax_post( - reverse("admin:domain_change", args=[self.dom.id]), - values - ) + self.ajax_post(reverse("admin:domain_change", args=[self.dom.id]), values) self.transport.refresh_from_db() - self.assertEqual( - self.transport._settings["relay_target_port"], 4040) + self.assertEqual(self.transport._settings["relay_target_port"], 4040) self.assertTrue( - models.RecipientAccess.objects.filter( - pattern=values["name"]).exists()) - values["relay_verify_recipients"] = False - self.ajax_post( - reverse("admin:domain_change", args=[self.dom.id]), - values + models.RecipientAccess.objects.filter(pattern=values["name"]).exists() ) + values["relay_verify_recipients"] = False + self.ajax_post(reverse("admin:domain_change", args=[self.dom.id]), values) self.assertFalse( - models.RecipientAccess.objects.filter( - pattern=values["name"]).exists()) + models.RecipientAccess.objects.filter(pattern=values["name"]).exists() + ) def test_relaydomain_domain_switch(self): """Check domain <-> relaydomain transitions.""" @@ -195,10 +178,9 @@ class RelayDomainsTestCase(ModoTestCase, Operations): "enabled": True, "service": "relay", "relay_target_host": self.transport._settings["relay_target_host"], - "relay_target_port": self.transport._settings["relay_target_port"] + "relay_target_port": self.transport._settings["relay_target_port"], } - self.ajax_post( - reverse("admin:domain_change", args=[domain_pk]), values) + self.ajax_post(reverse("admin:domain_change", args=[domain_pk]), values) with self.assertRaises(tr_models.Transport.DoesNotExist): self.transport.refresh_from_db() self.dom.refresh_from_db() @@ -208,13 +190,12 @@ class RelayDomainsTestCase(ModoTestCase, Operations): "type": "relaydomain", "enabled": True, "quota": 0, - "default_mailbox_quota": 0 + "default_mailbox_quota": 0, } - self.ajax_post( - reverse("admin:domain_change", args=[domain_pk]), values) + self.ajax_post(reverse("admin:domain_change", args=[domain_pk]), values) self.assertEqual( - admin_models.Domain.objects.get(name="relaydomain.tld").type, - "relaydomain") + admin_models.Domain.objects.get(name="relaydomain.tld").type, "relaydomain" + ) def test_edit_relaydomainalias(self): """Test the modification of a relay domain alias. @@ -228,22 +209,17 @@ class RelayDomainsTestCase(ModoTestCase, Operations): "relay_target_port": self.transport._settings["relay_target_port"], "type": "relaydomain", "aliases": "relaydomainalias.net", - "quota": 0, "default_mailbox_quota": 0 + "quota": 0, + "default_mailbox_quota": 0, } - self.ajax_post( - reverse("admin:domain_change", args=[self.dom.id]), - values - ) + self.ajax_post(reverse("admin:domain_change", args=[self.dom.id]), values) admin_models.DomainAlias.objects.get(name="relaydomainalias.net") with self.assertRaises(admin_models.DomainAlias.DoesNotExist): admin_models.DomainAlias.objects.get(name="relaydomainalias.tld") def test_delete_relaydomain(self): """Test the removal of a relay domain.""" - self.ajax_post( - reverse("admin:domain_delete", args=[self.dom.id]), - {} - ) + self.ajax_post(reverse("admin:domain_delete", args=[self.dom.id]), {}) with self.assertRaises(tr_models.Transport.DoesNotExist): self.transport.refresh_from_db() @@ -251,34 +227,33 @@ class RelayDomainsTestCase(ModoTestCase, Operations): """Test the removal of a recipient access.""" self.transport._settings["relay_verify_recipients"] = True self.transport.save() - self.ajax_post( - reverse("admin:domain_delete", args=[self.dom.id]), - {} - ) + self.ajax_post(reverse("admin:domain_delete", args=[self.dom.id]), {}) self.assertFalse( models.RecipientAccess.objects.filter( - pattern=self.transport.pattern).exists()) + pattern=self.transport.pattern + ).exists() + ) def test_alias_on_relaydomain(self): """Create an alias on a relay domain.""" values = { "address": "alias@relaydomain.tld", "recipients": "recipient@relaydomain.tld", - "enabled": True + "enabled": True, } self.ajax_post(reverse("admin:alias_add"), values) self.assertTrue( - admin_models.Alias.objects.filter( - address="alias@relaydomain.tld").exists()) + admin_models.Alias.objects.filter(address="alias@relaydomain.tld").exists() + ) values = { "address": "alias2@relaydomain.tld", "recipients": "local@relaydomain.tld", - "enabled": True + "enabled": True, } self.ajax_post(reverse("admin:alias_add"), values) self.assertTrue( - admin_models.Alias.objects.filter( - address="alias2@relaydomain.tld").exists()) + admin_models.Alias.objects.filter(address="alias2@relaydomain.tld").exists() + ) class ImportTestCase(ModoTestCase): @@ -286,16 +261,15 @@ class ImportTestCase(ModoTestCase): def test_webui_import(self): """Check if import from webui works.""" - f = ContentFile("relaydomain;relay.com;127.0.0.1;25;relay;True;True", - name="domains.csv") - self.client.post( - reverse("admin:domain_import"), { - "sourcefile": f - } + f = ContentFile( + "relaydomain;relay.com;127.0.0.1;25;relay;True;True", name="domains.csv" ) + self.client.post(reverse("admin:domain_import"), {"sourcefile": f}) self.assertTrue( admin_models.Domain.objects.filter( - name="relay.com", type="relaydomain").exists()) + name="relay.com", type="relaydomain" + ).exists() + ) class LimitsTestCase(ModoTestCase, Operations): @@ -306,11 +280,10 @@ class LimitsTestCase(ModoTestCase, Operations): super(LimitsTestCase, cls).setUpTestData() for name, _definition in limits_utils.get_user_limit_templates(): cls.localconfig.parameters.set_value( - "deflt_user_{0}_limit".format(name), 2, app="limits") + "deflt_user_{0}_limit".format(name), 2, app="limits" + ) cls.localconfig.save() - cls.user = UserFactory.create( - username="reseller", groups=("Resellers",) - ) + cls.user = UserFactory.create(username="reseller", groups=("Resellers",)) def setUp(self): """Initialize test.""" @@ -318,46 +291,32 @@ class LimitsTestCase(ModoTestCase, Operations): self.client.force_login(self.user) def test_relay_domains_limit(self): - self._create_relay_domain( - "relaydomain1.tld", quota=1, default_mailbox_quota=1) + self._create_relay_domain("relaydomain1.tld", quota=1, default_mailbox_quota=1) self._check_limit("domains", 1, 2) - self._create_relay_domain( - "relaydomain2.tld", quota=1, default_mailbox_quota=1) + self._create_relay_domain("relaydomain2.tld", quota=1, default_mailbox_quota=1) self._check_limit("domains", 2, 2) self._create_relay_domain("relaydomain3.tld", 403) self._check_limit("domains", 2, 2) domid = admin_models.Domain.objects.get(name="relaydomain2.tld").id - self.ajax_post( - reverse("admin:domain_delete", args=[domid]), {}) + self.ajax_post(reverse("admin:domain_delete", args=[domid]), {}) self._check_limit("domains", 1, 2) def test_relay_domain_aliases_limit(self): - self._create_relay_domain( - "relaydomain1.tld", quota=1, default_mailbox_quota=1) + self._create_relay_domain("relaydomain1.tld", quota=1, default_mailbox_quota=1) domain = admin_models.Domain.objects.get(name="relaydomain1.tld") - self._relay_domain_alias_operation( - "add", domain, "relay-domain-alias1.tld" - ) + self._relay_domain_alias_operation("add", domain, "relay-domain-alias1.tld") self._check_limit("domain_aliases", 1, 2) - self._relay_domain_alias_operation( - "add", domain, "relay-domain-alias2.tld" - ) + self._relay_domain_alias_operation("add", domain, "relay-domain-alias2.tld") self._check_limit("domain_aliases", 2, 2) self._relay_domain_alias_operation( "add", domain, "relay-domain-alias3.tld", 403 ) self._check_limit("domain_aliases", 2, 2) - self._relay_domain_alias_operation( - "delete", domain, "relay-domain-alias2.tld" - ) + self._relay_domain_alias_operation("delete", domain, "relay-domain-alias2.tld") self._check_limit("domain_aliases", 1, 2) class MapFilesTestCase(MapFilesTestCaseMixin, TestCase): - """Test case for relaydomains.""" - MAP_FILES = [ - "sql-relaydomains.cf", - "sql-relay-recipient-verification.cf" - ] + MAP_FILES = ["sql-relaydomains.cf", "sql-relay-recipient-verification.cf"] diff --git a/modoboa/relaydomains/transport.py b/modoboa/relaydomains/transport.py index a357f2b52..628be60bd 100644 --- a/modoboa/relaydomains/transport.py +++ b/modoboa/relaydomains/transport.py @@ -11,19 +11,32 @@ class RelayTransportBackend(backends.TransportBackend): name = "relay" settings = ( - {"name": "target_host", "label": _("target host address"), - "validator": "host_address"}, - {"name": "target_port", "label": _("target host port"), - "type": "int", "default": 25}, - {"name": "verify_recipients", "label": _("verify recipients"), - "type": "boolean", "required": False, "default": False}, + { + "name": "target_host", + "label": _("target host address"), + "validator": "host_address", + }, + { + "name": "target_port", + "label": _("target host port"), + "type": "int", + "default": 25, + }, + { + "name": "verify_recipients", + "label": _("verify recipients"), + "type": "boolean", + "required": False, + "default": False, + }, ) def serialize(self, transport): """Make sure next_hop is set.""" transport.next_hop = "[{}]:{}".format( transport._settings["relay_target_host"], - transport._settings["relay_target_port"]) + transport._settings["relay_target_port"], + ) backends.manager.register_backend(RelayTransportBackend) diff --git a/modoboa/test_settings.py b/modoboa/test_settings.py index 669959437..1e7457356 100644 --- a/modoboa/test_settings.py +++ b/modoboa/test_settings.py @@ -54,10 +54,7 @@ else: "HOST": "localhost", "PORT": os.environ.get("POSTGRES_PORT", "5432"), "ATOMIC_REQUESTS": True, - "OPTIONS": { - "client_encoding": "UTF8", - "sslmode": "disable" - }, + "OPTIONS": {"client_encoding": "UTF8", "sslmode": "disable"}, "TEST": { "CHARSET": "UTF8", }, diff --git a/modoboa/transport/api/v2/urls.py b/modoboa/transport/api/v2/urls.py index 816410035..3fe4372df 100644 --- a/modoboa/transport/api/v2/urls.py +++ b/modoboa/transport/api/v2/urls.py @@ -6,7 +6,6 @@ from . import viewsets router = routers.SimpleRouter() -router.register( - r"transports", viewsets.TransportViewSet, basename="transport") +router.register(r"transports", viewsets.TransportViewSet, basename="transport") urlpatterns = router.urls diff --git a/modoboa/transport/api/v2/viewsets.py b/modoboa/transport/api/v2/viewsets.py index c8f4b82f0..c8faebe60 100644 --- a/modoboa/transport/api/v2/viewsets.py +++ b/modoboa/transport/api/v2/viewsets.py @@ -12,11 +12,9 @@ from ... import backends class TransportViewSet(GetThrottleViewsetMixin, viewsets.ViewSet): """Viewset for Transport.""" - permissions = (permissions.IsAuthenticated, ) + permissions = (permissions.IsAuthenticated,) - @extend_schema( - responses={200: serializers.TransportBackendSerializer} - ) + @extend_schema(responses={200: serializers.TransportBackendSerializer}) def list(self, request, *args, **kwargs): """Return the list of all defined transport backends.""" all_settings = backends.manager.get_all_backend_settings() diff --git a/modoboa/transport/backends.py b/modoboa/transport/backends.py index ab18bfaeb..85d5fe8bb 100644 --- a/modoboa/transport/backends.py +++ b/modoboa/transport/backends.py @@ -17,7 +17,7 @@ class TransportBackend(object): """Validate an host address syntax.""" validator_list = [ lib_validators.validate_hostname, - validators.validate_ipv46_address + validators.validate_ipv46_address, ] for validator in validator_list: try: @@ -77,11 +77,7 @@ class TransportBackendManager(object): def get_backend_list(self): """Return known backend list.""" return sorted( - ( - (key, key) - for key, backend in self.backends.items() - ), - key=lambda i: i[1] + ((key, key) for key, backend in self.backends.items()), key=lambda i: i[1] ) def get_backend_settings(self, name): diff --git a/modoboa/transport/constants.py b/modoboa/transport/constants.py index 80c50d66b..601e5755b 100644 --- a/modoboa/transport/constants.py +++ b/modoboa/transport/constants.py @@ -1,5 +1,3 @@ """Transport constants.""" -PERMISSIONS = { - -} +PERMISSIONS = {} diff --git a/modoboa/transport/factories.py b/modoboa/transport/factories.py index 332bd400a..37ecc8ffe 100644 --- a/modoboa/transport/factories.py +++ b/modoboa/transport/factories.py @@ -10,7 +10,7 @@ class TransportFactory(factory.django.DjangoModelFactory): class Meta: model = models.Transport - django_get_or_create = ("pattern", ) + django_get_or_create = ("pattern",) pattern = factory.Sequence(lambda n: "transport{}".format(n)) service = "relay" diff --git a/modoboa/transport/forms.py b/modoboa/transport/forms.py index 9d7ec1787..cfe61ba28 100644 --- a/modoboa/transport/forms.py +++ b/modoboa/transport/forms.py @@ -7,7 +7,7 @@ from . import backends, models TYPE_TO_FIELD_MAP = { "int": forms.IntegerField, "boolean": forms.BooleanField, - "string": forms.CharField + "string": forms.CharField, } @@ -32,7 +32,8 @@ class BackendSettingsMixin(object): if "widget" in setting: options["widget"] = setting["widget"] self.fields[fullname] = TYPE_TO_FIELD_MAP[ftype]( - label=setting["label"], required=False, **options) + label=setting["label"], required=False, **options + ) self.setting_field_names.append(fullname) def clean_backend_fields(self, name): @@ -45,8 +46,7 @@ class BackendSettingsMixin(object): """Set settings to JSON field.""" transport = super(BackendSettingsMixin, self).save(commit=False) transport._settings = { - name: self.cleaned_data[name] - for name in self.setting_field_names + name: self.cleaned_data[name] for name in self.setting_field_names } if commit: transport.save() diff --git a/modoboa/transport/migrations/0001_initial.py b/modoboa/transport/migrations/0001_initial.py index 450dc0d69..001c8b0da 100644 --- a/modoboa/transport/migrations/0001_initial.py +++ b/modoboa/transport/migrations/0001_initial.py @@ -6,21 +6,38 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Transport', + name="Transport", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('pattern', models.CharField(max_length=254, unique=True, verbose_name='pattern')), - ('service', models.CharField(max_length=30, verbose_name='service')), - ('next_hop', models.CharField(blank=True, max_length=100, verbose_name='next hop')), - ('_settings', models.JSONField(default=dict)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "pattern", + models.CharField( + max_length=254, unique=True, verbose_name="pattern" + ), + ), + ("service", models.CharField(max_length=30, verbose_name="service")), + ( + "next_hop", + models.CharField( + blank=True, max_length=100, verbose_name="next hop" + ), + ), + ("_settings", models.JSONField(default=dict)), ], options={ - 'ordering': ['pattern'], + "ordering": ["pattern"], }, ), ] diff --git a/modoboa/transport/migrations/0002_auto_20180928_1520.py b/modoboa/transport/migrations/0002_auto_20180928_1520.py index c44052197..4fe35cb61 100644 --- a/modoboa/transport/migrations/0002_auto_20180928_1520.py +++ b/modoboa/transport/migrations/0002_auto_20180928_1520.py @@ -5,13 +5,15 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('transport', '0001_initial'), + ("transport", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='transport', - name='pattern', - field=models.CharField(db_index=True, max_length=253, unique=True, verbose_name='pattern'), + model_name="transport", + name="pattern", + field=models.CharField( + db_index=True, max_length=253, unique=True, verbose_name="pattern" + ), ), ] diff --git a/modoboa/transport/migrations/0003_alter_transport__settings.py b/modoboa/transport/migrations/0003_alter_transport__settings.py index 79982d0d7..e1c715858 100644 --- a/modoboa/transport/migrations/0003_alter_transport__settings.py +++ b/modoboa/transport/migrations/0003_alter_transport__settings.py @@ -7,18 +7,20 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('transport', '0002_auto_20180928_1520'), + ("transport", "0002_auto_20180928_1520"), ] operations = [ migrations.AlterField( - model_name='transport', - name='_settings', + model_name="transport", + name="_settings", field=models.JSONField(default=dict), ), migrations.AlterField( - model_name='transport', - name='_settings', - field=models.JSONField(default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + model_name="transport", + name="_settings", + field=models.JSONField( + default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder + ), ), ] diff --git a/modoboa/transport/models.py b/modoboa/transport/models.py index edd96fb58..67f79da61 100644 --- a/modoboa/transport/models.py +++ b/modoboa/transport/models.py @@ -12,8 +12,7 @@ from . import backends class Transport(models.Model): """Transport table.""" - pattern = models.CharField( - _("pattern"), unique=True, max_length=253, db_index=True) + pattern = models.CharField(_("pattern"), unique=True, max_length=253, db_index=True) service = models.CharField(_("service"), max_length=30) next_hop = models.CharField(_("next hop"), max_length=100, blank=True) diff --git a/modoboa/transport/postfix_maps.py b/modoboa/transport/postfix_maps.py index 0be6fdcc2..35cb3caa8 100644 --- a/modoboa/transport/postfix_maps.py +++ b/modoboa/transport/postfix_maps.py @@ -1,5 +1,6 @@ """Map file definitions for postfix.""" + class TransportMap(object): """A transport map.""" diff --git a/modoboa/urls.py b/modoboa/urls.py index 9eaee8196..9f240ee04 100644 --- a/modoboa/urls.py +++ b/modoboa/urls.py @@ -9,7 +9,9 @@ from django.urls import include, path from django.views.i18n import JavaScriptCatalog from drf_spectacular.views import ( - SpectacularJSONAPIView, SpectacularSwaggerView, SpectacularRedocView + SpectacularJSONAPIView, + SpectacularSwaggerView, + SpectacularRedocView, ) from modoboa.admin.views import user as user_views @@ -17,39 +19,47 @@ from modoboa.core import signals as core_signals, views as core_views from modoboa.core.extensions import exts_pool urlpatterns = [ - path('jsi18n/', JavaScriptCatalog.as_view(), name="javascript-catalog"), - - path('ckeditor/upload/', login_required(cku_views.upload), - name="ckeditor_upload"), - path('ckeditor/browse/', login_required(cku_views.browse), - name="ckeditor_browse"), - - path('', include("modoboa.core.urls")), - path('admin/', include("modoboa.admin.urls")), - path('dnstools/', include("modoboa.dnstools.urls")), - path('pdfcredentials', include("modoboa.pdfcredentials.urls")), - path('stats/', include("modoboa.maillog.urls")), - path('dmarc/', include("modoboa.dmarc.urls")), - path('imap_migration/', include("modoboa.imap_migration.urls")), - - path('user/forward/', user_views.forward, name="user_forward"), - - path('accounts/password_reset/', core_views.PasswordResetView.as_view(), - name="password_reset"), - path('accounts/password_reset/done/', - auth_views.PasswordResetDoneView.as_view(), - name="password_reset_done"), - path('reset/confirm_code/', - core_views.VerifySMSCodeView.as_view(), - name="password_reset_confirm_code"), - path('reset/resend_code/', - core_views.ResendSMSCodeView.as_view(), - name="password_reset_resend_code"), - path('reset///', - auth_views.PasswordResetConfirmView.as_view(), - name="password_reset_confirm"), - path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), - name="password_reset_complete"), + path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"), + path("ckeditor/upload/", login_required(cku_views.upload), name="ckeditor_upload"), + path("ckeditor/browse/", login_required(cku_views.browse), name="ckeditor_browse"), + path("", include("modoboa.core.urls")), + path("admin/", include("modoboa.admin.urls")), + path("dnstools/", include("modoboa.dnstools.urls")), + path("pdfcredentials", include("modoboa.pdfcredentials.urls")), + path("stats/", include("modoboa.maillog.urls")), + path("dmarc/", include("modoboa.dmarc.urls")), + path("imap_migration/", include("modoboa.imap_migration.urls")), + path("user/forward/", user_views.forward, name="user_forward"), + path( + "accounts/password_reset/", + core_views.PasswordResetView.as_view(), + name="password_reset", + ), + path( + "accounts/password_reset/done/", + auth_views.PasswordResetDoneView.as_view(), + name="password_reset_done", + ), + path( + "reset/confirm_code/", + core_views.VerifySMSCodeView.as_view(), + name="password_reset_confirm_code", + ), + path( + "reset/resend_code/", + core_views.ResendSMSCodeView.as_view(), + name="password_reset_resend_code", + ), + path( + "reset///", + auth_views.PasswordResetConfirmView.as_view(), + name="password_reset_confirm", + ), + path( + "reset/done/", + auth_views.PasswordResetCompleteView.as_view(), + name="password_reset_complete", + ), ] exts_pool.load_all() @@ -57,39 +67,46 @@ urlpatterns += exts_pool.get_urls() extra_routes = core_signals.extra_uprefs_routes.send(sender="urls") if extra_routes: - extra_routes = reduce( - lambda a, b: a + b, [route[1] for route in extra_routes]) + extra_routes = reduce(lambda a, b: a + b, [route[1] for route in extra_routes]) urlpatterns += extra_routes # API urls urlpatterns += [ # FIXME: legacy, to remove ASAP - path('docs/openapi.json', - SpectacularJSONAPIView.as_view(api_version="v1"), name='schema-v1-legacy'), - - path('api/schema-v1/', - SpectacularJSONAPIView.as_view(api_version="v1"), name='schema-v1'), - path('api/schema-v1/swagger/', - SpectacularSwaggerView.as_view(url_name='schema-v1'), - name='docs-index-v1'), - path('api/schema-v1/redoc/', - SpectacularRedocView.as_view(url_name='schema-v1')), - - path('api/schema-v2/', - SpectacularJSONAPIView.as_view(api_version="v2"), name='schema-v2'), - path('api/schema-v2/swagger/', - SpectacularSwaggerView.as_view(url_name='schema-v2'), - name='docs-index-v2'), - path('api/schema-v2/redoc/', - SpectacularRedocView.as_view(url_name='schema-v2')), - path('api/v1/', include("modoboa.urls_api_v1", namespace="v1")), - path('api/v2/', include("modoboa.urls_api_v2", namespace="v2")), + path( + "docs/openapi.json", + SpectacularJSONAPIView.as_view(api_version="v1"), + name="schema-v1-legacy", + ), + path( + "api/schema-v1/", + SpectacularJSONAPIView.as_view(api_version="v1"), + name="schema-v1", + ), + path( + "api/schema-v1/swagger/", + SpectacularSwaggerView.as_view(url_name="schema-v1"), + name="docs-index-v1", + ), + path("api/schema-v1/redoc/", SpectacularRedocView.as_view(url_name="schema-v1")), + path( + "api/schema-v2/", + SpectacularJSONAPIView.as_view(api_version="v2"), + name="schema-v2", + ), + path( + "api/schema-v2/swagger/", + SpectacularSwaggerView.as_view(url_name="schema-v2"), + name="docs-index-v2", + ), + path("api/schema-v2/redoc/", SpectacularRedocView.as_view(url_name="schema-v2")), + path("api/v1/", include("modoboa.urls_api_v1", namespace="v1")), + path("api/v2/", include("modoboa.urls_api_v2", namespace="v2")), ] if settings.DEBUG: from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.conf.urls.static import static + urlpatterns += staticfiles_urlpatterns() - urlpatterns += static( - settings.MEDIA_URL, document_root=settings.MEDIA_ROOT - ) + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/modoboa/urls_api_v1.py b/modoboa/urls_api_v1.py index 7697ba96b..36863abd2 100644 --- a/modoboa/urls_api_v1.py +++ b/modoboa/urls_api_v1.py @@ -7,10 +7,10 @@ from modoboa.core.extensions import exts_pool app_name = "api" urlpatterns = [ - path('', include("modoboa.core.api.v1.urls")), - path('', include("modoboa.admin.api.v1.urls")), - path('', include("modoboa.limits.api.v1.urls")), - path('', include("modoboa.relaydomains.api.v1.urls")) + path("", include("modoboa.core.api.v1.urls")), + path("", include("modoboa.admin.api.v1.urls")), + path("", include("modoboa.limits.api.v1.urls")), + path("", include("modoboa.relaydomains.api.v1.urls")), ] urlpatterns += exts_pool.get_urls(category="api") diff --git a/modoboa/urls_api_v2.py b/modoboa/urls_api_v2.py index 91ab85b39..9da57ae1a 100644 --- a/modoboa/urls_api_v2.py +++ b/modoboa/urls_api_v2.py @@ -7,17 +7,17 @@ from modoboa.core.extensions import exts_pool app_name = "api" urlpatterns = [ - path('', include("modoboa.core.api.v2.urls")), - path('', include("modoboa.admin.api.v2.urls")), - path('', include("modoboa.parameters.api.v2.urls")), - path('', include("modoboa.imap_migration.api.v2.urls")), - path('', include("modoboa.limits.api.v1.urls")), - path('', include("modoboa.relaydomains.api.v1.urls")), - path('', include("modoboa.dmarc.api.v2.urls")), - path('', include("modoboa.dnstools.api.v2.urls")), - path('', include("modoboa.maillog.api.v2.urls")), - path('', include("modoboa.transport.api.v2.urls")), - path('', include("modoboa.pdfcredentials.api.v2.urls")) + path("", include("modoboa.core.api.v2.urls")), + path("", include("modoboa.admin.api.v2.urls")), + path("", include("modoboa.parameters.api.v2.urls")), + path("", include("modoboa.imap_migration.api.v2.urls")), + path("", include("modoboa.limits.api.v1.urls")), + path("", include("modoboa.relaydomains.api.v1.urls")), + path("", include("modoboa.dmarc.api.v2.urls")), + path("", include("modoboa.dnstools.api.v2.urls")), + path("", include("modoboa.maillog.api.v2.urls")), + path("", include("modoboa.transport.api.v2.urls")), + path("", include("modoboa.pdfcredentials.api.v2.urls")), ] urlpatterns += exts_pool.get_urls(category="api") diff --git a/modoboa/version.py b/modoboa/version.py index 09e8459f7..bbb35225e 100644 --- a/modoboa/version.py +++ b/modoboa/version.py @@ -7,10 +7,10 @@ def local_scheme(version): def get_version(): from setuptools_scm import get_version as default_version - github_version = os.environ.get('GITHUB_REF_NAME', None) - github_type = os.environ.get('GITHUB_REF_TYPE', None) + + github_version = os.environ.get("GITHUB_REF_NAME", None) + github_type = os.environ.get("GITHUB_REF_TYPE", None) if github_version is not None and github_type == "tag": print(f"GITHUB_REF_NAME found, using version: {github_version}") return github_version return default_version(local_scheme=local_scheme) - diff --git a/setup.py b/setup.py index 76446487b..c5c91f3eb 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,8 @@ if __name__ == "__main__": INSTALL_REQUIRES = get_requirements(path.join(HERE, "requirements.txt")) MYSQL_REQUIRES = get_requirements(path.join(HERE, "mysql-requirements.txt")) POSTGRESQL_REQUIRES = get_requirements( - path.join(HERE, "postgresql-requirements.txt")) + path.join(HERE, "postgresql-requirements.txt") + ) LDAP_REQUIRES = get_requirements(path.join(HERE, "ldap-requirements.txt")) with open(path.join(HERE, "README.rst")) as readme: @@ -47,7 +48,7 @@ if __name__ == "__main__": def local_scheme(version): """Skip the local version (eg. +xyz of 0.6.1.dev4+gdf99fe2) - to be able to upload to Test PyPI""" + to be able to upload to Test PyPI""" return "" setup( @@ -74,14 +75,13 @@ if __name__ == "__main__": ], keywords="email", packages=find_packages( - exclude=["doc", "test_data", "test_project", "frontend"]), + exclude=["doc", "test_data", "test_project", "frontend"] + ), include_package_data=True, zip_safe=False, scripts=["bin/modoboa-admin.py"], install_requires=INSTALL_REQUIRES, - use_scm_version={ - "local_scheme": local_scheme - }, + use_scm_version={"local_scheme": local_scheme}, python_requires=">=3.7", setup_requires=["setuptools_scm>6.4"], extras_require={ diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py index 8a7299947..10b10b2a9 100644 --- a/test_project/test_project/settings.py +++ b/test_project/test_project/settings.py @@ -22,31 +22,31 @@ BASE_DIR = os.path.realpath(os.path.dirname(os.path.dirname(__file__))) # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '!8o(-dbbl3e+*bh7nx-^xysdt)1gso*%@4ze4-9_9o+i&t--u_' +SECRET_KEY = "!8o(-dbbl3e+*bh7nx-^xysdt)1gso*%@4ze4-9_9o+i&t--u_" # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = 'DEBUG' in os.environ +DEBUG = "DEBUG" in os.environ ALLOWED_HOSTS = [ - 'api', - '127.0.0.1', - 'localhost', + "api", + "127.0.0.1", + "localhost", ] -INTERNAL_IPS = ['127.0.0.1'] +INTERNAL_IPS = ["127.0.0.1"] SITE_ID = 1 -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # A list of all the people who get code error notifications. When DEBUG=False # and a view raises an exception, Django will email these people with the full # exception information. # See https://docs.djangoproject.com/en/dev/ref/settings/#admins -#ADMINS = [('Administrator', 'admin@example.net')] +# ADMINS = [('Administrator', 'admin@example.net')] # The email address that error messages come from, such as those sent to ADMINS -#SERVER_EMAIL = 'webmaster@example.net' +# SERVER_EMAIL = 'webmaster@example.net' # Security settings @@ -57,43 +57,43 @@ SESSION_COOKIE_SECURE = False # Application definition INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.sites', - 'django.contrib.staticfiles', - 'reversion', - 'ckeditor', - 'ckeditor_uploader', - 'rest_framework', - 'rest_framework.authtoken', - 'drf_spectacular', - 'django_otp', - 'django_otp.plugins.otp_totp', - 'django_otp.plugins.otp_static', - 'django_rename_app', - 'django_rq', + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.sites", + "django.contrib.staticfiles", + "reversion", + "ckeditor", + "ckeditor_uploader", + "rest_framework", + "rest_framework.authtoken", + "drf_spectacular", + "django_otp", + "django_otp.plugins.otp_totp", + "django_otp.plugins.otp_static", + "django_rename_app", + "django_rq", ) # A dedicated place to register Modoboa applications # Do not delete it. # Do not change the order. MODOBOA_APPS = ( - 'modoboa', - 'modoboa.core', - 'modoboa.lib', - 'modoboa.admin', - 'modoboa.transport', - 'modoboa.relaydomains', - 'modoboa.limits', - 'modoboa.parameters', - 'modoboa.dnstools', - 'modoboa.policyd', - 'modoboa.maillog', - 'modoboa.pdfcredentials', - 'modoboa.dmarc', - 'modoboa.imap_migration', + "modoboa", + "modoboa.core", + "modoboa.lib", + "modoboa.admin", + "modoboa.transport", + "modoboa.relaydomains", + "modoboa.limits", + "modoboa.parameters", + "modoboa.dnstools", + "modoboa.policyd", + "modoboa.maillog", + "modoboa.pdfcredentials", + "modoboa.dmarc", + "modoboa.imap_migration", # Modoboa extensions here. ) @@ -106,30 +106,30 @@ else: INSTALLED_APPS += MODOBOA_APPS -AUTH_USER_MODEL = 'core.User' +AUTH_USER_MODEL = "core.User" MIDDLEWARE = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'x_forwarded_for.middleware.XForwardedForMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django_otp.middleware.OTPMiddleware', - 'modoboa.core.middleware.TwoFAMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'modoboa.core.middleware.LocalConfigMiddleware', - 'modoboa.lib.middleware.AjaxLoginRedirect', - 'modoboa.lib.middleware.CommonExceptionCatcher', - 'modoboa.lib.middleware.RequestCatcherMiddleware', + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "x_forwarded_for.middleware.XForwardedForMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django_otp.middleware.OTPMiddleware", + "modoboa.core.middleware.TwoFAMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "modoboa.core.middleware.LocalConfigMiddleware", + "modoboa.lib.middleware.AjaxLoginRedirect", + "modoboa.lib.middleware.CommonExceptionCatcher", + "modoboa.lib.middleware.RequestCatcherMiddleware", ) AUTHENTICATION_BACKENDS = ( # 'modoboa.lib.authbackends.LDAPBackend', # 'modoboa.lib.authbackends.SMTPBackend', - 'django.contrib.auth.backends.ModelBackend', - 'modoboa.imap_migration.auth_backends.IMAPBackend', + "django.contrib.auth.backends.ModelBackend", + "modoboa.imap_migration.auth_backends.IMAPBackend", ) # SMTP authentication @@ -140,37 +140,37 @@ AUTHENTICATION_BACKENDS = ( TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', - 'modoboa.core.context_processors.top_notifications', - 'modoboa.core.context_processors.new_admin_url' + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", + "modoboa.core.context_processors.top_notifications", + "modoboa.core.context_processors.new_admin_url", ], - 'debug': DEBUG, + "debug": DEBUG, }, }, ] -ROOT_URLCONF = 'test_project.urls' +ROOT_URLCONF = "test_project.urls" -WSGI_APPLICATION = 'test_project.wsgi.application' +WSGI_APPLICATION = "test_project.wsgi.application" # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ -LANGUAGE_CODE = 'en' +LANGUAGE_CODE = "en" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -181,72 +181,70 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ -STATIC_URL = '/sitestatic/' -STATIC_ROOT = os.path.join(BASE_DIR, 'sitestatic') -STATICFILES_DIRS = ( - os.path.join(BASE_DIR, '..', 'modoboa', 'bower_components'), -) +STATIC_URL = "/sitestatic/" +STATIC_ROOT = os.path.join(BASE_DIR, "sitestatic") +STATICFILES_DIRS = (os.path.join(BASE_DIR, "..", "modoboa", "bower_components"),) -MEDIA_URL = '/media/' -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_URL = "/media/" +MEDIA_ROOT = os.path.join(BASE_DIR, "media") # Rest framework settings REST_FRAMEWORK = { - 'DEFAULT_THROTTLE_RATES': { - 'user': '200/minute', - 'ddos': '10/second', - 'ddos_lesser': '200/minute', - 'login': '10/minute', - 'password_recovery_request': '11/hour', - 'password_recovery_totp_check': '25/hour', - 'password_recovery_apply': '25/hour' + "DEFAULT_THROTTLE_RATES": { + "user": "200/minute", + "ddos": "10/second", + "ddos_lesser": "200/minute", + "login": "10/minute", + "password_recovery_request": "11/hour", + "password_recovery_totp_check": "25/hour", + "password_recovery_apply": "25/hour", }, - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'modoboa.core.drf_authentication.JWTAuthenticationWith2FA', - 'rest_framework.authentication.TokenAuthentication', - 'rest_framework.authentication.SessionAuthentication', + "DEFAULT_AUTHENTICATION_CLASSES": ( + "modoboa.core.drf_authentication.JWTAuthenticationWith2FA", + "rest_framework.authentication.TokenAuthentication", + "rest_framework.authentication.SessionAuthentication", ), - 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', - 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning', + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning", } # Uncomment if you need a custom sub url for new_admin interface # NEW_ADMIN_URL = 'new-admin' SPECTACULAR_SETTINGS = { - 'SCHEMA_PATH_PREFIX': r'/api/v[0-9]', - 'TITLE': 'Modoboa API', - 'VERSION': None, - 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], + "SCHEMA_PATH_PREFIX": r"/api/v[0-9]", + "TITLE": "Modoboa API", + "VERSION": None, + "SERVE_PERMISSIONS": ["rest_framework.permissions.IsAuthenticated"], } # Modoboa settings # MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png") # DOVECOT_LOOKUP_PATH = ('/path/to/dovecot', ) -DOVECOT_USER="root" +DOVECOT_USER = "root" -MODOBOA_API_URL = 'https://api.modoboa.org/1/' +MODOBOA_API_URL = "https://api.modoboa.org/1/" -PID_FILE_STORAGE_PATH = '/tmp' +PID_FILE_STORAGE_PATH = "/tmp" # REDIS -REDIS_HOST = os.environ.get('REDIS_HOST', '127.0.0.1') -REDIS_PORT = os.environ.get('REDIS_PORT', 6379) +REDIS_HOST = os.environ.get("REDIS_HOST", "127.0.0.1") +REDIS_PORT = os.environ.get("REDIS_PORT", 6379) REDIS_QUOTA_DB = 0 -REDIS_URL = 'redis://{}:{}/{}'.format(REDIS_HOST, REDIS_PORT, REDIS_QUOTA_DB) +REDIS_URL = "redis://{}:{}/{}".format(REDIS_HOST, REDIS_PORT, REDIS_QUOTA_DB) # RQ RQ_QUEUES = { - 'dkim': { - 'URL': REDIS_URL, + "dkim": { + "URL": REDIS_URL, }, - 'modoboa': { - 'URL': REDIS_URL, + "modoboa": { + "URL": REDIS_URL, }, } @@ -254,8 +252,8 @@ RQ_QUEUES = { CACHES = { "default": { - "BACKEND": 'django.core.cache.backends.redis.RedisCache', - "LOCATION": f"redis://{REDIS_HOST}:{REDIS_PORT}" + "BACKEND": "django.core.cache.backends.redis.RedisCache", + "LOCATION": f"redis://{REDIS_HOST}:{REDIS_PORT}", } } @@ -264,25 +262,20 @@ CACHES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, { - 'NAME': 'modoboa.core.password_validation.ComplexityValidator', - 'OPTIONS': { - 'upper': 1, - 'lower': 1, - 'digits': 1, - 'specials': 0 - } + "NAME": "modoboa.core.password_validation.ComplexityValidator", + "OPTIONS": {"upper": 1, "lower": 1, "digits": 1, "specials": 0}, }, ] @@ -299,21 +292,23 @@ CKEDITOR_BROWSE_SHOW_DIRS = True CKEDITOR_ALLOW_NONIMAGE_FILES = False CKEDITOR_CONFIGS = { - 'default': { - 'allowedContent': True, - 'toolbar': 'Modoboa', - 'width': None, - 'toolbar_Modoboa': [ - ['Bold', 'Italic', 'Underline'], - ['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'], - ['BidiLtr', 'BidiRtl', 'Language'], - ['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent'], - ['Undo', 'Redo'], - ['Link', 'Unlink', 'Anchor', '-', 'Smiley'], - ['TextColor', 'BGColor', '-', 'Source'], - ['Font', 'FontSize'], - ['Image', ], - ['SpellChecker'] + "default": { + "allowedContent": True, + "toolbar": "Modoboa", + "width": None, + "toolbar_Modoboa": [ + ["Bold", "Italic", "Underline"], + ["JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyBlock"], + ["BidiLtr", "BidiRtl", "Language"], + ["NumberedList", "BulletedList", "-", "Outdent", "Indent"], + ["Undo", "Redo"], + ["Link", "Unlink", "Anchor", "-", "Smiley"], + ["TextColor", "BGColor", "-", "Source"], + ["Font", "FontSize"], + [ + "Image", + ], + ["SpellChecker"], ], }, } @@ -321,65 +316,53 @@ CKEDITOR_CONFIGS = { # Logging configuration LOGGING = { - 'version': 1, - 'formatters': { - 'syslog': { - 'format': '%(name)s: %(levelname)s %(message)s' - }, - 'django.server': { - '()': 'django.utils.log.ServerFormatter', - 'format': '[%(server_time)s] %(message)s', - } - }, - 'handlers': { - 'mail-admins': { - 'level': 'ERROR', - 'class': 'django.utils.log.AdminEmailHandler', - 'include_html': True - }, - 'syslog-auth': { - 'class': 'logging.handlers.SysLogHandler', - 'facility': SysLogHandler.LOG_AUTH, - 'formatter': 'syslog' - }, - 'modoboa': { - 'class': 'modoboa.core.loggers.SQLHandler', - }, - 'console': { - 'class': 'logging.StreamHandler' - }, - 'django.server': { - 'level': 'INFO', - 'class': 'logging.StreamHandler', - 'formatter': 'django.server', + "version": 1, + "formatters": { + "syslog": {"format": "%(name)s: %(levelname)s %(message)s"}, + "django.server": { + "()": "django.utils.log.ServerFormatter", + "format": "[%(server_time)s] %(message)s", }, }, - 'loggers': { - 'django': { - 'handlers': ['mail-admins'], - 'level': 'ERROR', - 'propagate': False + "handlers": { + "mail-admins": { + "level": "ERROR", + "class": "django.utils.log.AdminEmailHandler", + "include_html": True, }, - 'modoboa.auth': { - 'handlers': ['syslog-auth', 'modoboa'], - 'level': 'INFO', - 'propagate': False + "syslog-auth": { + "class": "logging.handlers.SysLogHandler", + "facility": SysLogHandler.LOG_AUTH, + "formatter": "syslog", }, - 'modoboa.admin': { - 'handlers': ['modoboa'], - 'level': 'INFO', - 'propagate': False + "modoboa": { + "class": "modoboa.core.loggers.SQLHandler", }, - 'django.server': { - 'handlers': ['django.server'], - 'level': 'INFO', - 'propagate': False, + "console": {"class": "logging.StreamHandler"}, + "django.server": { + "level": "INFO", + "class": "logging.StreamHandler", + "formatter": "django.server", + }, + }, + "loggers": { + "django": {"handlers": ["mail-admins"], "level": "ERROR", "propagate": False}, + "modoboa.auth": { + "handlers": ["syslog-auth", "modoboa"], + "level": "INFO", + "propagate": False, + }, + "modoboa.admin": {"handlers": ["modoboa"], "level": "INFO", "propagate": False}, + "django.server": { + "handlers": ["django.server"], + "level": "INFO", + "propagate": False, }, # 'django_auth_ldap': { # 'level': 'DEBUG', # 'handlers': ['console'] # }, - } + }, } SILENCED_SYSTEM_CHECKS = [ @@ -390,4 +373,4 @@ DISABLE_DASHBOARD_EXTERNAL_QUERIES = False # Load settings from extensions -LDAP_SERVER_PORT = os.environ.get('LDAP_SERVER_PORT', 3389) +LDAP_SERVER_PORT = os.environ.get("LDAP_SERVER_PORT", 3389) diff --git a/tests.py b/tests.py index df463c12e..db2cf2fe7 100644 --- a/tests.py +++ b/tests.py @@ -25,9 +25,14 @@ class DeployTest(unittest.TestCase): self.workdir = tempfile.mkdtemp() def test_silent(self): - dburl = "default:%s://%s:%s@%s:%s/%s" \ - % (self.dbtype, self.dbuser, self.dbpassword, - self.dbhost, self.dbport, self.projname) + dburl = "default:%s://%s:%s@%s:%s/%s" % ( + self.dbtype, + self.dbuser, + self.dbpassword, + self.dbhost, + self.dbport, + self.projname, + ) cmd = ( "modoboa-admin.py deploy --collectstatic " "--dburl %s --domain %s --admin-username admin %s"