mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
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.
This commit is contained in:
@@ -12,3 +12,6 @@ DEV
|
||||
modoboa.iml
|
||||
.tox
|
||||
modoboa_test
|
||||
.coverage
|
||||
build/
|
||||
dist/
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+12
@@ -42,3 +42,15 @@ file_filter = modoboa/admin/locale/<lang>/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/<lang>/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/<lang>/LC_MESSAGES/djangojs.po
|
||||
source_file = modoboa/transport/locale/en/LC_MESSAGES/djangojs.po
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
||||
@@ -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")),
|
||||
]
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -63,6 +63,7 @@ MODOBOA_APPS = (
|
||||
'modoboa.core',
|
||||
'modoboa.lib',
|
||||
'modoboa.admin',
|
||||
'modoboa.transport',
|
||||
'modoboa.relaydomains',
|
||||
'modoboa.limits',
|
||||
'modoboa.parameters',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"})
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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("""<script src="{{ STATIC_URL }}relaydomains/js/relay_domains.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
var rdomain;
|
||||
$(document).ready(function() {
|
||||
rdomain = new RelayDomains({});
|
||||
});
|
||||
</script>
|
||||
""")
|
||||
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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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'"
|
||||
)
|
||||
|
||||
@@ -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($('<option />', {
|
||||
value: id,
|
||||
html: label
|
||||
}));
|
||||
});
|
||||
$('.modal-body').prepend(
|
||||
build_success_alert(gettext('Services updated'))
|
||||
);
|
||||
}).fail(function(jqxhr) {
|
||||
var result = $.parseJSON(jqxhr.responseText);
|
||||
$('.modal-body').prepend(
|
||||
build_error_alert(result[0])
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
srv_tag_handler: function(tag, $link) {
|
||||
if (this.navobj.getparam(tag + "filter") === undefined && $link.hasClass(tag)) {
|
||||
var text = $link.attr("name");
|
||||
this.navobj
|
||||
.setparam("domfilter", "relaydomain")
|
||||
.setparam(tag + "filter", text)
|
||||
.update();
|
||||
if ($("a[name=dom]").length === 0) {
|
||||
$("#taglist").append(this.make_tag("relaydomain", "dom"));
|
||||
}
|
||||
$("#taglist").append(this.make_tag(text, tag));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
{% load form_tags %}
|
||||
|
||||
{% render_field form.target_host %}
|
||||
{% render_field form.target_port %}
|
||||
{% include "relaydomains/service_field.html" with field=form.service label_width="col-sm-4" %}
|
||||
{% render_field form.verify_recipients %}
|
||||
{% render_field form.relay_target_host %}
|
||||
{% render_field form.relay_target_port %}
|
||||
{% render_field form.relay_verify_recipients %}
|
||||
|
||||
@@ -12,6 +12,8 @@ from modoboa.core.factories import UserFactory
|
||||
from modoboa.lib.tests import ModoTestCase
|
||||
from modoboa.lib.test_utils import MapFilesTestCaseMixin
|
||||
from modoboa.limits import utils as limits_utils
|
||||
from modoboa.transport import factories as tr_factories
|
||||
from modoboa.transport import models as tr_models
|
||||
|
||||
from . import models
|
||||
|
||||
@@ -19,12 +21,16 @@ from . import models
|
||||
class Operations(object):
|
||||
|
||||
def _create_relay_domain(self, name, status=200, **kwargs):
|
||||
srv, created = models.Service.objects.get_or_create(name='dummy')
|
||||
values = {
|
||||
"name": name, "create_dom_admin": False, "type": "relaydomain",
|
||||
"target_host": "external.host.tld", "target_port": 25,
|
||||
"service": srv.id, "enabled": True, "stepid": "step3",
|
||||
"quota": 0, "default_mailbox_quota": 0
|
||||
"name": name,
|
||||
"create_dom_admin": False,
|
||||
"type": "relaydomain",
|
||||
"relay_target_host": "external.host.tld",
|
||||
"relay_target_port": 25,
|
||||
"enabled": True,
|
||||
"stepid": "step3",
|
||||
"quota": 0,
|
||||
"default_mailbox_quota": 0
|
||||
}
|
||||
values.update(kwargs)
|
||||
return self.ajax_post(
|
||||
@@ -32,16 +38,17 @@ class Operations(object):
|
||||
values, status
|
||||
)
|
||||
|
||||
def _relay_domain_alias_operation(self, optype, rdomain, name, status=200):
|
||||
rdom = models.RelayDomain.objects.get(domain__name=rdomain)
|
||||
def _relay_domain_alias_operation(self, optype, domain, name, status=200):
|
||||
transport = tr_models.Transport.objects.get(pattern=domain.name)
|
||||
values = {
|
||||
"name": rdom.domain.name, "target_host": rdom.target_host,
|
||||
"target_port": rdom.target_port,
|
||||
"type": "relaydomain", "service": rdom.service.id,
|
||||
"quota": rdom.domain.quota,
|
||||
"default_mailbox_quota": rdom.domain.default_mailbox_quota
|
||||
"name": domain.name,
|
||||
"relay_target_host": transport._settings["relay_target_host"],
|
||||
"relay_target_port": transport._settings["relay_target_port"],
|
||||
"type": "relaydomain",
|
||||
"quota": domain.quota,
|
||||
"default_mailbox_quota": domain.default_mailbox_quota
|
||||
}
|
||||
aliases = [alias.name for alias in rdom.domain.domainalias_set.all()]
|
||||
aliases = [alias.name for alias in domain.domainalias_set.all()]
|
||||
if optype == 'add':
|
||||
aliases.append(name)
|
||||
else:
|
||||
@@ -51,7 +58,7 @@ class Operations(object):
|
||||
values[fname] = alias
|
||||
return self.ajax_post(
|
||||
reverse("admin:domain_change",
|
||||
args=[rdom.domain.id]),
|
||||
args=[domain.id]),
|
||||
values, status
|
||||
)
|
||||
|
||||
@@ -68,15 +75,19 @@ class RelayDomainsTestCase(ModoTestCase, Operations):
|
||||
"""Create test data."""
|
||||
super(RelayDomainsTestCase, cls).setUpTestData()
|
||||
admin_factories.populate_database()
|
||||
dom = admin_factories.DomainFactory(
|
||||
cls.dom = admin_factories.DomainFactory(
|
||||
name="relaydomain.tld", type="relaydomain")
|
||||
dom.relaydomain.target_host = "external.host.tld"
|
||||
dom.relaydomain.save()
|
||||
cls.rdom = dom.relaydomain
|
||||
cls.transport = tr_factories.TransportFactory(
|
||||
pattern=cls.dom.name, _settings={
|
||||
"relay_target_host": "external.host.tld",
|
||||
"relay_target_port": "25",
|
||||
"relay_verify_recipients": False
|
||||
}
|
||||
)
|
||||
admin_factories.DomainAliasFactory(
|
||||
name="relaydomainalias.tld", target=cls.rdom.domain)
|
||||
name="relaydomainalias.tld", target=cls.dom)
|
||||
admin_factories.MailboxFactory(
|
||||
domain=cls.rdom.domain, address="local",
|
||||
domain=cls.dom, address="local",
|
||||
user__username="local@relaydomain.tld",
|
||||
user__groups=("SimpleUsers", )
|
||||
)
|
||||
@@ -88,7 +99,7 @@ class RelayDomainsTestCase(ModoTestCase, Operations):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
content = response.json()
|
||||
self.assertIn("relaydomain.tld", content["rows"])
|
||||
self.assertIn("Relay Domain", content["rows"])
|
||||
self.assertIn("Relay domain", content["rows"])
|
||||
|
||||
def test_create_relaydomain(self):
|
||||
"""Test the creation of a relay domain.
|
||||
@@ -99,11 +110,11 @@ class RelayDomainsTestCase(ModoTestCase, Operations):
|
||||
FIXME: add a check for domain alias.
|
||||
"""
|
||||
self._create_relay_domain('relaydomain1.tld')
|
||||
rdom = models.RelayDomain.objects.get(domain__name='relaydomain1.tld')
|
||||
self.assertEqual(rdom.target_host, 'external.host.tld')
|
||||
self.assertEqual(rdom.service.name, 'dummy')
|
||||
self.assertEqual(rdom.domain.enabled, True)
|
||||
self.assertEqual(rdom.verify_recipients, False)
|
||||
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)
|
||||
|
||||
resp = self._create_relay_domain('test.com', 400)
|
||||
self.assertEqual(resp['form_errors']['name'][0],
|
||||
@@ -123,17 +134,17 @@ class RelayDomainsTestCase(ModoTestCase, Operations):
|
||||
FIXME: add a check for domain alias.
|
||||
"""
|
||||
self._relay_domain_alias_operation(
|
||||
'add', self.rdom.domain.name, 'relaydomainalias1.tld'
|
||||
'add', self.dom, 'relaydomainalias1.tld'
|
||||
)
|
||||
resp = self._relay_domain_alias_operation(
|
||||
'add', self.rdom.domain.name, 'test.com', 400
|
||||
'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.rdom.domain.name, self.rdom.domain.name, 400
|
||||
'add', self.dom, self.dom.name, 400
|
||||
)
|
||||
self.assertEqual(
|
||||
resp['form_errors']['aliases_2'][0],
|
||||
@@ -146,36 +157,49 @@ class RelayDomainsTestCase(ModoTestCase, Operations):
|
||||
Rename 'relaydomain.tld' domain to 'relaydomain.org'
|
||||
"""
|
||||
values = {
|
||||
"name": "relaydomain.org", "target_host": self.rdom.target_host,
|
||||
"target_port": 4040, "type": "relaydomain", "enabled": True,
|
||||
"service": self.rdom.service.id,
|
||||
"name": "relaydomain.org",
|
||||
"relay_target_host": self.transport._settings["relay_target_host"],
|
||||
"relay_target_port": 4040,
|
||||
"relay_verify_recipients": True,
|
||||
"type": "relaydomain",
|
||||
"enabled": True,
|
||||
"quota": 0, "default_mailbox_quota": 0
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("admin:domain_change", args=[self.rdom.domain.id]),
|
||||
reverse("admin:domain_change", args=[self.dom.id]),
|
||||
values
|
||||
)
|
||||
self.rdom.refresh_from_db()
|
||||
self.assertEqual(self.rdom.target_port, 4040)
|
||||
self.transport.refresh_from_db()
|
||||
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
|
||||
)
|
||||
self.assertFalse(
|
||||
models.RecipientAccess.objects.filter(
|
||||
pattern=values["name"]).exists())
|
||||
|
||||
def test_relaydomain_domain_switch(self):
|
||||
"""Check domain <-> relaydomain transitions."""
|
||||
domain_pk = self.rdom.domain.pk
|
||||
domain_pk = self.dom.pk
|
||||
values = {
|
||||
"name": "relaydomain.tld",
|
||||
"type": "domain",
|
||||
"quota": 0,
|
||||
"default_mailbox_quota": 0,
|
||||
"enabled": True,
|
||||
"target_host": self.rdom.target_host,
|
||||
"target_port": self.rdom.target_port,
|
||||
"service": self.rdom.service.pk
|
||||
"relay_target_host": self.transport._settings["relay_target_host"],
|
||||
"relay_target_port": self.transport._settings["relay_target_port"]
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("admin:domain_change", args=[domain_pk]), values)
|
||||
self.assertFalse(
|
||||
models.RelayDomain.objects.filter(
|
||||
domain__pk=domain_pk).exists())
|
||||
with self.assertRaises(tr_models.Transport.DoesNotExist):
|
||||
self.transport.refresh_from_db()
|
||||
self.assertEqual(
|
||||
admin_models.Domain.objects.get(name="relaydomain.tld").type,
|
||||
"domain")
|
||||
@@ -191,9 +215,6 @@ class RelayDomainsTestCase(ModoTestCase, Operations):
|
||||
self.assertEqual(
|
||||
admin_models.Domain.objects.get(name="relaydomain.tld").type,
|
||||
"relaydomain")
|
||||
self.assertTrue(
|
||||
models.RelayDomain.objects.filter(
|
||||
domain__name="relaydomain.tld").exists())
|
||||
|
||||
def test_edit_relaydomainalias(self):
|
||||
"""Test the modification of a relay domain alias.
|
||||
@@ -201,15 +222,15 @@ class RelayDomainsTestCase(ModoTestCase, Operations):
|
||||
Rename 'relaydomainalias.tld' domain to 'relaydomainalias.net'
|
||||
"""
|
||||
values = {
|
||||
"name": "relaydomain.org", "target_host": self.rdom.target_host,
|
||||
"target_port": self.rdom.target_port,
|
||||
"type": "relaydomain", "service": self.rdom.service.id,
|
||||
"name": "relaydomain.org",
|
||||
"relay_target_host": self.transport._settings["relay_target_host"],
|
||||
"relay_target_port": self.transport._settings["relay_target_port"],
|
||||
"type": "relaydomain",
|
||||
"aliases": "relaydomainalias.net",
|
||||
"quota": 0, "default_mailbox_quota": 0
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse('admin:domain_change',
|
||||
args=[self.rdom.domain.id]),
|
||||
reverse('admin:domain_change', args=[self.dom.id]),
|
||||
values
|
||||
)
|
||||
admin_models.DomainAlias.objects.get(name='relaydomainalias.net')
|
||||
@@ -219,20 +240,23 @@ class RelayDomainsTestCase(ModoTestCase, Operations):
|
||||
def test_delete_relaydomain(self):
|
||||
"""Test the removal of a relay domain."""
|
||||
self.ajax_post(
|
||||
reverse("admin:domain_delete",
|
||||
args=[self.rdom.domain.id]),
|
||||
reverse("admin:domain_delete", args=[self.dom.id]),
|
||||
{}
|
||||
)
|
||||
with self.assertRaises(models.RelayDomain.DoesNotExist):
|
||||
models.RelayDomain.objects.get(domain__name='relaydomain.tld')
|
||||
with self.assertRaises(tr_models.Transport.DoesNotExist):
|
||||
self.transport.refresh_from_db()
|
||||
|
||||
def test_delete_relaydomainalias(self):
|
||||
"""Test the removal of a relay domain alias."""
|
||||
self._relay_domain_alias_operation(
|
||||
'del', self.rdom.domain.name, 'relaydomainalias.tld'
|
||||
def test_delete_recipientaccess(self):
|
||||
"""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]),
|
||||
{}
|
||||
)
|
||||
with self.assertRaises(models.RelayDomain.DoesNotExist):
|
||||
models.RelayDomain.objects.get(domain__name='relaydomainalias.tld')
|
||||
self.assertFalse(
|
||||
models.RecipientAccess.objects.filter(
|
||||
pattern=self.transport.pattern).exists())
|
||||
|
||||
def test_alias_on_relaydomain(self):
|
||||
"""Create an alias on a relay domain."""
|
||||
@@ -309,20 +333,21 @@ class LimitsTestCase(ModoTestCase, Operations):
|
||||
def test_relay_domain_aliases_limit(self):
|
||||
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', 'relaydomain1.tld', 'relay-domain-alias1.tld'
|
||||
'add', domain, 'relay-domain-alias1.tld'
|
||||
)
|
||||
self._check_limit('domain_aliases', 1, 2)
|
||||
self._relay_domain_alias_operation(
|
||||
'add', 'relaydomain1.tld', 'relay-domain-alias2.tld'
|
||||
'add', domain, 'relay-domain-alias2.tld'
|
||||
)
|
||||
self._check_limit('domain_aliases', 2, 2)
|
||||
self._relay_domain_alias_operation(
|
||||
'add', 'relaydomain1.tld', 'relay-domain-alias3.tld', 403
|
||||
'add', domain, 'relay-domain-alias3.tld', 403
|
||||
)
|
||||
self._check_limit('domain_aliases', 2, 2)
|
||||
self._relay_domain_alias_operation(
|
||||
'delete', 'relaydomain1.tld', 'relay-domain-alias2.tld'
|
||||
'delete', domain, 'relay-domain-alias2.tld'
|
||||
)
|
||||
self._check_limit('domain_aliases', 1, 2)
|
||||
|
||||
@@ -333,6 +358,5 @@ class MapFilesTestCase(MapFilesTestCaseMixin, TestCase):
|
||||
|
||||
MAP_FILES = [
|
||||
"sql-relaydomains.cf",
|
||||
"sql-relaydomains-transport.cf",
|
||||
"sql-relay-recipient-verification.cf"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Transport backend definition."""
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from modoboa.transport import backends
|
||||
|
||||
|
||||
class RelayTransportBackend(backends.TransportBackend):
|
||||
"""Relay backend class."""
|
||||
|
||||
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},
|
||||
)
|
||||
|
||||
def serialize(self, transport):
|
||||
"""Make sure next_hop is set."""
|
||||
transport.next_hop = "[{}]:{}".format(
|
||||
transport._settings["relay_target_host"],
|
||||
transport._settings["relay_target_port"])
|
||||
|
||||
|
||||
backends.manager.register_backend(RelayTransportBackend)
|
||||
@@ -1,13 +0,0 @@
|
||||
"""Relay domains urls."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^scan_services/$', views.scan_for_services,
|
||||
name="service_scan"),
|
||||
]
|
||||
@@ -1,26 +0,0 @@
|
||||
"""Custom views."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.auth.decorators import (
|
||||
login_required, permission_required
|
||||
)
|
||||
from django.views.decorators.http import require_http_methods
|
||||
|
||||
from modoboa.lib.web_utils import render_to_json_response
|
||||
|
||||
from .models import Service
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("relaydomains.add_service")
|
||||
@require_http_methods(["POST"])
|
||||
def scan_for_services(request):
|
||||
try:
|
||||
Service.objects.load_from_master_cf()
|
||||
except IOError as e:
|
||||
return render_to_json_response([str(e)], status=500)
|
||||
|
||||
return render_to_json_response(
|
||||
dict((srv.name, srv.id) for srv in Service.objects.all())
|
||||
)
|
||||
@@ -255,6 +255,15 @@ html body div.navbar div.container-fluid div#navigate-navbar-collapse.collapse u
|
||||
float:none;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table th#actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
table td[name="actions"] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* --------- specific pages --------- */
|
||||
|
||||
/* identities tables */
|
||||
|
||||
@@ -188,8 +188,8 @@ function simple_ajax_form_post(e, options) {
|
||||
histomanager.update(true);
|
||||
}
|
||||
}
|
||||
if (data) {
|
||||
$("body").notify('success', data, 2000);
|
||||
if (data && data.message) {
|
||||
$("body").notify('success', data.message, 2000);
|
||||
}
|
||||
}).fail(function(jqxhr) {
|
||||
var data = $.parseJSON(jqxhr.responseText);
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
{% load core_tags %}
|
||||
|
||||
{% block container_content %}
|
||||
<div id="leftcol" class="sidebar collapse navbar-collapse">
|
||||
{% block leftcol %}{% endblock %}
|
||||
</div>
|
||||
<div class="main">
|
||||
{% if messages %}{% display_messages messages %}{% endif %}
|
||||
{% block apparea %}{% endblock %}
|
||||
</div>
|
||||
<div id="leftcol" class="sidebar collapse navbar-collapse">
|
||||
{% block leftcol %}{% endblock %}
|
||||
</div>
|
||||
<div class="main">
|
||||
{% block apparea %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% if messages %}{% display_messages messages %}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
default_app_config = "modoboa.transport.apps.TransportConfig"
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,11 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TransportConfig(AppConfig):
|
||||
name = 'modoboa.transport'
|
||||
|
||||
def ready(self):
|
||||
from . import handlers
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Transport backend."""
|
||||
|
||||
from django import forms
|
||||
from django.core import validators
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from modoboa.lib import validators as lib_validators
|
||||
|
||||
|
||||
class TransportBackend(object):
|
||||
"""Base backend class."""
|
||||
|
||||
name = None
|
||||
settings = ()
|
||||
|
||||
def _validate_host_address(self, value):
|
||||
"""Validate an host address syntax."""
|
||||
validator_list = [
|
||||
lib_validators.validate_hostname,
|
||||
validators.validate_ipv46_address
|
||||
]
|
||||
for validator in validator_list:
|
||||
try:
|
||||
validator(value)
|
||||
except forms.ValidationError:
|
||||
pass
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
def clean_fields(self, values):
|
||||
"""Check that values are correct."""
|
||||
errors = []
|
||||
for setting in self.settings:
|
||||
fname = "{}_{}".format(self.name, setting["name"])
|
||||
value = values.get(fname)
|
||||
if not value:
|
||||
if not setting.get("required", True):
|
||||
continue
|
||||
errors.append((fname, _("This field is required")))
|
||||
continue
|
||||
ftype = setting.get("type", "string")
|
||||
if ftype == "string":
|
||||
validator = setting.get("validator")
|
||||
vfunc = "_validate_{}".format(validator)
|
||||
if validator and hasattr(self, vfunc):
|
||||
if getattr(self, vfunc)(value):
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
elif ftype == "int" and isinstance(value, int):
|
||||
continue
|
||||
elif ftype == "boolean" and isinstance(value, bool):
|
||||
continue
|
||||
errors.append((fname, _("Invalid value")))
|
||||
return errors
|
||||
|
||||
def serialize(self, transport):
|
||||
"""Transform received values if needed."""
|
||||
pass
|
||||
|
||||
|
||||
class TransportBackendManager(object):
|
||||
"""Transport backends manager."""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructor."""
|
||||
self.backends = {}
|
||||
|
||||
def get_backend(self, name, **kwargs):
|
||||
"""Return backend instance."""
|
||||
backend_class = self.backends.get(name)
|
||||
if backend_class is None:
|
||||
return None
|
||||
return backend_class(**kwargs)
|
||||
|
||||
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])
|
||||
|
||||
def get_backend_settings(self, name):
|
||||
"""Return backend settings."""
|
||||
backend_class = self.backends.get(name)
|
||||
if backend_class is None:
|
||||
return None
|
||||
return backend_class.settings
|
||||
|
||||
def get_all_backend_settings(self):
|
||||
"""Return all backend settings."""
|
||||
return {
|
||||
name: backend_class.settings
|
||||
for name, backend_class in list(self.backends.items())
|
||||
}
|
||||
|
||||
def register_backend(self, backend_class):
|
||||
"""Register a new backend."""
|
||||
if not backend_class.name:
|
||||
raise RuntimeError("missing backend name")
|
||||
self.backends[backend_class.name] = backend_class
|
||||
|
||||
|
||||
manager = TransportBackendManager()
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Transport constants."""
|
||||
|
||||
PERMISSIONS = {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Transport factories."""
|
||||
|
||||
import factory
|
||||
|
||||
from modoboa.core.factories import PermissionFactory
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class TransportFactory(PermissionFactory):
|
||||
"""Factory for Transport."""
|
||||
|
||||
class Meta:
|
||||
model = models.Transport
|
||||
django_get_or_create = ("pattern", )
|
||||
|
||||
pattern = factory.Sequence(lambda n: "transport{}".format(n))
|
||||
service = "relay"
|
||||
next_hop = "[external.host.tld]:25"
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Transport forms."""
|
||||
|
||||
from django import forms
|
||||
|
||||
from . import backends
|
||||
from . import models
|
||||
|
||||
|
||||
TYPE_TO_FIELD_MAP = {
|
||||
"int": forms.IntegerField,
|
||||
"boolean": forms.BooleanField,
|
||||
"string": forms.CharField
|
||||
}
|
||||
|
||||
|
||||
class BackendSettingsMixin(object):
|
||||
"""A mixin to deal with backend settings in a model form."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Constructor."""
|
||||
super(BackendSettingsMixin, self).__init__(*args, **kwargs)
|
||||
self.setting_fields = []
|
||||
|
||||
def inject_backend_settings(self, name, settings):
|
||||
"""Inject backend settings to form."""
|
||||
for setting in settings:
|
||||
fullname = "{}_{}".format(name, setting["name"])
|
||||
options = {}
|
||||
ftype = setting.get("type", "string")
|
||||
if self.instance.pk:
|
||||
options["initial"] = self.instance._settings.get(fullname)
|
||||
elif "default" in setting:
|
||||
options["initial"] = setting["default"]
|
||||
if "widget" in setting:
|
||||
options["widget"] = setting["widget"]
|
||||
self.fields[fullname] = TYPE_TO_FIELD_MAP[ftype](
|
||||
label=setting["label"], required=False, **options)
|
||||
self.setting_fields.append(fullname)
|
||||
|
||||
def clean_backend_fields(self, name):
|
||||
"""Clean backend fields."""
|
||||
self.backend = backends.manager.get_backend(name)
|
||||
for field, error in self.backend.clean_fields(self.cleaned_data):
|
||||
self.add_error(field, error)
|
||||
|
||||
def save(self, commit=True):
|
||||
"""Set settings to JSON field."""
|
||||
transport = super(BackendSettingsMixin, self).save(commit=False)
|
||||
transport._settings = {
|
||||
name: self.cleaned_data[name] for name in self.setting_fields
|
||||
}
|
||||
if commit:
|
||||
transport.save()
|
||||
return transport
|
||||
|
||||
|
||||
class TransportForm(BackendSettingsMixin, forms.ModelForm):
|
||||
"""Transport model form."""
|
||||
|
||||
service = forms.ChoiceField(choices=[])
|
||||
|
||||
class Meta:
|
||||
fields = ("pattern", "service", "enabled")
|
||||
model = models.Transport
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Set backend list."""
|
||||
super(TransportForm, self).__init__(*args, **kwargs)
|
||||
self.fields["service"].choices = backends.manager.get_backend_list()
|
||||
settings = backends.manager.get_all_backend_settings()
|
||||
for name, backend_settings in settings.items():
|
||||
self.inject_backend_settings(name, backend_settings)
|
||||
|
||||
def _clean_fields(self):
|
||||
"""Make backend settings required."""
|
||||
backend_name = self.data.get("service")
|
||||
backend_settings = backends.manager.get_backend_settings(backend_name)
|
||||
for name, field in self.fields.items():
|
||||
if name.startswith("{}_".format(backend_name)):
|
||||
name = name.replace("{}_".format(backend_name), "")
|
||||
for setting in backend_settings:
|
||||
if setting["name"] == name:
|
||||
break
|
||||
if setting.get("required", True):
|
||||
field.required = True
|
||||
return super(TransportForm, self)._clean_fields()
|
||||
|
||||
def clean(self):
|
||||
"""Check values."""
|
||||
cleaned_data = super(TransportForm, self).clean()
|
||||
if self.errors:
|
||||
return cleaned_data
|
||||
self.clean_backend_fields(cleaned_data["service"])
|
||||
return cleaned_data
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Transport handlers."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.admin import signals as admin_signals
|
||||
from modoboa.core import signals as core_signals
|
||||
|
||||
from . import postfix_maps
|
||||
|
||||
|
||||
@receiver(admin_signals.extra_domain_menu_entries)
|
||||
def extra_domain_menu_entries(sender, user, **kwargs):
|
||||
"""Add extra menu entries."""
|
||||
return [{
|
||||
"name": "transports",
|
||||
"label": _("Transports"),
|
||||
"url": reverse("transport:transport_list"),
|
||||
"img": "fa fa-bus",
|
||||
}]
|
||||
|
||||
|
||||
@receiver(core_signals.register_postfix_maps)
|
||||
def register_postfix_maps(sender, **kwargs):
|
||||
"""Register postfix maps."""
|
||||
return [
|
||||
postfix_maps.TransportMap,
|
||||
]
|
||||
@@ -0,0 +1,124 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-11-24 16:42+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: backends.py:31
|
||||
msgid "target host address"
|
||||
msgstr ""
|
||||
|
||||
#: backends.py:32
|
||||
msgid "target host port"
|
||||
msgstr ""
|
||||
|
||||
#: backends.py:34
|
||||
msgid "verify recipients"
|
||||
msgstr ""
|
||||
|
||||
#: backends.py:46
|
||||
msgid "This field is required"
|
||||
msgstr ""
|
||||
|
||||
#: backends.py:54
|
||||
msgid "Invalid value"
|
||||
msgstr ""
|
||||
|
||||
#: handlers.py:20 templates/transport/transport_list.html:5
|
||||
msgid "Transports"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:18
|
||||
msgid "pattern"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:19
|
||||
msgid "service"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:20
|
||||
msgid "next hop"
|
||||
msgstr ""
|
||||
|
||||
#: serializers.py:23
|
||||
msgid "Unsupported service"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transport/_transport_headers.html:3
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transport/_transport_headers.html:4
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transport/_transport_headers.html:5
|
||||
#: templates/transport/transport_list.html:33
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transport/transport_list.html:21
|
||||
msgid "Add"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transport/transport_list.html:31
|
||||
msgid "Pattern"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transport/transport_list.html:32
|
||||
msgid "Service"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transport/transport_list.html:39
|
||||
msgid "disabled"
|
||||
msgstr ""
|
||||
|
||||
#: templates/transport/transport_list.html:49
|
||||
#, python-format
|
||||
msgid "Delete %(pattern)s? "
|
||||
msgstr ""
|
||||
|
||||
#: templates/transport/transport_list.html:56
|
||||
msgid "No transport defined yet."
|
||||
msgstr ""
|
||||
|
||||
#: views.py:47
|
||||
msgid "Transport created"
|
||||
msgstr ""
|
||||
|
||||
#: views.py:99
|
||||
msgid "Create"
|
||||
msgstr ""
|
||||
|
||||
#: views.py:100
|
||||
msgid "New transport"
|
||||
msgstr ""
|
||||
|
||||
#: views.py:112
|
||||
msgid "Transport updated"
|
||||
msgstr ""
|
||||
|
||||
#: views.py:121
|
||||
msgid "Update"
|
||||
msgstr ""
|
||||
|
||||
#: views.py:122
|
||||
msgid "Edit transport"
|
||||
msgstr ""
|
||||
|
||||
#: views.py:141
|
||||
msgid "Transport deleted"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,22 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-11-24 16:44+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: static/transport/js/transport.js:36
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,124 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-11-24 16:42+0100\n"
|
||||
"PO-Revision-Date: 2017-11-24 17:22+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: backends.py:31
|
||||
msgid "target host address"
|
||||
msgstr "adresse de l'hôte cible"
|
||||
|
||||
#: backends.py:32
|
||||
msgid "target host port"
|
||||
msgstr "admin/tests/"
|
||||
|
||||
#: backends.py:34
|
||||
msgid "verify recipients"
|
||||
msgstr "vérifier les destinataires"
|
||||
|
||||
#: backends.py:46
|
||||
msgid "This field is required"
|
||||
msgstr "Ce champ est requis"
|
||||
|
||||
#: backends.py:54
|
||||
msgid "Invalid value"
|
||||
msgstr "Valeur incorrecte"
|
||||
|
||||
#: handlers.py:20 templates/transport/transport_list.html:5
|
||||
msgid "Transports"
|
||||
msgstr "Transports"
|
||||
|
||||
#: models.py:18
|
||||
msgid "pattern"
|
||||
msgstr "patron"
|
||||
|
||||
#: models.py:19
|
||||
msgid "service"
|
||||
msgstr "service"
|
||||
|
||||
#: models.py:20
|
||||
msgid "next hop"
|
||||
msgstr "saut suivant"
|
||||
|
||||
#: serializers.py:23
|
||||
msgid "Unsupported service"
|
||||
msgstr "Service non supporté"
|
||||
|
||||
#: templates/transport/_transport_headers.html:3
|
||||
msgid "Name"
|
||||
msgstr "Nom"
|
||||
|
||||
#: templates/transport/_transport_headers.html:4
|
||||
msgid "Tags"
|
||||
msgstr "Libellés"
|
||||
|
||||
#: templates/transport/_transport_headers.html:5
|
||||
#: templates/transport/transport_list.html:33
|
||||
msgid "Actions"
|
||||
msgstr "Actions"
|
||||
|
||||
#: templates/transport/transport_list.html:21
|
||||
msgid "Add"
|
||||
msgstr "Ajouter"
|
||||
|
||||
#: templates/transport/transport_list.html:31
|
||||
msgid "Pattern"
|
||||
msgstr "Patron"
|
||||
|
||||
#: templates/transport/transport_list.html:32
|
||||
msgid "Service"
|
||||
msgstr "Service"
|
||||
|
||||
#: templates/transport/transport_list.html:39
|
||||
msgid "disabled"
|
||||
msgstr "désactivé"
|
||||
|
||||
#: templates/transport/transport_list.html:49
|
||||
#, python-format
|
||||
msgid "Delete %(pattern)s? "
|
||||
msgstr "Supprimer %(pattern)s ? "
|
||||
|
||||
#: templates/transport/transport_list.html:56
|
||||
msgid "No transport defined yet."
|
||||
msgstr "Aucun transport défini"
|
||||
|
||||
#: views.py:47
|
||||
msgid "Transport created"
|
||||
msgstr "Transport créé"
|
||||
|
||||
#: views.py:99
|
||||
msgid "Create"
|
||||
msgstr "Créer"
|
||||
|
||||
#: views.py:100
|
||||
msgid "New transport"
|
||||
msgstr "Nouveau transport"
|
||||
|
||||
#: views.py:112
|
||||
msgid "Transport updated"
|
||||
msgstr "Transport mis à jour"
|
||||
|
||||
#: views.py:121
|
||||
msgid "Update"
|
||||
msgstr "Mettre à jour"
|
||||
|
||||
#: views.py:122
|
||||
msgid "Edit transport"
|
||||
msgstr "Modifier le transport"
|
||||
|
||||
#: views.py:141
|
||||
msgid "Transport deleted"
|
||||
msgstr "Transport supprimé"
|
||||
@@ -0,0 +1,22 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-11-24 16:44+0100\n"
|
||||
"PO-Revision-Date: 2017-11-24 16:45+0100\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: static/transport/js/transport.js:36
|
||||
msgid "Search"
|
||||
msgstr "Chercher"
|
||||
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-11-24 11:16
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
import jsonfield.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Transport',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('creation', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('last_modification', models.DateTimeField(auto_now=True)),
|
||||
('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')),
|
||||
('enabled', models.BooleanField(default=True, verbose_name='enabled')),
|
||||
('_settings', jsonfield.fields.JSONField(default={})),
|
||||
],
|
||||
options={
|
||||
'ordering': ['pattern'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Transport models."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from reversion import revisions as reversion
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import jsonfield
|
||||
|
||||
from modoboa.admin import models as admin_models
|
||||
|
||||
|
||||
class Transport(admin_models.AdminObject):
|
||||
"""Transport table."""
|
||||
|
||||
pattern = models.CharField(_("pattern"), unique=True, max_length=254)
|
||||
service = models.CharField(_("service"), max_length=30)
|
||||
next_hop = models.CharField(_("next hop"), max_length=100, blank=True)
|
||||
enabled = models.BooleanField("enabled", default=True)
|
||||
|
||||
_settings = jsonfield.JSONField(default={})
|
||||
|
||||
class Meta:
|
||||
ordering = ["pattern"]
|
||||
|
||||
def __str__(self):
|
||||
return self.pattern
|
||||
|
||||
|
||||
reversion.register(Transport)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Map file definitions for postfix."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
class TransportMap(object):
|
||||
"""A transport map."""
|
||||
|
||||
filename = "sql-transport.cf"
|
||||
mysql = (
|
||||
"SELECT CONCAT(service, ':', next_hop) "
|
||||
"FROM transport_transport "
|
||||
"WHERE enabled=1 AND pattern='%s'"
|
||||
)
|
||||
postgres = (
|
||||
"SELECT service || ':' || next_hop "
|
||||
"FROM transport_transport "
|
||||
"WHERE enabled AND pattern='%s'"
|
||||
)
|
||||
sqlite = (
|
||||
"SELECT service || ':' || next_hop "
|
||||
"FROM transport_transport "
|
||||
"WHERE enabled=1 AND pattern='%s'"
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Transport serializers."""
|
||||
|
||||
import json
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from . import backends
|
||||
from . import models
|
||||
|
||||
|
||||
class TransportSerializer(serializers.ModelSerializer):
|
||||
"""Transport serializer class."""
|
||||
|
||||
class Meta:
|
||||
model = models.Transport
|
||||
fields = ("pk", "pattern", "service", "enabled", "_settings")
|
||||
|
||||
def validate(self, data):
|
||||
"""Check fields based on backend."""
|
||||
self.backend = backends.manager.get_backend(data["service"])
|
||||
if not self.backend:
|
||||
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])
|
||||
})
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Use backend to serialize data."""
|
||||
transport = models.Transport(**validated_data)
|
||||
self.backend.serialize(transport)
|
||||
transport.save(creator=self.context["request"].user)
|
||||
return transport
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Use backend to serialize data."""
|
||||
for key, value in validated_data.items():
|
||||
setattr(instance, key, value)
|
||||
self.backend.serialize(instance)
|
||||
instance.save()
|
||||
return instance
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Transport javascript tools.
|
||||
*/
|
||||
|
||||
function displayBackendSettings(name) {
|
||||
$('#backend_settings').find("div[class*=form-group]").hide();
|
||||
if (name !== '') {
|
||||
$('#backend_settings').find('[name*=' + name + '_]').each(function (index, input) {
|
||||
$(input).parents("div[class*=form-group]").show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function transportFormCallback () {
|
||||
var $service_field = $('#id_service');
|
||||
|
||||
$service_field.change(function (event) {
|
||||
displayBackendSettings($(this).val());
|
||||
});
|
||||
displayBackendSettings($service_field.val());
|
||||
|
||||
$('.submit').one('click', $.proxy(function(e) {
|
||||
simple_ajax_form_post(e, {
|
||||
formid: 'transport_form',
|
||||
error_cb: transportFormCallback
|
||||
});
|
||||
}, this));
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$('#searchquery').focus(function () {
|
||||
$(this).val('');
|
||||
}).blur(function (e) {
|
||||
var $this = $(this);
|
||||
if ($this.val() === '') {
|
||||
$this.val(gettext('Search'));
|
||||
}
|
||||
});
|
||||
$('a[name=deltransport]').confirm({
|
||||
question: function() { return this.$element.attr('title'); },
|
||||
method: 'DELETE'
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
{% extends "common/generic_modal_form.html" %}
|
||||
|
||||
{% load form_tags %}
|
||||
|
||||
{% block formbody %}
|
||||
{% render_field form.pattern %}
|
||||
{% render_field form.service %}
|
||||
{% render_field form.enabled %}
|
||||
<div id="backend_settings">
|
||||
{% for field in setting_fields %}
|
||||
{% render_field field hidden=True %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% load i18n %}
|
||||
|
||||
<th id="name" class="sortable" data-sort_order="name" width="">{% trans "Name" %}</th>
|
||||
<th id="tags" class="sortable" data-sort_order="tags" width="">{% trans "Tags" %}</th>
|
||||
<th id="actions" width="">{% trans "Actions" %}</th>
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends "twocols.html" %}
|
||||
|
||||
{% load i18n admin_tags lib_tags static %}
|
||||
|
||||
{% block pagetitle %}{% trans "Transports" %}{% endblock %}
|
||||
|
||||
{% block leftcol %}
|
||||
<ul class="nav nav-sidebar">
|
||||
{% domains_menu "transports" user False %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block apparea %}
|
||||
<div class="row">
|
||||
<div class="col-sm-5">
|
||||
{% include "admin/searchform.html" %}
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
{% if perms.transport.add_transport %}
|
||||
<a href="{% url 'transport:transport_create' %}" class="btn btn-primary" data-toggle="ajaxmodal" modalcb="transportFormCallback">
|
||||
<span class="fa fa-plus"></span> {% trans "Add" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="taglist" class="col-sm-4"> </div>
|
||||
</div>
|
||||
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="pattern" class="sortable" data-sort_order="pattern" width="">{% trans "Pattern" %}</th>
|
||||
<th id="service" class="sortable" data-sort_order="service" width="">{% trans "Service" %}</th>
|
||||
<th id="actions" width="">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for transport in object_list %}
|
||||
<tr class="{% if not transport.enabled %}text-muted{% endif %}">
|
||||
<td>{{ transport.pattern }}{% if not transport.enabled %} ({% trans "disabled" %}){% endif %}</td>
|
||||
<td><span class="label label-default">
|
||||
<a href="#" class="filter transport">{{ transport.service }}</a>
|
||||
</span></td>
|
||||
<td name="actions">
|
||||
<a href="{% url 'transport:transport_update' transport.pk %}"
|
||||
data-toggle="ajaxmodal" modalcb="transportFormCallback">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
<a href="{% url 'transport:transport_delete' transport.pk %}"
|
||||
name="deltransport" title="{% blocktrans with pattern=transport.pattern %}Delete {{ pattern }}? {% endblocktrans %}">
|
||||
<span class="fa fa-trash"></span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="3">
|
||||
<div class="alert alert-info">{% trans "No transport defined yet." %}</div>
|
||||
</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
<script type="text/javascript" src="{% static 'transport/js/transport.js' %}"></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Transport tests."""
|
||||
|
||||
import json
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from modoboa.lib.tests import ModoAPITestCase, ModoTestCase
|
||||
|
||||
from . import factories
|
||||
from . import models
|
||||
|
||||
|
||||
class DataMixin(object):
|
||||
"""A mixin to provide test data."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super(DataMixin, cls).setUpTestData()
|
||||
cls.transport1 = factories.TransportFactory(
|
||||
pattern="domain1.test", _settings={
|
||||
"relay_target_host": "external.host.tld",
|
||||
"relay_target_port": 25,
|
||||
"relay_verify_recipients": False
|
||||
}
|
||||
)
|
||||
cls.transport2 = factories.TransportFactory(
|
||||
pattern="domain2.test", _settings={
|
||||
"relay_target_host": "external.host.tld",
|
||||
"relay_target_port": 25,
|
||||
"relay_verify_recipients": True
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TransportViewsTestCase(DataMixin, ModoTestCase):
|
||||
"""Test transport views."""
|
||||
|
||||
def test_transport_list(self):
|
||||
"""Test list view."""
|
||||
url = reverse("transport:transport_list")
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, self.transport1.pattern)
|
||||
self.assertContains(response, self.transport2.pattern)
|
||||
|
||||
url = "{}?searchquery=domain1".format(url)
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, self.transport1.pattern)
|
||||
self.assertNotContains(response, self.transport2.pattern)
|
||||
|
||||
def test_create_transport(self):
|
||||
"""Test create view."""
|
||||
url = reverse("transport:transport_create")
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, "New transport")
|
||||
data = {
|
||||
"pattern": "domain3.test",
|
||||
"service": "relay",
|
||||
"relay_target_host": "1.2.3.4",
|
||||
}
|
||||
response = self.ajax_post(url, data, 400)
|
||||
|
||||
data.update({"relay_target_port": 25})
|
||||
self.ajax_post(url, data)
|
||||
transport = models.Transport.objects.get(pattern="domain3.test")
|
||||
self.assertEqual(
|
||||
transport.next_hop, "[{}]:{}".format(
|
||||
data["relay_target_host"], data["relay_target_port"])
|
||||
)
|
||||
|
||||
def test_update_transport(self):
|
||||
"""Test update view."""
|
||||
url = reverse("transport:transport_update", args=[self.transport1.pk])
|
||||
response = self.client.get(url)
|
||||
self.assertContains(response, "Edit transport")
|
||||
data = {
|
||||
"pattern": self.transport1.pattern,
|
||||
"service": self.transport1.service,
|
||||
"relay_target_host": "1.2.3.5",
|
||||
"relay_target_port": 234,
|
||||
"relay_verify_recipients": True
|
||||
}
|
||||
self.ajax_post(url, data)
|
||||
self.transport1.refresh_from_db()
|
||||
self.assertTrue(
|
||||
self.transport1._settings["relay_verify_recipients"])
|
||||
self.assertEqual(
|
||||
self.transport1.next_hop, "[{}]:{}".format(
|
||||
data["relay_target_host"], data["relay_target_port"])
|
||||
)
|
||||
|
||||
def test_delete_transport(self):
|
||||
"""Test delete view."""
|
||||
url = reverse("transport:transport_delete", args=[self.transport1.pk])
|
||||
response = self.client.delete(url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
with self.assertRaises(models.Transport.DoesNotExist):
|
||||
self.transport1.refresh_from_db()
|
||||
|
||||
|
||||
class TransportAPITestCase(DataMixin, ModoAPITestCase):
|
||||
"""API test cases."""
|
||||
|
||||
def test_list(self):
|
||||
"""Test list service."""
|
||||
url = reverse("api:transport-list")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(len(response.data), 2)
|
||||
|
||||
def test_create(self):
|
||||
"""Test create service."""
|
||||
url = reverse("api:transport-list")
|
||||
settings = {
|
||||
"relay_target_host": "1.2.3.4"
|
||||
}
|
||||
data = {
|
||||
"pattern": "domain3.test",
|
||||
"service": "relay",
|
||||
"_settings": json.dumps(settings)
|
||||
}
|
||||
response = self.client.post(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(
|
||||
response.data["_settings"],
|
||||
["relay_target_port: This field is required"]
|
||||
)
|
||||
|
||||
settings.update({"relay_target_port": 25})
|
||||
data["_settings"] = json.dumps(settings)
|
||||
response = self.client.post(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
transport = models.Transport.objects.get(pattern="domain3.test")
|
||||
self.assertEqual(
|
||||
transport.next_hop, "[{}]:{}".format(
|
||||
settings["relay_target_host"], settings["relay_target_port"])
|
||||
)
|
||||
|
||||
def test_update(self):
|
||||
"""Test update service."""
|
||||
url = reverse("api:transport-detail", args=[self.transport1.pk])
|
||||
settings = self.transport1._settings.copy()
|
||||
settings.update({"relay_target_port": 1000})
|
||||
data = {
|
||||
"pattern": "domain3.test",
|
||||
"service": "relay",
|
||||
"_settings": json.dumps(settings)
|
||||
}
|
||||
response = self.client.put(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.transport1.refresh_from_db()
|
||||
self.assertEqual(self.transport1.pattern, data["pattern"])
|
||||
self.assertEqual(
|
||||
self.transport1.next_hop, "[{}]:{}".format(
|
||||
settings["relay_target_host"], settings["relay_target_port"])
|
||||
)
|
||||
|
||||
def test_delete(self):
|
||||
"""Test delete service."""
|
||||
url = reverse("api:transport-detail", args=[self.transport1.pk])
|
||||
response = self.client.delete(url)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
with self.assertRaises(models.Transport.DoesNotExist):
|
||||
self.transport1.refresh_from_db()
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Transport urls."""
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^$", views.TransportListView.as_view(),
|
||||
name="transport_list"),
|
||||
url("^create/$", views.TransportCreateView.as_view(),
|
||||
name="transport_create"),
|
||||
url("^update/(?P<pk>\d+)/$", views.TransportUpdateView.as_view(),
|
||||
name="transport_update"),
|
||||
url("^delete/(?P<pk>\d+)/$", views.TransportDeleteView.as_view(),
|
||||
name="transport_delete"),
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Transport API urls."""
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from . import viewsets
|
||||
|
||||
|
||||
router = routers.SimpleRouter()
|
||||
router.register(
|
||||
r"transports", viewsets.TransportViewSet, base_name="transport")
|
||||
urlpatterns = router.urls
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Transport views."""
|
||||
|
||||
from django import http
|
||||
from django.core.urlresolvers import reverse, reverse_lazy
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.views import generic
|
||||
|
||||
from django.contrib.auth import mixins as auth_mixins
|
||||
from django.contrib import messages
|
||||
|
||||
from . import backends
|
||||
from . import forms
|
||||
from . import models
|
||||
|
||||
|
||||
class TransportListView(
|
||||
auth_mixins.PermissionRequiredMixin, generic.ListView):
|
||||
"""ListView for Transport."""
|
||||
|
||||
model = models.Transport
|
||||
permission_required = "transport.add_transport"
|
||||
template_name = "transport/transport_list.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Inject search query."""
|
||||
context = super(TransportListView, self).get_context_data(**kwargs)
|
||||
context.update({
|
||||
"selection": "domains",
|
||||
"squery": self.request.GET.get("searchquery")
|
||||
})
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter."""
|
||||
qs = super(TransportListView, self).get_queryset()
|
||||
searchquery = self.request.GET.get("searchquery")
|
||||
if searchquery:
|
||||
qs = qs.filter(pattern__icontains=searchquery)
|
||||
return qs
|
||||
|
||||
|
||||
class TransportMixin(object):
|
||||
"""Mixin for Transport creation/update."""
|
||||
|
||||
form_class = forms.TransportForm
|
||||
model = models.Transport
|
||||
success_message = ugettext_lazy("Transport created")
|
||||
success_url = reverse_lazy("transport:transport_list")
|
||||
template_name = "transport/_transport_form.html"
|
||||
|
||||
def form_invalid(self, form):
|
||||
"""Return JSON response."""
|
||||
return http.JsonResponse({
|
||||
"form_errors": form.errors,
|
||||
"prefix": form.prefix
|
||||
}, status=400)
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Save transport."""
|
||||
transport = form.save(commit=False)
|
||||
backend = backends.manager.get_backend(transport.service)
|
||||
backend.serialize(transport)
|
||||
options = {}
|
||||
if not transport.pk:
|
||||
options.update({"creator": self.request.user})
|
||||
transport.save(**options)
|
||||
messages.success(self.request, self.success_message)
|
||||
return http.JsonResponse({})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Include extra data."""
|
||||
context = super(TransportMixin, self).get_context_data(
|
||||
**kwargs)
|
||||
setting_fields = []
|
||||
for fname in context["form"].setting_fields:
|
||||
setting_fields.append(context["form"][fname])
|
||||
context.update({
|
||||
"action_classes": "submit",
|
||||
"formid": "transport_form",
|
||||
"setting_fields": setting_fields
|
||||
})
|
||||
return context
|
||||
|
||||
|
||||
class TransportCreateView(
|
||||
auth_mixins.PermissionRequiredMixin,
|
||||
TransportMixin,
|
||||
generic.CreateView):
|
||||
"""Transport create view."""
|
||||
|
||||
permission_required = "transport.add_transport"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Include extra data."""
|
||||
context = super(TransportCreateView, self).get_context_data(
|
||||
**kwargs)
|
||||
context.update({
|
||||
"action": reverse("transport:transport_create"),
|
||||
"action_label": _("Create"),
|
||||
"title": _("New transport"),
|
||||
})
|
||||
return context
|
||||
|
||||
|
||||
class TransportUpdateView(
|
||||
auth_mixins.PermissionRequiredMixin,
|
||||
TransportMixin,
|
||||
generic.UpdateView):
|
||||
"""Transport update view."""
|
||||
|
||||
permission_required = "transport.change_transport"
|
||||
success_message = ugettext_lazy("Transport updated")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Include extra data."""
|
||||
context = super(TransportUpdateView, self).get_context_data(
|
||||
**kwargs)
|
||||
context.update({
|
||||
"action": reverse(
|
||||
"transport:transport_update", args=[self.object.pk]),
|
||||
"action_label": _("Update"),
|
||||
"title": _("Edit transport"),
|
||||
})
|
||||
return context
|
||||
|
||||
|
||||
class TransportDeleteView(
|
||||
auth_mixins.PermissionRequiredMixin,
|
||||
TransportMixin,
|
||||
generic.DeleteView):
|
||||
"""Transport delete view."""
|
||||
|
||||
model = models.Transport
|
||||
permission_required = "transport.del_transport"
|
||||
success_url = reverse_lazy("transport:transport_list")
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
"""Add message."""
|
||||
response = super(TransportDeleteView, self).delete(
|
||||
request, *args, **kwargs)
|
||||
messages.success(request, _("Transport deleted"))
|
||||
return response
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Transport viewsets."""
|
||||
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
|
||||
|
||||
from . import models
|
||||
from . import serializers
|
||||
|
||||
|
||||
class TransportViewSet(viewsets.ModelViewSet):
|
||||
"""Transport viewset."""
|
||||
|
||||
permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
|
||||
serializer_class = serializers.TransportSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter queryset based on current user."""
|
||||
return models.Transport.objects.get_for_admin(self.request.user)
|
||||
+2
-2
@@ -28,8 +28,8 @@ urlpatterns = [
|
||||
url('', include('modoboa.core.urls', namespace="core")),
|
||||
url('^user/forward/', forward, name="user_forward"),
|
||||
url('admin/', include('modoboa.admin.urls', namespace="admin")),
|
||||
url('relaydomains/',
|
||||
include('modoboa.relaydomains.urls', namespace="relaydomains")),
|
||||
url(r'^transports/',
|
||||
include("modoboa.transport.urls", namespace="transport")),
|
||||
# No namespace
|
||||
url(r'^accounts/password_reset/$', core_views.password_reset,
|
||||
name="password_reset"),
|
||||
|
||||
@@ -10,5 +10,6 @@ from modoboa.core.extensions import exts_pool
|
||||
urlpatterns = [
|
||||
url("", include("modoboa.admin.urls_api")),
|
||||
url("", include("modoboa.limits.urls_api")),
|
||||
url("", include("modoboa.transport.urls_api")),
|
||||
]
|
||||
urlpatterns += exts_pool.get_urls(category="api")
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
django>=1.10,<=1.10.99
|
||||
django-braces==1.11.0
|
||||
django-ckeditor==5.2.1
|
||||
django-reversion==1.10.2
|
||||
django-subcommand2==0.1.1
|
||||
@@ -13,12 +14,12 @@ djangorestframework==3.7.1
|
||||
|
||||
argparse
|
||||
# bcrypt requires libffi-dev
|
||||
bcrypt==3.1.3
|
||||
bcrypt==3.1.4
|
||||
dnspython==1.15.0
|
||||
feedparser==5.2.1
|
||||
gevent==1.2.1
|
||||
ipaddress
|
||||
jsonfield==1.0.3
|
||||
jsonfield==2.0.2
|
||||
passlib==1.7.1
|
||||
progressbar33==2.4
|
||||
py-dateutil==2.2
|
||||
|
||||
@@ -60,6 +60,7 @@ MODOBOA_APPS = (
|
||||
'modoboa.lib',
|
||||
'modoboa.admin',
|
||||
'modoboa.limits',
|
||||
'modoboa.transport',
|
||||
'modoboa.relaydomains',
|
||||
'modoboa.parameters',
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user