From 98eea782a080a3cdfea5abea7d288ff3d49595c6 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 27 Nov 2017 10:42:27 +0100 Subject: [PATCH] Generic transport editor. (#1309) Generic transport editor. see #1301 * Fixed unit test. * Added unit tests. * Fixed unit test. * Added transport module to test list. * Moved relay transport backend to appropriate app. see #1301 * Added missing file. --- .gitignore | 3 + .travis.yml | 3 +- .tx/config | 12 ++ modoboa/admin/constants.py | 8 + modoboa/admin/forms/domain.py | 22 +-- modoboa/admin/models/base.py | 19 ++ modoboa/admin/models/domain.py | 33 +--- modoboa/admin/models/mailbox.py | 2 + modoboa/admin/signals.py | 4 +- modoboa/admin/tests/test_repair.py | 2 +- modoboa/admin/views/domain.py | 9 +- .../core/commands/templates/settings.py.tpl | 1 + .../management/commands/load_initial_data.py | 12 +- modoboa/core/tests/test_views.py | 3 +- modoboa/limits/forms.py | 2 +- modoboa/relaydomains/app_settings.py | 24 --- modoboa/relaydomains/apps.py | 11 +- modoboa/relaydomains/constants.py | 14 -- modoboa/relaydomains/forms.py | 67 ++++--- modoboa/relaydomains/handlers.py | 127 +++++--------- modoboa/relaydomains/lib.py | 25 ++- .../migrations/0007_recipientaccess.py | 23 +++ .../migrations/0008_auto_20171123_1653.py | 52 ++++++ .../migrations/0009_auto_20171124_1508.py | 29 ++++ modoboa/relaydomains/models.py | 135 +-------------- modoboa/relaydomains/postfix_maps.py | 70 ++------ .../static/relaydomains/js/relay_domains.js | 70 -------- .../relaydomains/relaydomain_form.html | 7 +- modoboa/relaydomains/tests.py | 154 ++++++++++------- modoboa/relaydomains/transport.py | 29 ++++ modoboa/relaydomains/urls.py | 13 -- modoboa/relaydomains/views.py | 26 --- modoboa/static/css/custom.css | 9 + modoboa/static/js/global.js | 4 +- modoboa/templates/twocols.html | 17 +- modoboa/transport/__init__.py | 1 + modoboa/transport/admin.py | 3 + modoboa/transport/apps.py | 11 ++ modoboa/transport/backends.py | 105 +++++++++++ modoboa/transport/constants.py | 5 + modoboa/transport/factories.py | 20 +++ modoboa/transport/forms.py | 94 ++++++++++ modoboa/transport/handlers.py | 31 ++++ .../transport/locale/en/LC_MESSAGES/django.po | 124 +++++++++++++ .../locale/en/LC_MESSAGES/djangojs.po | 22 +++ .../transport/locale/fr/LC_MESSAGES/django.po | 124 +++++++++++++ .../locale/fr/LC_MESSAGES/djangojs.po | 22 +++ modoboa/transport/migrations/0001_initial.py | 34 ++++ modoboa/transport/migrations/__init__.py | 0 modoboa/transport/models.py | 32 ++++ modoboa/transport/postfix_maps.py | 24 +++ modoboa/transport/serializers.py | 49 ++++++ .../static/transport/js/transport.js | 43 +++++ .../templates/transport/_transport_form.html | 14 ++ .../transport/_transport_headers.html | 5 + .../templates/transport/transport_list.html | 66 +++++++ modoboa/transport/tests.py | 163 ++++++++++++++++++ modoboa/transport/urls.py | 17 ++ modoboa/transport/urls_api.py | 11 ++ modoboa/transport/views.py | 142 +++++++++++++++ modoboa/transport/viewsets.py | 18 ++ modoboa/urls.py | 4 +- modoboa/urls_api.py | 1 + requirements.txt | 5 +- test_project/test_project/settings.py | 1 + 65 files changed, 1621 insertions(+), 611 deletions(-) delete mode 100644 modoboa/relaydomains/app_settings.py delete mode 100644 modoboa/relaydomains/constants.py create mode 100644 modoboa/relaydomains/migrations/0007_recipientaccess.py create mode 100644 modoboa/relaydomains/migrations/0008_auto_20171123_1653.py create mode 100644 modoboa/relaydomains/migrations/0009_auto_20171124_1508.py delete mode 100644 modoboa/relaydomains/static/relaydomains/js/relay_domains.js create mode 100644 modoboa/relaydomains/transport.py delete mode 100644 modoboa/relaydomains/urls.py delete mode 100644 modoboa/relaydomains/views.py create mode 100644 modoboa/transport/__init__.py create mode 100644 modoboa/transport/admin.py create mode 100644 modoboa/transport/apps.py create mode 100644 modoboa/transport/backends.py create mode 100644 modoboa/transport/constants.py create mode 100644 modoboa/transport/factories.py create mode 100644 modoboa/transport/forms.py create mode 100644 modoboa/transport/handlers.py create mode 100644 modoboa/transport/locale/en/LC_MESSAGES/django.po create mode 100644 modoboa/transport/locale/en/LC_MESSAGES/djangojs.po create mode 100644 modoboa/transport/locale/fr/LC_MESSAGES/django.po create mode 100644 modoboa/transport/locale/fr/LC_MESSAGES/djangojs.po create mode 100644 modoboa/transport/migrations/0001_initial.py create mode 100644 modoboa/transport/migrations/__init__.py create mode 100644 modoboa/transport/models.py create mode 100644 modoboa/transport/postfix_maps.py create mode 100644 modoboa/transport/serializers.py create mode 100644 modoboa/transport/static/transport/js/transport.js create mode 100644 modoboa/transport/templates/transport/_transport_form.html create mode 100644 modoboa/transport/templates/transport/_transport_headers.html create mode 100644 modoboa/transport/templates/transport/transport_list.html create mode 100644 modoboa/transport/tests.py create mode 100644 modoboa/transport/urls.py create mode 100644 modoboa/transport/urls_api.py create mode 100644 modoboa/transport/views.py create mode 100644 modoboa/transport/viewsets.py diff --git a/.gitignore b/.gitignore index a018c094f..7b725b7a5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ DEV modoboa.iml .tox modoboa_test +.coverage +build/ +dist/ diff --git a/.travis.yml b/.travis.yml index fbfb621d2..066b49c97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ cache: pip python: - "2.7" - "3.4" + - "3.6" addons: apt: packages: @@ -43,7 +44,7 @@ before_script: script: - python ./tests.py - cd test_project - - coverage run --source ../modoboa manage.py test modoboa.core modoboa.lib modoboa.admin modoboa.limits modoboa.relaydomains + - coverage run --source ../modoboa manage.py test modoboa.core modoboa.lib modoboa.admin modoboa.limits modoboa.transport modoboa.relaydomains after_success: - codecov diff --git a/.tx/config b/.tx/config index 40b7b23c7..9e7eef446 100644 --- a/.tx/config +++ b/.tx/config @@ -42,3 +42,15 @@ file_filter = modoboa/admin/locale//LC_MESSAGES/djangojs.po source_file = modoboa/admin/locale/en/LC_MESSAGES/djangojs.po source_lang = en type = PO + +[modoboa.transport-djangopo] +file_filter = modoboa/transport/locale//LC_MESSAGES/django.po +source_file = modoboa/transport/locale/en/LC_MESSAGES/django.po +source_lang = en +type = PO + +[modoboa.transport-djangojspo] +file_filter = modoboa/transport/locale//LC_MESSAGES/djangojs.po +source_file = modoboa/transport/locale/en/LC_MESSAGES/djangojs.po +source_lang = en +type = PO diff --git a/modoboa/admin/constants.py b/modoboa/admin/constants.py index 7a78b2cc0..42fdc1277 100644 --- a/modoboa/admin/constants.py +++ b/modoboa/admin/constants.py @@ -2,6 +2,9 @@ from __future__ import unicode_literals +from django.utils.translation import ugettext_lazy as _ + + DNSBL_PROVIDERS = [ "aspews.ext.sorbs.net", "b.barracudacentral.org", @@ -91,3 +94,8 @@ RESERVED_TLD = [ "localhost", "test" ] + +DOMAIN_TYPES = [ + ("domain", _("Domain")), + ("relaydomain", _("Relay domain")), +] diff --git a/modoboa/admin/forms/domain.py b/modoboa/admin/forms/domain.py index c93b7439a..23e2e9343 100644 --- a/modoboa/admin/forms/domain.py +++ b/modoboa/admin/forms/domain.py @@ -20,6 +20,7 @@ from modoboa.lib.form_utils import ( from modoboa.lib.web_utils import render_to_json_response from modoboa.parameters import tools as param_tools +from .. import constants from .. import lib from .. import signals @@ -28,11 +29,6 @@ from ..models import ( ) -DOMAIN_TYPES = [ - ("domain", _("Domain")), -] - - class DomainFormGeneral(forms.ModelForm, DynamicForm): """A form to create/edit a domain.""" @@ -63,12 +59,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): self.fields["quota"].initial = params["default_domain_quota"] self.fields["default_mailbox_quota"].initial = ( params["default_mailbox_quota"]) - extra_domain_types = reduce( - lambda a, b: a + b, - [result[1] for result in signals.extra_domain_types.send( - sender=self.__class__)] - ) - self.fields["type"].choices = DOMAIN_TYPES + extra_domain_types + self.fields["type"].choices = constants.DOMAIN_TYPES self.field_widths = { "quota": 3, "default_mailbox_quota": 3 @@ -382,10 +373,13 @@ class DomainForm(TabForms): first_form = self.forms[0]["instance"] options = {} if isinstance(first_form, DomainFormGeneral): - options["domalias_post_create"] = True - first_form.save(self.request.user, **options) + domain = first_form.save( + self.request.user, domalias_post_create=True) + options.update({"domain": domain}) + else: + first_form.save(self.request.user) for f in self.forms[1:]: - f["instance"].save(self.request.user) + f["instance"].save(self.request.user, **options) def done(self): return render_to_json_response(_("Domain modified")) diff --git a/modoboa/admin/models/base.py b/modoboa/admin/models/base.py index f3fb46848..9ab6fc6c8 100644 --- a/modoboa/admin/models/base.py +++ b/modoboa/admin/models/base.py @@ -5,11 +5,27 @@ from __future__ import unicode_literals from django.db import models from django.utils import timezone +from django.contrib.contenttypes.fields import GenericRelation + +from modoboa.core import models as core_models from modoboa.lib.permissions import ( grant_access_to_object, ungrant_access_to_object ) +class AdminObjectManager(models.Manager): + + def get_for_admin(self, admin): + """Return the objects belonging to this admin + + The result is a ``QuerySet`` object, so this function can be used + to fill ``ModelChoiceField`` objects. + """ + if admin.is_superuser: + return self.get_queryset() + return self.get_queryset().filter(owners__user=admin) + + class AdminObject(models.Model): """Abstract model to support dates. @@ -20,8 +36,11 @@ class AdminObject(models.Model): creation = models.DateTimeField(default=timezone.now) last_modification = models.DateTimeField(auto_now=True) + owners = GenericRelation(core_models.ObjectAccess) _objectname = None + objects = AdminObjectManager() + class Meta: abstract = True diff --git a/modoboa/admin/models/domain.py b/modoboa/admin/models/domain.py index c5579c1a3..eee336dbf 100644 --- a/modoboa/admin/models/domain.py +++ b/modoboa/admin/models/domain.py @@ -3,10 +3,8 @@ from __future__ import unicode_literals import datetime -from functools import reduce from django.db import models -from django.db.models.manager import Manager from django.utils import timezone from django.utils.encoding import ( python_2_unicode_compatible, smart_text, force_text @@ -14,31 +12,15 @@ from django.utils.encoding import ( from django.utils.functional import cached_property from django.utils.translation import ugettext as _, ugettext_lazy -from django.contrib.contenttypes.fields import GenericRelation - from reversion import revisions as reversion from modoboa.core import signals as core_signals -from modoboa.core.models import User, ObjectAccess +from modoboa.core.models import User from modoboa.lib.exceptions import BadRequest, Conflict from modoboa.parameters import tools as param_tools from .base import AdminObject from .. import constants -from .. import signals - - -class DomainManager(Manager): - - def get_for_admin(self, admin): - """Return the domains belonging to this admin - - The result is a ``QuerySet`` object, so this function can be used - to fill ``ModelChoiceField`` objects. - """ - if admin.is_superuser: - return self.get_queryset() - return self.get_queryset().filter(owners__user=admin) @python_2_unicode_compatible @@ -67,15 +49,12 @@ class Domain(AdminObject): help_text=ugettext_lazy("Check to activate this domain"), default=True ) - owners = GenericRelation(ObjectAccess) type = models.CharField(default="domain", max_length=20) enable_dns_checks = models.BooleanField( ugettext_lazy("Enable DNS checks"), default=True, help_text=ugettext_lazy("Check to enable DNS checks for this domain") ) - objects = DomainManager() - class Meta: permissions = ( ("view_domain", "View domain"), @@ -111,11 +90,11 @@ class Domain(AdminObject): @property def tags(self): - if self.type == "domain": - return [{"name": "domain", "label": _("Domain"), "type": "dom"}] - results = signals.get_domain_tags.send( - sender=self.__class__, domain=self) - return reduce(lambda a, b: a + b, [result[1] for result in results]) + label = "" + for dt in constants.DOMAIN_TYPES: + if self.type == dt[0]: + label = dt[1] + return [{"name": self.type, "label": label, "type": "dom"}] @property def admins(self): diff --git a/modoboa/admin/models/mailbox.py b/modoboa/admin/models/mailbox.py index 10bc59a9c..1cf18320d 100644 --- a/modoboa/admin/models/mailbox.py +++ b/modoboa/admin/models/mailbox.py @@ -345,6 +345,7 @@ class Mailbox(AdminObject): username=self.full_address) super(Mailbox, self).save(*args, **kwargs) + reversion.register(Mailbox) @@ -365,6 +366,7 @@ class SenderAddress(models.Model): """Return address.""" return smart_text(self.address) + reversion.register(SenderAddress) diff --git a/modoboa/admin/signals.py b/modoboa/admin/signals.py index 79f2ed116..facb879b9 100644 --- a/modoboa/admin/signals.py +++ b/modoboa/admin/signals.py @@ -21,12 +21,12 @@ extra_domain_forms = django.dispatch.Signal(providing_args=["user", "domain"]) extra_domain_menu_entries = django.dispatch.Signal(providing_args=["user"]) extra_domain_qset_filters = django.dispatch.Signal( providing_args=["domfilter", "extrafilters"]) -extra_domain_types = django.dispatch.Signal() +# extra_domain_types = django.dispatch.Signal() extra_domain_wizard_steps = django.dispatch.Signal() get_account_form_instances = django.dispatch.Signal( providing_args=["user", "account"]) get_domain_form_instances = django.dispatch.Signal( providing_args=["user", "domain"]) -get_domain_tags = django.dispatch.Signal(providing_args=["domain"]) +# get_domain_tags = django.dispatch.Signal(providing_args=["domain"]) import_object = django.dispatch.Signal(providing_args=["objtype"]) use_external_recipients = django.dispatch.Signal(providing_args=["recipients"]) diff --git a/modoboa/admin/tests/test_repair.py b/modoboa/admin/tests/test_repair.py index aa4ce47fd..e3e3b67ef 100644 --- a/modoboa/admin/tests/test_repair.py +++ b/modoboa/admin/tests/test_repair.py @@ -57,7 +57,7 @@ class RepairTestCase(ModoTestCase): """Check that problem is fixed.""" count, detail = models.Alias.objects.filter( address="user@test.com", internal=True).delete() - self.assertEqual(count, 2) + self.assertEqual(count, 3) ret = management.call_command("modo", "repair", "--quiet") assert ret is None self.assertTrue( diff --git a/modoboa/admin/views/domain.py b/modoboa/admin/views/domain.py index 8fada95f7..8cb51fabe 100644 --- a/modoboa/admin/views/domain.py +++ b/modoboa/admin/views/domain.py @@ -45,11 +45,10 @@ def index(request): ) def _domains(request): sort_order, sort_dir = get_sort_order(request.GET, "name") - extra_filters = reduce( - lambda a, b: a + b, - [result[1] for result in - signals.extra_domain_filters.send(sender="_domains")] - ) + 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]) filters = dict( (flt, request.GET.get(flt, None)) for flt in ["domfilter", "searchquery"] + extra_filters diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index a1ed35115..c61596c2e 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -63,6 +63,7 @@ MODOBOA_APPS = ( 'modoboa.core', 'modoboa.lib', 'modoboa.admin', + 'modoboa.transport', 'modoboa.relaydomains', 'modoboa.limits', 'modoboa.parameters', diff --git a/modoboa/core/management/commands/load_initial_data.py b/modoboa/core/management/commands/load_initial_data.py index 00c357764..f9eed3395 100644 --- a/modoboa/core/management/commands/load_initial_data.py +++ b/modoboa/core/management/commands/load_initial_data.py @@ -14,7 +14,6 @@ from django.core.management.base import BaseCommand from modoboa.lib.cryptutils import random_key from modoboa.lib.permissions import add_permissions_to_group -import modoboa.relaydomains.models as relay_models from ... import constants from ... import extensions @@ -58,18 +57,15 @@ class Command(BaseCommand): lc.parameters.set_value("secret_key", random_key()) lc.save() - for service_name in ["relay", "smtp"]: - relay_models.Service.objects.get_or_create(name=service_name) - 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) - permissions = ( - constants.PERMISSIONS.get(groupname, []) + - reduce(lambda a, b: a + b, [result[1] for result in results]) - ) + permissions = constants.PERMISSIONS.get(groupname, []) + if results: + permissions += reduce( + lambda a, b: a + b, [result[1] for result in results]) if not permissions: continue add_permissions_to_group(group, permissions) diff --git a/modoboa/core/tests/test_views.py b/modoboa/core/tests/test_views.py index d75dae290..8e06bdfce 100644 --- a/modoboa/core/tests/test_views.py +++ b/modoboa/core/tests/test_views.py @@ -120,7 +120,6 @@ class SettingsTestCase(ModoTestCase): "core-ldap_bind_password": "", "limits-deflt_domain_domain_aliases_limit": "0", "core-secret_key": ":?j3]QPWo!.'_c4n", - "relaydomains-master_cf_path": "/etc/postfix/master.cf", "limits-deflt_domain_domain_admins_limit": "0", "limits-enable_admin_limits": "True", "core-ldap_bind_dn": "", @@ -166,7 +165,7 @@ class SettingsTestCase(ModoTestCase): """Test settings display.""" url = reverse("core:parameters") response = self.ajax_get(url) - for app in ["core", "admin", "limits", "relaydomains"]: + for app in ["core", "admin", "limits"]: self.assertIn('data-app="{}"'.format(app), response["content"]) def test_save_settings(self): diff --git a/modoboa/limits/forms.py b/modoboa/limits/forms.py index a30d86f88..3a013a6f2 100644 --- a/modoboa/limits/forms.py +++ b/modoboa/limits/forms.py @@ -92,7 +92,7 @@ class DomainLimitsForm(forms.Form): self.add_error(lname, _("Invalid limit")) return cleaned_data - def save(self, user): + def save(self, user, **kwargs): """Set limits.""" for name, ltpl in utils.get_domain_limit_templates(): fieldname = "{}_limit".format(name) diff --git a/modoboa/relaydomains/app_settings.py b/modoboa/relaydomains/app_settings.py deleted file mode 100644 index ac892234e..000000000 --- a/modoboa/relaydomains/app_settings.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Relay domains settings definition.""" - -from __future__ import unicode_literals - -from django import forms -from django.utils.translation import ugettext_lazy - -from modoboa.lib.form_utils import SeparatorField -from modoboa.parameters import forms as param_forms - - -class AdminParametersForm(param_forms.AdminParametersForm): - """Admin parameters definition.""" - - app = "relaydomains" - - general_sep = SeparatorField(label=ugettext_lazy("General")) - - master_cf_path = forms.CharField( - label=ugettext_lazy("Postfix's master.cf path"), - initial="/etc/postfix/master.cf", - help_text=ugettext_lazy('Path to the master.cf configuration file'), - widget=forms.TextInput(attrs={"class": "form-control"}) - ) diff --git a/modoboa/relaydomains/apps.py b/modoboa/relaydomains/apps.py index 937456f2b..8a0236773 100644 --- a/modoboa/relaydomains/apps.py +++ b/modoboa/relaydomains/apps.py @@ -3,15 +3,6 @@ from __future__ import unicode_literals from django.apps import AppConfig -from django.utils.translation import ugettext as _ - - -def load_relaydomains_settings(): - """Load application settings.""" - from modoboa.parameters import tools as param_tools - from .app_settings import AdminParametersForm - - param_tools.registry.add("global", AdminParametersForm, _("Relay domains")) class RelayDomainsConfig(AppConfig): @@ -22,5 +13,5 @@ class RelayDomainsConfig(AppConfig): verbose_name = "Modoboa relay domains" def ready(self): - load_relaydomains_settings() from . import handlers + from . import transport diff --git a/modoboa/relaydomains/constants.py b/modoboa/relaydomains/constants.py deleted file mode 100644 index 1e4b24230..000000000 --- a/modoboa/relaydomains/constants.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Relaydomains app constants.""" - -from __future__ import unicode_literals - -PERMISSIONS = { - "Resellers": [ - ("relaydomains", "relaydomain", "add_relaydomain"), - ("relaydomains", "relaydomain", "change_relaydomain"), - ("relaydomains", "relaydomain", "delete_relaydomain"), - ("relaydomains", "service", "add_service"), - ("relaydomains", "service", "change_service"), - ("relaydomains", "service", "delete_service") - ] -} diff --git a/modoboa/relaydomains/forms.py b/modoboa/relaydomains/forms.py index de872f917..7a1d2b134 100644 --- a/modoboa/relaydomains/forms.py +++ b/modoboa/relaydomains/forms.py @@ -3,13 +3,11 @@ from __future__ import unicode_literals from django import forms -from django.core import validators -from django.utils.translation import ugettext as _ from modoboa.lib.form_utils import WizardStep -from modoboa.lib import validators as lib_validators - -from .models import RelayDomain +from modoboa.transport import backends as tr_backends +from modoboa.transport import forms as tr_forms +from modoboa.transport import models as tr_models class RelayDomainWizardStep(WizardStep): @@ -21,47 +19,40 @@ class RelayDomainWizardStep(WizardStep): return wizard.steps[0].form.cleaned_data["type"] == "relaydomain" -class RelayDomainFormGeneral(forms.ModelForm): - """RelayDomain form.""" +class RelayDomainFormGeneral(tr_forms.BackendSettingsMixin, forms.ModelForm): + """A form to display transport of type relay.""" class Meta(object): - model = RelayDomain - exclude = ["domain", "creation", "last_modification"] - widgets = { - "service": forms.Select(attrs={"class": "form-control"}) - } + model = tr_models.Transport + exclude = [ + "creation", "pattern", "service", "next_hop", "enabled", + "_settings" + ] def __init__(self, *args, **kwargs): super(RelayDomainFormGeneral, self).__init__(*args, **kwargs) - self.field_widths = { - "service": 3 - } + settings = tr_backends.manager.get_backend_settings("relay") + self.inject_backend_settings("relay", settings) - def clean_target_host(self): - """Check that target host is valid.""" - validator_list = [ - lib_validators.validate_hostname, - validators.validate_ipv46_address - ] - value = self.cleaned_data.get("target_host") - for validator in validator_list: - try: - validator(value) - except forms.ValidationError: - pass - else: - return value - raise forms.ValidationError(_("Invalid value"), code="invalid") + def clean(self): + """Check values.""" + cleaned_data = super(RelayDomainFormGeneral, self).clean() + if self.errors: + return cleaned_data + self.clean_backend_fields("relay") + return cleaned_data def save(self, *args, **kwargs): """Custom save method.""" - domain = kwargs.get("domain") - instance = super(RelayDomainFormGeneral, self).save(commit=False) - if instance.domain and instance.domain.type == "domain": - # Avoid new creation if domain type has been changed. + domain = kwargs.pop("domain", None) + if domain.type != "relaydomain": + # We don't want to recreate the transport we just deleted + # (post_save signal). return None - if domain: - instance.domain = domain - if instance.domain.type == "relaydomain": - instance.save() + instance = super(RelayDomainFormGeneral, self).save(commit=False) + instance.pattern = domain.name + instance.service = "relay" + instance.enabled = domain.enabled + self.backend.serialize(instance) + instance.save() return instance diff --git a/modoboa/relaydomains/handlers.py b/modoboa/relaydomains/handlers.py index 2bdd168a9..107ef2b91 100644 --- a/modoboa/relaydomains/handlers.py +++ b/modoboa/relaydomains/handlers.py @@ -2,18 +2,16 @@ from __future__ import unicode_literals -from django.conf import settings from django.db.models import signals from django.dispatch import receiver -from django.template import Template, Context from django.utils.translation import ugettext as _ from modoboa.admin import models as admin_models from modoboa.admin import signals as admin_signals from modoboa.core import signals as core_signals from modoboa.lib.email_utils import split_mailbox +from modoboa.transport import models as tr_models -from . import constants from . import forms from . import lib from . import models @@ -21,38 +19,31 @@ from . import postfix_maps @receiver(admin_signals.use_external_recipients) -def check_relaydomain_alias(sender, **kwargs): +def check_relaydomain_alias(sender, recipients, **kwargs): """Allow the creation of an alias on a relaydomain.""" - recipient = kwargs.get("recipients") - if not recipient: - return - localpart, domain = split_mailbox(recipient) - if not (models.RelayDomain.objects.select_related().filter( - domain__name=domain).exists()): + localpart, domain = split_mailbox(recipients) + qset = admin_models.Domain.objects.filter(name=domain, type="relaydomain") + if not qset.exists(): return False - if (admin_models.Mailbox.objects.select_related("domain").filter( - domain__name=domain, address=localpart).exists()): + qset = admin_models.Mailbox.objects.select_related("domain").filter( + domain__name=domain, address=localpart) + if qset.exists(): return False return True @receiver(signals.post_save, sender=admin_models.Domain) def clean_domain(sender, instance, **kwargs): - """Remove or create RelayDomain record if needed.""" - if kwargs.get("created"): + """Remove or create Transport record if needed.""" + if kwargs.get("created") or instance.type == "relaydomain": return - if instance.type == "domain": - try: - instance.relaydomain.delete() - except models.RelayDomain.DoesNotExist: - pass - else: - # Make sure to create a RelayDomain instance since we can't do it - # at form level... - models.RelayDomain.objects.get_or_create( - domain=instance, - defaults={"service": models.Service.objects.first()} - ) + tr_models.Transport.objects.filter(pattern=instance.name).delete() + + +@receiver(signals.pre_delete, sender=admin_models.Domain) +def delete_transport(sender, instance, **kwargs): + """Delete Transport instance if any.""" + tr_models.Transport.objects.filter(pattern=instance.name).delete() @receiver(core_signals.register_postfix_maps) @@ -60,41 +51,11 @@ def register_postfix_maps(sender, **kwargs): """Register postfix maps.""" return [ postfix_maps.RelayDomainsMap, - postfix_maps.RelayDomainsTransportMap, postfix_maps.SplitedDomainsTransportMap, postfix_maps.RelayRecipientVerification ] -@receiver(core_signals.extra_role_permissions) -def extra_role_permissions(sender, role, **kwargs): - """Add permissions to the Resellers group.""" - return constants.PERMISSIONS.get(role, []) - - -@receiver(core_signals.extra_static_content) -def static_content(sender, caller, st_type, user, **kwargs): - """Add extra static content.""" - if caller != "domains" or st_type != "js": - return "" - - t = Template(""" - -""") - return t.render(Context({"STATIC_URL": settings.STATIC_URL})) - - -@receiver(admin_signals.extra_domain_filters) -def extra_domain_filters(sender, **kwargs): - """Return relaydomain filters.""" - return ["srvfilter"] - - @receiver(admin_signals.extra_domain_forms) def extra_domain_form(sender, user, **kwargs): """Return relay settings for domain edition.""" @@ -114,28 +75,15 @@ def extra_domain_form(sender, user, **kwargs): def fill_domain_instances(sender, user, domain, **kwargs): """Fill the relaydomain form with the right instance.""" condition = ( - not user.has_perm("relaydomains.change_relaydomain") or + not user.has_perm("transport.change_transport") or domain.type != "relaydomain" ) if condition: return {} - return {"relaydomain": domain.relaydomain} - - -@receiver(admin_signals.extra_domain_qset_filters) -def extra_domain_entries(sender, domfilter, extrafilters, **kwargs): - """Return extra queryset filters.""" - if domfilter is not None and domfilter and domfilter != "relaydomain": - return {} - if "srvfilter" in extrafilters and extrafilters["srvfilter"]: - return {"relaydomain__service__name": extrafilters["srvfilter"]} - return {} - - -@receiver(admin_signals.extra_domain_types) -def extra_domain_types(sender, **kwargs): - """Declare the relay domain type.""" - return [("relaydomain", _("Relay domain"))] + return { + "relaydomain": ( + tr_models.Transport.objects.filter(pattern=domain.name).first()) + } @receiver(admin_signals.extra_domain_wizard_steps) @@ -147,17 +95,32 @@ def extra_wizard_step(sender, **kwargs): )] -@receiver(admin_signals.get_domain_tags) -def get_tags_for_domain(sender, domain, **kwargs): - """Return relay domain custom tags.""" - if domain.type != "relaydomain": - return [] - return domain.relaydomain.tags - - @receiver(admin_signals.import_object) def get_import_func(sender, objtype, **kwargs): """Return function used to import objtype.""" if objtype == "relaydomain": return lib.import_relaydomain return None + + +@receiver(signals.post_save, sender=tr_models.Transport) +def update_recipient_access(sender, instance, **kwargs): + """Create or delete recipient access rule.""" + if instance.service != "relay": + return + if instance._settings.get("relay_verify_recipients", False): + models.RecipientAccess.objects.get_or_create( + pattern=instance.pattern, + defaults={"action": "reject_unverified_recipient"} + ) + else: + models.RecipientAccess.objects.filter( + pattern=instance.pattern).delete() + + +@receiver(signals.post_delete, sender=tr_models.Transport) +def delete_recipient_access(sender, instance, **kwargs): + """Delete recipient access rule.""" + if instance.service != "relay": + return + models.RecipientAccess.objects.filter(pattern=instance.pattern).delete() diff --git a/modoboa/relaydomains/lib.py b/modoboa/relaydomains/lib.py index b32a710ca..8ad4fdb74 100644 --- a/modoboa/relaydomains/lib.py +++ b/modoboa/relaydomains/lib.py @@ -2,9 +2,30 @@ from __future__ import unicode_literals -from .models import RelayDomain +from django.utils.translation import ugettext as _ + +from modoboa.admin import models as admin_models +from modoboa.lib.exceptions import BadRequest +from modoboa.transport import backends as tr_backends +from modoboa.transport import models as tr_models def import_relaydomain(user, row, formopts): """Specific code for relay domains import""" - RelayDomain().from_csv(user, row) + 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"]) + ) + 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"]) + } + transport = tr_models.Transport( + pattern=domain.name, service="relay", _settings=settings) + tr_backends.manager.get_backend("relay").serialize(transport) + transport.save(creator=user) diff --git a/modoboa/relaydomains/migrations/0007_recipientaccess.py b/modoboa/relaydomains/migrations/0007_recipientaccess.py new file mode 100644 index 000000000..4c86b5099 --- /dev/null +++ b/modoboa/relaydomains/migrations/0007_recipientaccess.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-11-24 14:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('relaydomains', '0006_auto_20170215_0948'), + ] + + operations = [ + migrations.CreateModel( + 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)), + ], + ), + ] diff --git a/modoboa/relaydomains/migrations/0008_auto_20171123_1653.py b/modoboa/relaydomains/migrations/0008_auto_20171123_1653.py new file mode 100644 index 000000000..662bddd34 --- /dev/null +++ b/modoboa/relaydomains/migrations/0008_auto_20171123_1653.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-11-23 15:53 +from __future__ import unicode_literals + +from django.db import migrations + + +def move_relaydomain_to_transport(apps, schema_editor): + """Transform relaydomains to transports.""" + RelayDomain = apps.get_model("relaydomains", "RelayDomain") + RecipientAccess = apps.get_model("relaydomains", "RecipientAccess") + Transport = apps.get_model("transport", "Transport") + tr_to_create = [] + ra_to_create = [] + for rd in RelayDomain.objects.select_related("domain", "service"): + next_hop = "[{}]:{}".format(rd.target_host, rd.target_port) + tr = Transport( + pattern=rd.domain.name, + service="relay", + enabled=rd.domain.enabled, + next_hop=next_hop, + _settings={ + "relay_target_host": rd.target_host, + "relay_target_port": rd.target_port, + "relay_verify_recipients": rd.verify_recipients + } + ) + tr_to_create.append(tr) + if not rd.verify_recipients: + continue + ra_to_create.append( + RecipientAccess( + pattern=rd.domain.name, action="reject_unverified_recipient")) + Transport.objects.bulk_create(tr_to_create) + RecipientAccess.objects.bulk_create(ra_to_create) + + +def forward(apps, schema_editor): + """Empty.""" + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('relaydomains', '0007_recipientaccess'), + ('transport', '0001_initial'), + ] + + 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 new file mode 100644 index 000000000..2c4a0f9c7 --- /dev/null +++ b/modoboa/relaydomains/migrations/0009_auto_20171124_1508.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-11-24 14:08 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('relaydomains', '0008_auto_20171123_1653'), + ] + + operations = [ + migrations.RemoveField( + model_name='relaydomain', + name='domain', + ), + migrations.RemoveField( + model_name='relaydomain', + name='service', + ), + migrations.DeleteModel( + name='RelayDomain', + ), + migrations.DeleteModel( + name='Service', + ), + ] diff --git a/modoboa/relaydomains/models.py b/modoboa/relaydomains/models.py index 4347a2a13..75e8fb312 100644 --- a/modoboa/relaydomains/models.py +++ b/modoboa/relaydomains/models.py @@ -3,137 +3,10 @@ from __future__ import unicode_literals from django.db import models -from django.db.models.manager import Manager -from django.utils.translation import ugettext as _, ugettext_lazy - -from reversion import revisions as reversion - -from modoboa.admin import models as admin_models -from modoboa.lib.exceptions import BadRequest -from modoboa.parameters import tools as param_tools -class ServiceManager(Manager): +class RecipientAccess(models.Model): + """An recipient level access table.""" - def load_from_master_cf(self): - """Load services from the master.cf file. - - Parse the configuration file to update the service table. New - entries are saved and outdated ones (ie. present in the - database but not in the file) are removed. - """ - with open(param_tools.get_global_parameter("master_cf_path")) as fp: - content = fp.read() - services = [] - for line in content.split('\n'): - if not line or line.startswith('#'): - continue - parts = line.strip().split() - if len(parts) != 8: - continue - if parts[7] != 'smtp': - continue - srv, created = self.get_or_create(name=parts[0]) - services.append(parts[0]) - to_delete = [] - for service in self.all(): - if service.name not in services: - to_delete.append(service.name) - Service.objects.filter(name__in=to_delete).delete() - - -class Service(models.Model): - """Postfix service.""" - - name = models.CharField( - ugettext_lazy('name'), max_length=100, unique=True, - help_text=ugettext_lazy('The service name') - ) - - objects = ServiceManager() - - class Meta(object): - db_table = "postfix_relay_domains_service" - - def __str__(self): - return self.name - - -class RelayDomain(admin_models.AdminObject): - """Relay domain. - - A relay domain differs from a usual domaine because its final - destination is not reached yet. It must be accepted by the MTA but - it will then be transfered to another one. - """ - - domain = models.OneToOneField("admin.Domain", null=True) - target_host = models.CharField( - ugettext_lazy("target host address"), max_length=255, - help_text=ugettext_lazy( - "Remote address (hostname or IP) of this domain") - ) - target_port = models.IntegerField( - ugettext_lazy("target host port"), - help_text=ugettext_lazy( - "Remote port of this domain"), - default=25 - ) - service = models.ForeignKey(Service) - verify_recipients = models.BooleanField( - ugettext_lazy('verify recipients'), - help_text=ugettext_lazy('Check for valid recipients'), - default=False - ) - - class Meta(object): - ordering = ["domain__name"] - db_table = "postfix_relay_domains_relaydomain" - - @property - def tags(self): - return [ - {"name": "relaydomain", "label": _("Relay Domain"), "type": "dom"}, - {"name": self.service.name, "label": "%s:" % self.service.name, - "type": "srv", "color": "info"} - ] - - def __str__(self): - return self.domain.name - - def to_csv(self, csvwriter): - """Export this relay domain to CSV. - - :param csvwriter: - """ - csvwriter.writerow( - ["relaydomain", self.domain.name, self.target_host, - self.target_port, self.service.name, self.domain.enabled, - self.verify_recipients] - ) - for dalias in self.domain.domainalias_set.all(): - dalias.to_csv(csvwriter) - - def from_csv(self, user, row): - """Import a relay domain from CSV. - - :param user: user importing the relay domain - :param str row: relay domain definition - """ - if len(row) != 7: - raise BadRequest(_("Invalid line")) - self.domain = admin_models.Domain( - name=row[1].strip(), type="relaydomain", quota=0, - enabled=(row[5].strip().lower() in ["true", "1", "yes", "y"]) - ) - self.domain.save(creator=user) - self.target_host = row[2].strip() - self.target_port = row[3].strip() - self.service, created = Service.objects.get_or_create( - name=row[4].strip()) - self.verify_recipients = ( - row[6].strip().lower() in ["true", "1", "yes", "y"]) - self.save(creator=user) - - -reversion.register(RelayDomain) + pattern = models.CharField(unique=True, max_length=254) + action = models.CharField(max_length=40) diff --git a/modoboa/relaydomains/postfix_maps.py b/modoboa/relaydomains/postfix_maps.py index fdb084a61..d0440337f 100644 --- a/modoboa/relaydomains/postfix_maps.py +++ b/modoboa/relaydomains/postfix_maps.py @@ -22,40 +22,6 @@ class RelayDomainsMap(object): ) -class RelayDomainsTransportMap(object): - - """A transport map for relay domains.""" - - filename = "sql-relaydomains-transport.cf" - mysql = ( - "SELECT CONCAT(srv.name, ':[', rdom.target_host, ']:'" - ", rdom.target_port) " - "FROM postfix_relay_domains_service AS srv " - "INNER JOIN postfix_relay_domains_relaydomain AS rdom " - "ON rdom.service_id=srv.id " - "INNER JOIN admin_domain AS dom ON rdom.domain_id=dom.id " - "WHERE dom.enabled=1 AND dom.name='%s'" - ) - postgres = ( - "SELECT srv.name || ':[' || rdom.target_host || ']:' || " - "rdom.target_port " - "FROM postfix_relay_domains_service AS srv " - "INNER JOIN postfix_relay_domains_relaydomain AS rdom " - "ON rdom.service_id=srv.id " - "INNER JOIN admin_domain AS dom ON rdom.domain_id=dom.id " - "WHERE dom.enabled AND dom.name='%s'" - ) - sqlite = ( - "SELECT srv.name || ':[' || rdom.target_host || ']:' || " - "rdom.target_host " - "FROM postfix_relay_domains_service AS srv " - "INNER JOIN postfix_relay_domains_relaydomain AS rdom " - "ON rdom.service_id=srv.id " - "INNER JOIN admin_domain AS dom ON rdom.domain_id=dom.id " - "WHERE dom.enabled=1 AND dom.name='%s'" - ) - - class SplitedDomainsTransportMap(object): """A transport map for splited domains. @@ -66,32 +32,29 @@ class SplitedDomainsTransportMap(object): filename = "sql-spliteddomains-transport.cf" mysql = ( "SELECT 'lmtp:unix:private/dovecot-lmtp' " - "FROM postfix_relay_domains_relaydomain AS rdom " - "INNER JOIN admin_domain AS dom " - "ON rdom.domain_id=dom.id " + "FROM admin_domain AS dom " "INNER JOIN admin_mailbox AS mbox ON dom.id=mbox.domain_id " "INNER JOIN core_user AS u ON mbox.user_id=u.id " - "WHERE dom.enabled=1 AND dom.name='%d' AND u.is_active=1 " + "WHERE dom.type='relaydomain' AND dom.enabled=1 " + "AND dom.name='%d' AND u.is_active=1 " "AND mbox.address='%u'" ) postgres = ( "SELECT 'lmtp:unix:private/dovecot-lmtp' " - "FROM postfix_relay_domains_relaydomain AS rdom " - "INNER JOIN admin_domain AS dom " - "ON rdom.domain_id=dom.id " + "FROM admin_domain AS dom " "INNER JOIN admin_mailbox AS mbox ON dom.id=mbox.domain_id " "INNER JOIN core_user AS u ON mbox.user_id=u.id " - "WHERE dom.enabled AND dom.name='%d' AND u.is_active " + "WHERE dom.type='relaydomain' AND dom.enabled " + "AND dom.name='%d' AND u.is_active " "AND mbox.address='%u'" ) sqlite = ( "SELECT 'lmtp:unix:private/dovecot-lmtp' " - "FROM postfix_relay_domains_relaydomain AS rdom " - "INNER JOIN admin_domain AS dom " - "ON rdom.domain_id=dom.id " + "FROM admin_domain AS dom " "INNER JOIN admin_mailbox AS mbox ON dom.id=mbox.domain_id " "INNER JOIN core_user AS u ON mbox.user_id=u.id " - "WHERE dom.enabled=1 AND dom.name='%d' AND u.is_active=1 " + "WHERE dom.type='relaydomain' AND dom.enabled=1 " + "AND dom.name='%d' AND u.is_active=1 " "AND mbox.address='%u'" ) @@ -102,20 +65,11 @@ class RelayRecipientVerification(object): filename = "sql-relay-recipient-verification.cf" mysql = ( - "SELECT 'reject_unverified_recipient' " - "FROM postfix_relay_domains_relaydomain AS rdom " - "INNER JOIN admin_domain AS dom ON rdom.domain_id=dom.id " - "WHERE rdom.verify_recipients=1 AND dom.name='%d'" + "SELECT action FROM relaydomains_recipientaccess WHERE pattern='%d'" ) postgres = ( - "SELECT 'reject_unverified_recipient' " - "FROM postfix_relay_domains_relaydomain AS rdom " - "INNER JOIN admin_domain AS dom ON rdom.domain_id=dom.id " - "WHERE rdom.verify_recipients AND dom.name='%d'" + "SELECT action FROM relaydomains_recipientaccess WHERE pattern='%d'" ) sqlite = ( - "SELECT 'reject_unverified_recipient' " - "FROM postfix_relay_domains_relaydomain AS rdom " - "INNER JOIN admin_domain AS dom ON rdom.domain_id=dom.id " - "WHERE rdom.verify_recipients=1 AND dom.name='%d'" + "SELECT action FROM relaydomains_recipientaccess WHERE pattern='%d'" ) diff --git a/modoboa/relaydomains/static/relaydomains/js/relay_domains.js b/modoboa/relaydomains/static/relaydomains/js/relay_domains.js deleted file mode 100644 index 807b53d6f..000000000 --- a/modoboa/relaydomains/static/relaydomains/js/relay_domains.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * @module relay_domains - * @desc Utility class to manage relay domains. - */ - -var RelayDomains = function(options) { - this.initialize(options); -}; - -RelayDomains.prototype = { - constructor: RelayDomains, - - defaults: { - }, - - initialize: function(options) { - this.options = $.extend({}, this.defaults, options); - admin.register_tag_handler("srv", this.srv_tag_handler); - $(document).bind('domform_init', $.proxy(this.domain_form_init_cb, this)); - }, - - domain_form_init_cb: function() { - $('#btnscan').click(this.launch_scan); - }, - - launch_scan: function(e) { - var $link = get_target(e); - - e.preventDefault(); - $.ajax({ - type: "POST", - url: $link.attr("href"), - global: false - }).done(function(data) { - var $widget = $('#id_service'); - - $widget.empty(); - $.each(data, function(label, id) { - $widget.append($('