Initial support of relay domains.

see #491
This commit is contained in:
Antoine Nguyen
2013-10-18 16:09:29 +02:00
parent f148eb2968
commit 75ecddf879
32 changed files with 864 additions and 114 deletions
-12
View File
@@ -230,18 +230,6 @@ def get_modoboa_logo():
return logo
@register.simple_tag
def extra_head_content(user):
tpl = template.Template(
"{% for sc in static_content %}{{ sc|safe }}{% endfor %}"
)
return tpl.render(
template.Context({
'static_content': events.raiseQueryEvent("GetStaticContent", user)
})
)
@register.simple_tag
def load_optionalmenu(user):
menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu_middle", user)
+5 -4
View File
@@ -43,12 +43,13 @@ def password_change(user):
@events.observe("GetStaticContent")
def get_static_content(user):
if not user.mailbox_set.count():
def get_static_content(caller, user):
if caller != 'top' and not user.mailbox_set.count():
return []
return """<script type="text/javascript">
return ["""<script type="text/javascript">
$(document).ready(function() {
$(document).on('click', 'a[name=sendspam]', simple_ajax_request);
$(document).on('click', 'a[name=sendvirus]', simple_ajax_request);
});
</script>"""
</script>"""]
+5
View File
@@ -15,6 +15,11 @@ admin_events = [
"CreateDomain",
"DomainModified",
"DeleteDomain",
"ExtraDomainEntries",
"ExtraDomainMenuEntries",
"ExtraDomainFilters",
"GetDomainActions",
"GetDomainModifyLink",
"DomainAliasCreated",
"DomainAliasDeleted",
+26 -4
View File
@@ -1,13 +1,16 @@
# coding: utf-8
from functools import wraps
from itertools import chain
from django.db.models import Q
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.contrib.contenttypes.models import ContentType
from modoboa.core.models import User
from modoboa.lib import parameters
from modoboa.lib import parameters, events
from modoboa.lib.exceptions import ModoboaException
from modoboa.lib.emailutils import split_mailbox
from modoboa.extensions.admin.models import Alias
from modoboa.extensions.admin.models import (
Domain, Alias
)
def needs_mailbox():
@@ -65,8 +68,6 @@ def get_identities(user, searchquery=None, idtfilter=None, grpfilter=None):
:param list grpfilter: group names filters
:return: a queryset
"""
from itertools import chain
accounts = []
if idtfilter is None or not idtfilter or idtfilter == "account":
ids = user.objectaccess_set \
@@ -104,3 +105,24 @@ def get_identities(user, searchquery=None, idtfilter=None, grpfilter=None):
if idtfilter is not None and idtfilter:
aliases = [al for al in aliases if al.type == idtfilter]
return chain(accounts, aliases)
def get_domains(user, domfilter=None, searchquery=None, **extrafilters):
"""Return all the domains the user can access.
:param ``User`` user: user object
:param str searchquery: filter
:rtype: list
:return: a list of domains and/or relay domains
"""
domains = []
if domfilter is None or not domfilter or domfilter == 'domain':
domains = Domain.objects.get_for_admin(user)
if searchquery is not None:
q = Q(name__contains=searchquery)
q |= Q(domainalias__name__contains=searchquery)
domains = domains.filter(q).distinct()
extra_domain_entries = events.raiseQueryEvent(
'ExtraDomainEntries', user, domfilter, searchquery, **extrafilters
)
return chain(domains, extra_domain_entries)
@@ -75,6 +75,13 @@ class Migration(SchemaMigration):
'use_domain_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"})
},
u'admin.mailboxoperation': {
'Meta': {'object_name': 'MailboxOperation'},
'argument': ('django.db.models.fields.TextField', [], {}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.Mailbox']", 'null': 'True', 'blank': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
},
'admin.objectdates': {
'Meta': {'object_name': 'ObjectDates'},
'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+3 -1
View File
@@ -1,8 +1,10 @@
from .base import DatesAware
from .domain import Domain
from .domain_alias import DomainAlias
from .mailbox import Mailbox, Quota, MailboxOperation
from .alias import Alias
__all__ = [
'Domain', 'DomainAlias', 'Mailbox', 'Quota', 'Alias', 'MailboxOperation'
'Domain', 'DomainAlias', 'Mailbox', 'Quota', 'Alias',
'MailboxOperation', 'DatesAware'
]
@@ -56,6 +56,10 @@ class Domain(DatesAware):
def mbalias_count(self):
return self.alias_set.count()
@property
def tags(self):
return [{"name": "domain", "label": _("Domain"), "type": "dom"}]
@property
def admins(self):
"""Return the domain administrators of this domain
@@ -1,3 +1,8 @@
/**
* @module admin
* @desc Utility classes for the admin application
*/
var Admin = function(options) {
Listing.call(this, options);
};
@@ -12,6 +17,7 @@ Admin.prototype = {
Listing.prototype.initialize.call(this, options);
this.options = $.extend({}, this.defaults, this.options);
this.options.defcallback = $.proxy(this.list_cb, this);
this.tag_handlers = {};
this.navobj = new History(this.options);
@@ -37,12 +43,61 @@ Admin.prototype = {
list_cb: function(data) {
$("#listing").html(data.table);
this.update_listing(data);
$("a.filter").click($.proxy(this.filter_by_tag, this));
},
listen: function() {
$("#searchform").submit($.proxy(this.do_search, this));
},
register_tag_handler: function(name, handler) {
this.tag_handlers[name] = handler;
if (this.navobj.getparam(name + "filter") != undefined) {
var text = this.navobj.getparam(name + "filter");
$("#searchform").parent().after(this.make_tag(text, name));
}
},
generic_tag_handler: function(tag, $link) {
if (this.navobj.getparam(tag + "filter") == undefined && $link.hasClass(tag)) {
var text = $link.attr("name");
this.navobj.setparam(tag + "filter", text).update();
$("#searchform").parent().after(this.make_tag(text, tag));
return true;
}
return false;
},
make_tag: function(text, type) {
var $tag = $("<a />", {"name": type, "class" : "btn btn-mini", "html": text});
var $i = $("<i />", {"class" : "icon-remove"}).prependTo($tag);
$tag.click($.proxy(this.remove_tag, this));
return $tag;
},
remove_tag: function(e) {
var $tag = $(e.target);
if ($tag.is("i")) {
$tag = $tag.parent();
}
e.preventDefault();
this.navobj.delparam($tag.attr("name") + "filter").update();
$tag.remove();
},
filter_by_tag: function(e) {
var $link = $(e.target);
e.preventDefault();
for (var name in this.tag_handlers) {
if (this.tag_handlers[name].apply(this, [name, $link])) {
break;
}
}
},
do_search: function(e) {
e.preventDefault();
var squery = $("#searchquery").val();
@@ -104,6 +159,11 @@ var Domains = function(options) {
};
Domains.prototype = {
initialize: function(options) {
Admin.prototype.initialize.call(this, options);
this.register_tag_handler("dom", this.generic_tag_handler);
},
list_cb: function(data) {
Admin.prototype.list_cb.call(this, data);
var deloptions = (data.handle_mailboxes)
@@ -120,6 +180,11 @@ Domains.prototype = {
checkboxes: deloptions,
success_cb: $.proxy(this.reload_listing, this)
});
$("a[name=delrelaydomain]").confirm({
question: function() { return this.$element.attr('title'); },
method: "DELETE",
success_cb: $.proxy(this.reload_listing, this)
});
},
change_inputs_state: function(value) {
@@ -193,6 +258,17 @@ Domains.prototype = {
});
}, this));
$(document).trigger('domform_init');
},
rdomainform_cb: function() {
$(".submit").one('click', $.proxy(function(e) {
simple_ajax_form_post2(e, {
formid: "rdomform",
error_cb: $.proxy(this.rdomainform_cb, this),
reload_on_success: false,
success_cb: $.proxy(this.reload_listing, this)
});
}, this));
}
};
@@ -209,14 +285,8 @@ var Identities = function(options) {
Identities.prototype = {
initialize: function(options) {
Admin.prototype.initialize.call(this, options);
if (this.navobj.getparam("idtfilter") != undefined) {
var text = this.navobj.getparam("idtfilter");
$("#searchform").parent().after(this.make_tag(text, "idt"));
}
if (this.navobj.getparam("grpfilter") != undefined) {
var text = this.navobj.getparam("grpfilter");
$("#searchform").parent().after(this.make_tag(text, "grp"));
}
this.register_tag_handler("idt", this.generic_tag_handler);
this.register_tag_handler("grp", this.grp_tag_handler);
},
listen: function() {
@@ -237,7 +307,6 @@ Identities.prototype = {
if (data.handle_mailboxes == "yes") {
deloptions = {keepdir: gettext("Do not delete mailbox directory")};
}
$("a.filter").click($.proxy(this.filter_by_tag, this));
$("a[name=delaccount]").confirm({
question: function() { return this.$element.attr('title'); },
@@ -252,46 +321,20 @@ Identities.prototype = {
});
},
make_tag: function(text, type) {
var $tag = $("<a />", {"name": type, "class" : "btn btn-mini", "html": text});
var $i = $("<i />", {"class" : "icon-remove"}).prependTo($tag);
$tag.click($.proxy(this.remove_tag, this));
return $tag;
},
remove_tag: function(e) {
var $tag = $(e.target);
if ($tag.is("i")) {
$tag = $tag.parent();
}
e.preventDefault();
this.navobj.delparam($tag.attr("name") + "filter").update();
$tag.remove();
},
filter_by_tag: function(e) {
var $link = $(e.target);
e.preventDefault();
if (this.navobj.getparam("idtfilter") == undefined && $link.hasClass("idt")) {
var text = $link.attr("name");
this.navobj.setparam("idtfilter", text).update();
$("#searchform").parent().after(this.make_tag(text, "idt"));
return;
}
if (this.navobj.getparam("grpfilter") == undefined && $link.hasClass("grp")) {
grp_tag_handler: function(tag, $link) {
if (this.navobj.getparam(tag + "filter") == undefined && $link.hasClass(tag)) {
var text = $link.attr("name");
this.navobj
.setparam("idtfilter", "account")
.setparam("grpfilter", text)
.setparam(tag + "filter", text)
.update();
if ($("a[name=idt]").length == 0) {
$("#searchform").parent().after(this.make_tag("account", "idt"));
}
$("#searchform").parent().after(this.make_tag(text, "grp"));
$("#searchform").parent().after(this.make_tag(text, tag));
return true;
}
return false;
},
simpleuser_mode: function() {
@@ -7,5 +7,6 @@ $(document).ready(function() {
admin = new Domains();
});
</script>
{% extra_static_content "domains" user %}
{% endblock %}
{% block actions %}{% domains_menu "domains" user %}{% endblock %}
@@ -0,0 +1,21 @@
{% load i18n admin_tags lib_tags %}
<table id="{{ tableid }}" class="table {{ styles }}">
<thead>
<tr>
<th id="identity" class="sortable" data-sort_order="identity" width="">{% trans "Name" %}</th>
<th id="name_or_rcpt" class="sortable" data-sort_order="name_or_rcpt" width="">{% trans "Alias(es)" %}</th>
<th id="tags" class="sortable" data-sort_order="tags" width="">{% trans "Tags" %}</th>
<th id="actions" width="">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for domain in domains %}
<tr id="{{ domain.id }}" class="{% if not domain.enabled %}muted{% endif %}" name="{{ forloop.counter }}">
<td name="name">{% domain_modify_link domain %}</td>
<td name="aliases">{% domain_aliases domain %} </td>
<td name="tags">{% render_tags domain.tags %}</td>
<td name="actions">{% domain_actions user domain %}</td>
</tr>
{% endfor %}
</tbody>
</table>
@@ -20,5 +20,6 @@ $(document).ready(function() {
admin = new Identities();
});
</script>
{% extra_static_content "identities" user %}
{% endblock %}
{% block actions %}{% identities_menu user %}{% endblock %}
@@ -1,4 +1,4 @@
{% extends "admin/header.html" %}{% load i18n admin_tags lib_tags %}
{% extends "admin/header.html" %}{% load i18n admin_tags %}
{% block html_head %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/jquery.sortable.css" />
@@ -15,6 +15,16 @@ genders = {
@register.simple_tag
def domains_menu(selection, user):
"""Specific menu for domain related operations.
Corresponds to the menu visible on the left column when you go to
Domains.
:param str selection: menu entry currently selected
:param ``User`` user: connected user
:rtype: str
:return: rendered menu (as HTML)
"""
if not user.has_perm("admin.add_domain"):
return ""
@@ -25,6 +35,9 @@ def domains_menu(selection, user):
"modal": True,
"modalcb": "admin.newdomain_cb",
"url": reverse("modoboa.extensions.admin.views.domain.newdomain")},
]
entries += events.raiseQueryEvent("ExtraDomainMenuEntries", user)
entries += [
{"name": "import",
"label": _("Import"),
"img": "icon-folder-open",
@@ -111,23 +124,28 @@ def identities_menu(user):
@register.simple_tag
def domain_actions(user, domid):
from modoboa.extensions.admin.models import Domain
def domain_actions(user, domain):
actions = []
if domain.__class__.__name__ == 'Domain':
actions = [
{"name": "listidentities",
"url": reverse("modoboa.extensions.admin.views.identity.identities") + "#list/?searchquery=@%s" % domain.name,
"title": _("View the domain's identities"),
"img": "icon-user"}
]
if user.has_perm("admin.delete_domain"):
actions.append({
"name": "deldomain",
"url": reverse(
"modoboa.extensions.admin.views.domain.deldomain",
args=[domain.id]
),
"title": _("Delete %s?" % domain.name),
"img": "icon-trash"
})
else:
actions = events.raiseQueryEvent('GetDomainActions', user, domain)
domain = Domain.objects.get(pk=domid)
actions = [
{"name": "listidentities",
"url": reverse("modoboa.extensions.admin.views.identity.identities") + "#list/?searchquery=@%s" % domain.name,
"title": _("View the domain's identities"),
"img": "icon-user"}
]
if user.has_perm("admin.delete_domain"):
actions.append({
"name": "deldomain",
"url": reverse("modoboa.extensions.admin.views.domain.deldomain", args=[domid]),
"title": _("Delete %s?" % domain.name),
"img": "icon-trash"
})
return render_actions(actions)
@@ -153,8 +171,50 @@ def identity_actions(user, ident):
return render_actions(actions)
@register.simple_tag
def domain_modify_link(domain):
linkdef = {"label": domain.name, "modal": True}
if domain.__class__.__name__ == "Domain":
linkdef["url"] = reverse(
"modoboa.extensions.admin.views.domain.editdomain",
args=[domain.id]
)
linkdef["modalcb"] = "admin.domainform_cb"
else:
tmp = events.raiseDictEvent('GetDomainModifyLink', domain)
for key in ['url', 'modalcb']:
linkdef[key] = tmp[key]
return render_link(linkdef)
@register.simple_tag
def domain_aliases(domain):
"""Display domain aliases of this domain.
:param domain:
:rtype: str
"""
if domain.__class__.__name__ == "Domain":
if not domain.domainalias_set.count():
return '---'
res = ""
for da in domain.domainalias_set.all():
res += "%s<br/>" % da.name
return res
return '---'
@register.simple_tag
def identity_modify_link(identity, active_tab='default'):
"""Return the appropriate modification link.
According to the identity type, a specific modification link (URL)
must be used.
:param identity: a ``User`` or ``Alias`` instance
:param str active_tab: the tab to display
:rtype: str
"""
linkdef = {"label": identity.identity, "modal": True}
if identity.__class__.__name__ == "User":
linkdef["url"] = reverse(
+21 -10
View File
@@ -15,13 +15,13 @@ from modoboa.lib.webutils import (
from modoboa.lib.formutils import CreationWizard
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.templatetags.lib_tags import pagination_bar
from modoboa.extensions.admin.tables import DomainsTable
from modoboa.extensions.admin.lib import get_sort_order, get_listing_page
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.models import Domain, Mailbox
from modoboa.extensions.admin.forms import (
DomainForm, DomainFormGeneral, DomainFormOptions
)
from modoboa.extensions.admin.lib import get_domains
@login_required
@@ -35,17 +35,28 @@ def index(request):
)
def _domains(request):
sort_order, sort_dir = get_sort_order(request.GET, "name")
domains = Domain.objects.get_for_admin(request.user)
squery = request.GET.get("searchquery", None)
if squery is not None:
q = Q(name__contains=squery)
q |= Q(domainalias__name__contains=squery)
domains = domains.filter(q).distinct()
filters = dict(
(flt, request.GET.get(flt, None))
for flt in ['domfilter', 'searchquery']
+ events.raiseQueryEvent('ExtraDomainFilters')
)
domainlist = get_domains(
request.user, **filters
)
if sort_order in ["name", "domainalias__name"]:
domains = domains.order_by("%s%s" % (sort_dir, sort_order))
page = get_listing_page(domains, request.GET.get("page", 1))
domainlist = sorted(
domainlist,
key=lambda d: getattr(d, sort_order), reverse=sort_dir == '-'
)
else:
domainlist = sorted(domainlist, key=lambda d: d.tags[0],
reverse=sort_dir == '-')
page = get_listing_page(domainlist, request.GET.get("page", 1))
return ajax_simple_response({
"table": DomainsTable(request, page.object_list).render(),
"table": _render_to_string(request, 'admin/domains_table.html', {
'domains': domainlist,
'tableid': 'domains'
}),
"page": page.number,
"paginbar": pagination_bar(page),
"handle_mailboxes": parameters.get_admin("HANDLE_MAILBOXES",
+23 -17
View File
@@ -95,13 +95,14 @@ def on_delete_domain(domain):
@events.observe("GetStaticContent")
def extra_static_content(user):
def extra_static_content(caller, user):
if user.group == "SimpleUsers":
return []
tpl = Template("""<script type="text/javascript">
if caller == 'top' and parameters.get_admin("USER_CAN_RELEASE") == 'no':
tpl = Template("""<script type="text/javascript">
$(document).ready(function() {
{% if user_can_release == "no" %}var poller = new Poller("{{ url }}", {
var poller = new Poller("{{ url }}", {
interval: {{ interval }},
success_cb: function(data) {
var $link = $("#nbrequests");
@@ -112,22 +113,27 @@ $(document).ready(function() {
$link.parent().addClass('hidden');
}
}
});{% endif %}
$(document).bind('domform_init', function() {
activate_widget.call($('#id_spam_subject_tag2_act'));
});
});
</script>""")
</script>
""")
return [tpl.render(
Context({
'url': reverse("modoboa.extensions.amavis.views.nbrequests"),
'interval': int(parameters.get_admin("CHECK_REQUESTS_INTERVAL")) * 1000,
'text': _("pending requests"),
})
)]
return [tpl.render(
Context(dict(
url=reverse("modoboa.extensions.amavis.views.nbrequests"),
interval=int(parameters.get_admin("CHECK_REQUESTS_INTERVAL")) * 1000,
text=_("pending requests"),
user_can_release=parameters.get_admin("USER_CAN_RELEASE")
))
)]
if caller == 'domains':
tpl = Template("""<script type="text/javascript">
$(document).bind('domform_init', function() {
activate_widget.call($('#id_spam_subject_tag2_act'));
});
</script>
""")
return [tpl.render(Context({}))]
return []
@events.observe("TopNotifications")
+5 -3
View File
@@ -116,10 +116,12 @@ def fill_account_instances(user, account, instances):
@events.observe("GetStaticContent")
def get_static_content(user):
def get_static_content(caller, user):
if caller not in ['domains', 'identities']:
return []
if user.group == "SimpleUsers":
return []
return """<style>
return ["""<style>
.resource {
margin: 5px 0;
}
@@ -137,4 +139,4 @@ $(document).ready(function() {
$(".progress").tooltip();
});
</script>
"""
"""]
@@ -0,0 +1,118 @@
# coding: utf-8
from django.core.urlresolvers import reverse
from django.template import Template, Context
from django.conf import settings
from django.utils.translation import ugettext_lazy
from django.db.models import Q
from modoboa.lib import events, parameters
from modoboa.core.extensions import ModoExtension, exts_pool
from .models import RelayDomain, Service
extension_events = [
"RelayDomainCreated",
]
class PostfixRelayDomains(ModoExtension):
name = "postfix_relay_domains"
label = "Postfix relay domains"
version = "1.0"
description = ugettext_lazy("Relay domains support for Postfix")
def init(self):
"""Initialisation method.
Only run once, when the extension is enabled. Populates the
service table with default entries.
"""
for service_name in ['relay', 'smtp']:
Service.objects.get_or_create(name=service_name)
def load(self):
from .app_settings import AdminParametersForm
parameters.register(AdminParametersForm, ugettext_lazy("Relay domains"))
events.declare(extension_events)
def destroy(self):
events.unregister('GetExtraDomainEntries', extra_domain_entries)
exts_pool.register_extension(PostfixRelayDomains)
@events.observe('GetStaticContent')
def static_content(caller, user):
if caller != 'domains':
return []
t = Template("""<script src="{{ STATIC_URL }}postfix_relay_domains/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}))]
@events.observe('ExtraDomainFilters')
def extra_domain_filters():
return ['srvfilter']
@events.observe('ExtraDomainMenuEntries')
def extra_domain_menu_entries(user):
return [
{"name": "newrelaydomain",
"label": ugettext_lazy("Add relay domain"),
"img": "icon-plus",
"modal": True,
"modalcb": "rdomain.domainform_cb",
"url": reverse(
"modoboa.extensions.postfix_relay_domains.views.create"
)},
]
@events.observe('ExtraDomainEntries')
def extra_domain_entries(user, domfilter, searchquery, **extrafilters):
if domfilter is not None and domfilter and domfilter != 'relaydomain':
return []
relay_domains = RelayDomain.objects.get_for_admin(user)
if searchquery is not None:
q = Q(name__contains=searchquery)
relay_domains = relay_domains.filter(q).distinct()
if 'srvfilter' in extrafilters and extrafilters['srvfilter']:
relay_domains = relay_domains.filter(
Q(service__name=extrafilters['srvfilter'])
)
return relay_domains
@events.observe('GetDomainModifyLink')
def rdomain_modify_link(domain):
if not isinstance(domain, RelayDomain):
return {}
return {
'url': reverse(
'modoboa.extensions.postfix_relay_domains.views.edit',
args=[domain.id]
),
'modalcb': 'rdomain.domainform_cb'
}
@events.observe('GetDomainActions')
def rdomain_actions(user, domain):
if not isinstance(domain, RelayDomain):
return []
if not user.has_perm("postfix_relay_domains.delete_relaydomain"):
return []
return [{
"name": "delrelaydomain",
"url": reverse("modoboa.extensions.postfix_relay_domains.views.delete",
args=[domain.id]),
"title": ugettext_lazy("Delete %s?" % domain.name),
"img": "icon-trash"
}]
@@ -0,0 +1,14 @@
from django import forms
from django.utils.translation import ugettext_lazy
from modoboa.lib.formutils import SeparatorField
from modoboa.lib import parameters
class AdminParametersForm(parameters.AdminParametersForm):
app = "postfix_relay_domains"
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')
)
@@ -0,0 +1,12 @@
from django import forms
from .models import RelayDomain
class RelayDomainForm(forms.ModelForm):
class Meta:
model = RelayDomain
exclude = ['dates']
widgets = {
'service': forms.Select(attrs={'class': 'span2'})
}
@@ -0,0 +1,129 @@
import reversion
from django.db import models
from django.db.models.manager import Manager
from django.contrib.contenttypes import generic
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.core.models import ObjectAccess
from modoboa.lib import events, parameters
from modoboa.lib.permissions import ungrant_access_to_object
from modoboa.extensions.admin.models import DatesAware
class RelayDomainManager(Manager):
def get_for_admin(self, admin):
"""Return the relay 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_query_set()
return self.get_query_set().filter(owners__user=admin)
class ServiceManager(Manager):
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(parameters.get_admin('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 not service.name in services:
to_delete.append(service.name)
Service.objects.filter(name__in=to_delete()).delete()
class Service(models.Model):
name = models.CharField(
ugettext_lazy('name'), max_length=100, unique=True,
help_text=ugettext_lazy('The service name')
)
objects = ServiceManager()
def __str__(self):
return self.name
class RelayDomain(DatesAware):
"""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.
"""
name = models.CharField(
ugettext_lazy('name'), max_length=100, unique=True,
help_text=ugettext_lazy('The domain name')
)
target_host = models.CharField(
ugettext_lazy('target host'), max_length=255,
help_text=ugettext_lazy('')
)
service = models.ForeignKey(Service, default='relay')
enabled = models.BooleanField(
ugettext_lazy('enabled'),
help_text=ugettext_lazy('Check to activate this domain')
)
verify_recipients = models.BooleanField(
ugettext_lazy('verify recipients'),
help_text=ugettext_lazy('')
)
owners = generic.GenericRelation(ObjectAccess)
objects = RelayDomainManager()
class Meta:
ordering = ['name']
@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.name
def post_create(self, creator):
from modoboa.lib.permissions import grant_access_to_object
grant_access_to_object(creator, self, is_owner=True)
events.raiseEvent("RelayDomainCreated", creator, self)
def save(self, *args, **kwargs):
if "creator" in kwargs:
creator = kwargs["creator"]
del kwargs["creator"]
else:
creator = None
super(RelayDomain, self).save(*args, **kwargs)
if creator is not None:
self.post_create(creator)
def delete(self):
events.raiseEvent("RelayDomainDeleted", self)
ungrant_access_to_object(self)
super(RelayDomain, self).delete()
reversion.register(RelayDomain)
@@ -0,0 +1,79 @@
/**
* @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);
},
launch_scan: function(e) {
var $link = get_target(e);
e.preventDefault();
$.ajax({
type: "POST",
url: $link.attr("href")
}).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) {
if (jqxhr.getResponseHeader('Content-Type') != 'application/json') {
return;
}
var result = $.parseJSON(jqxhr.responseText);
$('.modal-body').prepend(
build_error_alert(result[0])
);
});
},
domainform_cb: function() {
$('#btnscan').click(this.launch_scan);
$(".submit").one('click', $.proxy(function(e) {
simple_ajax_form_post2(e, {
formid: "rdomform",
error_cb: $.proxy(this.domainform_cb, this),
reload_on_success: false,
success_cb: $.proxy(admin.reload_listing, admin)
});
}, this));
},
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) {
$("#searchform").parent().after(this.make_tag("relaydomain", "dom"));
}
$("#searchform").parent().after(this.make_tag(text, tag));
return true;
}
return false;
}
};
@@ -0,0 +1,7 @@
{% extends "common/generic_modal_form.html" %}{% load lib_tags %}{% block formbody %}
{% render_field form.name %}
{% render_field form.target_host %}
{% include "postfix_relay_domains/service_field.html" with field=form.service %}
{% render_field form.enabled %}
{% render_field form.verify_recipients %}
{% endblock %}
@@ -0,0 +1,2 @@
{% extends "common/generic_field.html" %}{% load i18n %}
{% block extra_controls %}<a id="btnscan" class="btn btn-success" style="margin-bottom: 8px" href="{% url 'modoboa.extensions.postfix_relay_domains.views.scan_for_services' %}">{% trans "Scan" %}</a>{% endblock %}
@@ -0,0 +1,16 @@
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)
@@ -0,0 +1,9 @@
from django.conf.urls import patterns
urlpatterns = patterns(
'modoboa.extensions.postfix_relay_domains.views',
(r'^relaydomains/new/', 'create'),
(r'^relaydomains/(?P<rdom_id>\d+)/edit/$', 'edit'),
(r'^relaydomains/(?P<rdom_id>\d+)/delete/$', 'delete'),
(r'^relaydomains/scan_services/$', 'scan_for_services'),
)
@@ -0,0 +1,87 @@
from django.shortcuts import render
from django.db import transaction
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import (
login_required, permission_required, user_passes_test
)
from modoboa.lib import events
from modoboa.lib.exceptions import ModoboaException, PermDeniedException
from modoboa.lib.webutils import render_to_json_response
from .models import RelayDomain, Service
from .forms import RelayDomainForm
@login_required
@permission_required("admin.add_relaydomain")
@transaction.commit_on_success
def create(request, tplname="postfix_relay_domains/relaydomain_form.html"):
commonctx = {"title": _("New relay domain"),
"action_label": _("Create"),
"action_classes": "submit",
"action": reverse(create),
"formid": "rdomform"}
if request.method == 'POST':
form = RelayDomainForm(request.POST)
if form.is_valid():
rdom = form.save()
rdom.post_create(request.user)
return render_to_json_response(
{"respmsg": _("Relay domain created")}
)
return render_to_json_response(form.errors, status=400)
commonctx['form'] = RelayDomainForm()
return render(request, tplname, commonctx)
@login_required
@permission_required("admin.change_relaydomain")
def edit(request, rdom_id, tplname='postfix_relay_domains/relaydomain_form.html'):
rdom = RelayDomain.objects.get(pk=rdom_id)
if not request.user.can_access(rdom):
raise PermDeniedException
if request.method == 'POST':
form = RelayDomainForm(request.POST, instance=rdom)
if form.is_valid():
form.save()
events.raiseEvent('RelayDomainModified', rdom)
return render_to_json_response(
{'respmsg': _('Relay domain modified')}
)
return render_to_json_response(form.errors, status=400)
ctx = {
'action': reverse(edit, args=[rdom.id]),
'formid': 'rdomform',
'title': rdom.name,
'action_label': _("Update"),
'action_classes': "submit",
'form': RelayDomainForm(instance=rdom)
}
return render(request, tplname, ctx)
@login_required
@permission_required("admin.delete_relaydomain")
def delete(request, rdom_id):
rdom = RelayDomain.objects.get(pk=rdom_id)
if not request.user.can_access(rdom):
raise PermDeniedException
rdom.delete()
return render_to_json_response({
'status': 'ok', 'respmsg': _('Relay domain deleted')
})
@login_required
@user_passes_test(lambda u: u.is_superuser)
@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())
)
+2 -2
View File
@@ -132,7 +132,7 @@ def raiseEvent(event, *args, **kwargs):
return 1
def raiseQueryEvent(event, *args):
def raiseQueryEvent(event, *args, **kwargs):
"""Raise a specific event and wait for answers from callbacks
Any additional keyword argument will be passed to registered
@@ -144,7 +144,7 @@ def raiseQueryEvent(event, *args):
if not event in events or not event in callbacks.keys():
return result
for callback in callbacks[event].values():
result += callback(*args)
result += callback(*args, **kwargs)
return result
+18
View File
@@ -4,6 +4,7 @@ from django import template
from django.template import Template, Context
from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy
from modoboa.lib import events
from modoboa.lib.formutils import SeparatorField
register = template.Library()
@@ -146,3 +147,20 @@ def render_tags(tags):
{% endfor %}
""")
return t.render(Context({"tags": tags}))
@register.simple_tag
def extra_static_content(caller, user):
"""Get extra static content from extensions.
:param str caller: the application (location) responsible for the call
:param ``User`` user: connected user
"""
tpl = template.Template(
"{% for sc in static_content %}{{ sc|safe }}{% endfor %}"
)
return tpl.render(
template.Context({
'static_content': events.raiseQueryEvent("GetStaticContent", caller, user)
})
)
+11
View File
@@ -100,6 +100,17 @@ def ajax_simple_response(content, **response_kwargs):
return HttpResponse(simplejson.dumps(content), **response_kwargs)
def render_to_json_response(context, **response_kwargs):
"""Simple shortcut to render a JSON response.
:param dict context: response content
:return: ``HttpResponse`` object
"""
data = simplejson.dumps(context)
response_kwargs['content_type'] = 'application/json'
return HttpResponse(data, **response_kwargs)
def static_url(path):
"""Returns the correct static url for a given file
+75 -2
View File
@@ -80,11 +80,20 @@ function modalbox_autowidth(e) {
/*
* Simple shorcut do create a bootstrap alert box (error mode)
*/
function build_error_alert(msg) {
return $('<div class="alert alert-error"> \
function build_alert_box(msg, level) {
return $('<div class="alert alert-' + level + '"> \
<a class="close" data-dismiss="alert" href="#">&times;</a>' + msg + "</div>");
}
function build_error_alert(msg) {
return build_alert_box(msg, 'error');
}
function build_success_alert(msg) {
return build_alert_box(msg, 'success');
}
/*
* '.keys()' method support for old browsers :p
*/
@@ -149,6 +158,70 @@ function simple_ajax_form_post(e, options) {
});
}
/*
* Replacement
*/
function simple_ajax_form_post2(e, options) {
e.preventDefault();
var $form = (options.formid != undefined) ? $("#" + options.formid) : $("form");
var defaults = {reload_on_success: true, reload_mode: 'full', modal: true};
var opts = $.extend({}, defaults, options);
var args = $form.serialize();
if (options.extradata != undefined) {
args += "&" + options.extradata;
}
$.ajax({
type: "POST",
url: $form.attr("action"),
data: args
}).done(function(data) {
$("#modalbox").modal('hide');
if (opts.success_cb != undefined) {
opts.success_cb(data);
return;
}
if (opts.reload_on_success) {
if (opts.reload_mode == 'full') {
window.location.reload();
} else {
history.update(true);
}
}
if (data.respmsg) {
$("body").notify('success', data.respmsg, 2000);
}
}).fail(function(jqxhr) {
if (jqxhr.status != 400) {
return;
}
var result = $.parseJSON(jqxhr.responseText);
$.each(result, function(id, value) {
var fullid = "id_" + id;
var $widget = $("#" + fullid);
var spanid = fullid + "-error";
var $span = $("#" + spanid);
if (!$widget.parents(".control-group").hasClass("error")) {
$widget.parents(".control-group").addClass("error");
}
if (!$span.length) {
$span = $("<span />", {
"class": "help-inline",
"html": value[0],
"id": spanid
});
$widget.parents(".controls").append($span);
} else {
$span.html(value[0]);
}
});
if (opts.error_cb) {
opts.error_cb(result);
}
});
}
/*
* The following code prevents a bug under IE7 because fullpath is
* returned instead of a relative one. (even if mootools uses
@@ -8,5 +8,6 @@
<input type="checkbox" class="activator" id="id_{{ field.name }}_act" name="{{ field.name }}_act" {% if activator_value %}checked='checked'{% else %}{% if deactivate_if_empty and field.value == None %}checked='checked'{% endif %}{% endif %}/>{{ activator_label }}
</label>{% endif %}
{% if field.errors %}<p class="help-block">{% for error in field.errors %}{{ error|safe }}{% endfor %}</p>{% endif %}
{% block extra_controls %}{% endblock %}
</div>
</div>
+1 -1
View File
@@ -23,7 +23,7 @@
<script type="text/javascript">
set_static_url("{{ STATIC_URL }}");
</script>
{% extra_head_content user %}
{% extra_static_content "top" user %}
{% block html_head %}{% endblock %}
</head>