From 75ecddf8791b1eb2722538530d9cb015e225f9a4 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 18 Oct 2013 16:09:29 +0200 Subject: [PATCH 01/46] Initial support of relay domains. see #491 --- modoboa/core/templatetags/core_tags.py | 12 -- modoboa/demo/__init__.py | 9 +- modoboa/extensions/admin/__init__.py | 5 + modoboa/extensions/admin/lib.py | 30 +++- .../admin/migrations/0037_rename_tables.py | 7 + modoboa/extensions/admin/models/__init__.py | 4 +- modoboa/extensions/admin/models/domain.py | 4 + .../extensions/admin/static/admin/js/admin.js | 125 +++++++++++------ .../admin/templates/admin/domains.html | 1 + .../admin/templates/admin/domains_table.html | 21 +++ .../admin/templates/admin/identities.html | 1 + .../admin/templates/admin/listing.html | 2 +- .../admin/templatetags/admin_tags.py | 92 ++++++++++--- modoboa/extensions/admin/views/domain.py | 31 +++-- modoboa/extensions/amavis/__init__.py | 40 +++--- modoboa/extensions/limits/__init__.py | 8 +- .../postfix_relay_domains/__init__.py | 118 ++++++++++++++++ .../postfix_relay_domains/app_settings.py | 14 ++ .../extensions/postfix_relay_domains/forms.py | 12 ++ .../postfix_relay_domains/models.py | 129 ++++++++++++++++++ .../postfix_relay_domains/js/relay_domains.js | 79 +++++++++++ .../relaydomain_form.html | 7 + .../postfix_relay_domains/service_field.html | 2 + .../extensions/postfix_relay_domains/tests.py | 16 +++ .../extensions/postfix_relay_domains/urls.py | 9 ++ .../extensions/postfix_relay_domains/views.py | 87 ++++++++++++ modoboa/lib/events.py | 4 +- modoboa/lib/templatetags/lib_tags.py | 18 +++ modoboa/lib/webutils.py | 11 ++ modoboa/static/js/global.js | 77 ++++++++++- modoboa/templates/common/generic_field.html | 1 + modoboa/templates/nlayout.html | 2 +- 32 files changed, 864 insertions(+), 114 deletions(-) create mode 100644 modoboa/extensions/admin/templates/admin/domains_table.html create mode 100644 modoboa/extensions/postfix_relay_domains/__init__.py create mode 100644 modoboa/extensions/postfix_relay_domains/app_settings.py create mode 100644 modoboa/extensions/postfix_relay_domains/forms.py create mode 100644 modoboa/extensions/postfix_relay_domains/models.py create mode 100644 modoboa/extensions/postfix_relay_domains/static/postfix_relay_domains/js/relay_domains.js create mode 100644 modoboa/extensions/postfix_relay_domains/templates/postfix_relay_domains/relaydomain_form.html create mode 100644 modoboa/extensions/postfix_relay_domains/templates/postfix_relay_domains/service_field.html create mode 100644 modoboa/extensions/postfix_relay_domains/tests.py create mode 100644 modoboa/extensions/postfix_relay_domains/urls.py create mode 100644 modoboa/extensions/postfix_relay_domains/views.py diff --git a/modoboa/core/templatetags/core_tags.py b/modoboa/core/templatetags/core_tags.py index 91238b489..b424c1f9f 100644 --- a/modoboa/core/templatetags/core_tags.py +++ b/modoboa/core/templatetags/core_tags.py @@ -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) diff --git a/modoboa/demo/__init__.py b/modoboa/demo/__init__.py index 76f5c05b4..ca8931b96 100755 --- a/modoboa/demo/__init__.py +++ b/modoboa/demo/__init__.py @@ -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 """""" +"""] diff --git a/modoboa/extensions/admin/__init__.py b/modoboa/extensions/admin/__init__.py index 79c8fad5d..c567fc059 100644 --- a/modoboa/extensions/admin/__init__.py +++ b/modoboa/extensions/admin/__init__.py @@ -15,6 +15,11 @@ admin_events = [ "CreateDomain", "DomainModified", "DeleteDomain", + "ExtraDomainEntries", + "ExtraDomainMenuEntries", + "ExtraDomainFilters", + "GetDomainActions", + "GetDomainModifyLink", "DomainAliasCreated", "DomainAliasDeleted", diff --git a/modoboa/extensions/admin/lib.py b/modoboa/extensions/admin/lib.py index 758371f13..bf142a297 100644 --- a/modoboa/extensions/admin/lib.py +++ b/modoboa/extensions/admin/lib.py @@ -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) diff --git a/modoboa/extensions/admin/migrations/0037_rename_tables.py b/modoboa/extensions/admin/migrations/0037_rename_tables.py index 077918c22..6b08aabd7 100644 --- a/modoboa/extensions/admin/migrations/0037_rename_tables.py +++ b/modoboa/extensions/admin/migrations/0037_rename_tables.py @@ -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'}), diff --git a/modoboa/extensions/admin/models/__init__.py b/modoboa/extensions/admin/models/__init__.py index 8edb89898..2a34185d3 100644 --- a/modoboa/extensions/admin/models/__init__.py +++ b/modoboa/extensions/admin/models/__init__.py @@ -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' ] diff --git a/modoboa/extensions/admin/models/domain.py b/modoboa/extensions/admin/models/domain.py index c4d7d5a36..d497d8260 100644 --- a/modoboa/extensions/admin/models/domain.py +++ b/modoboa/extensions/admin/models/domain.py @@ -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 diff --git a/modoboa/extensions/admin/static/admin/js/admin.js b/modoboa/extensions/admin/static/admin/js/admin.js index 0e7c225be..f811d322b 100644 --- a/modoboa/extensions/admin/static/admin/js/admin.js +++ b/modoboa/extensions/admin/static/admin/js/admin.js @@ -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 = $("", {"name": type, "class" : "btn btn-mini", "html": text}); + var $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 = $("", {"name": type, "class" : "btn btn-mini", "html": text}); - var $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() { diff --git a/modoboa/extensions/admin/templates/admin/domains.html b/modoboa/extensions/admin/templates/admin/domains.html index 65163e602..d32690232 100644 --- a/modoboa/extensions/admin/templates/admin/domains.html +++ b/modoboa/extensions/admin/templates/admin/domains.html @@ -7,5 +7,6 @@ $(document).ready(function() { admin = new Domains(); }); +{% extra_static_content "domains" user %} {% endblock %} {% block actions %}{% domains_menu "domains" user %}{% endblock %} diff --git a/modoboa/extensions/admin/templates/admin/domains_table.html b/modoboa/extensions/admin/templates/admin/domains_table.html new file mode 100644 index 000000000..b3f16ba9d --- /dev/null +++ b/modoboa/extensions/admin/templates/admin/domains_table.html @@ -0,0 +1,21 @@ +{% load i18n admin_tags lib_tags %} + + + + + + + + + + + {% for domain in domains %} + + + + + + + {% endfor %} + +
{% trans "Name" %}{% trans "Alias(es)" %}{% trans "Tags" %}{% trans "Actions" %}
{% domain_modify_link domain %}{% domain_aliases domain %} {% render_tags domain.tags %}{% domain_actions user domain %}
diff --git a/modoboa/extensions/admin/templates/admin/identities.html b/modoboa/extensions/admin/templates/admin/identities.html index 82e172a6d..632a6f31c 100644 --- a/modoboa/extensions/admin/templates/admin/identities.html +++ b/modoboa/extensions/admin/templates/admin/identities.html @@ -20,5 +20,6 @@ $(document).ready(function() { admin = new Identities(); }); +{% extra_static_content "identities" user %} {% endblock %} {% block actions %}{% identities_menu user %}{% endblock %} diff --git a/modoboa/extensions/admin/templates/admin/listing.html b/modoboa/extensions/admin/templates/admin/listing.html index 513512d17..cbcbbbc86 100644 --- a/modoboa/extensions/admin/templates/admin/listing.html +++ b/modoboa/extensions/admin/templates/admin/listing.html @@ -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 }} diff --git a/modoboa/extensions/admin/templatetags/admin_tags.py b/modoboa/extensions/admin/templatetags/admin_tags.py index 6736fbbe9..e1fee75c9 100644 --- a/modoboa/extensions/admin/templatetags/admin_tags.py +++ b/modoboa/extensions/admin/templatetags/admin_tags.py @@ -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
" % 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( diff --git a/modoboa/extensions/admin/views/domain.py b/modoboa/extensions/admin/views/domain.py index 91f2c1669..0b9d83ab8 100644 --- a/modoboa/extensions/admin/views/domain.py +++ b/modoboa/extensions/admin/views/domain.py @@ -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", diff --git a/modoboa/extensions/amavis/__init__.py b/modoboa/extensions/amavis/__init__.py index 9abc912ed..a376a9a34 100644 --- a/modoboa/extensions/amavis/__init__.py +++ b/modoboa/extensions/amavis/__init__.py @@ -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("""""") + +""") + 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(""" +""") + + return [tpl.render(Context({}))] + return [] @events.observe("TopNotifications") diff --git a/modoboa/extensions/limits/__init__.py b/modoboa/extensions/limits/__init__.py index 2a22f37e8..5c991598f 100644 --- a/modoboa/extensions/limits/__init__.py +++ b/modoboa/extensions/limits/__init__.py @@ -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 """ - -"""] diff --git a/modoboa/extensions/limits/general_callbacks.py b/modoboa/extensions/limits/general_callbacks.py new file mode 100644 index 000000000..0d41c2449 --- /dev/null +++ b/modoboa/extensions/limits/general_callbacks.py @@ -0,0 +1,97 @@ +from django.utils.translation import ugettext as _ +from modoboa.lib import events +from modoboa.extensions.limits.models import LimitTemplates +from modoboa.extensions.limits.forms import ResourcePoolForm + + +@events.observe("GetExtraRoles") +def get_extra_roles(user): + if user.is_superuser: + return [("Resellers", _("Reseller")), ] + return [] + + +@events.observe("ExtraAdminContent") +def display_pool_usage(user, target, currentpage): + from django.template.loader import render_to_string + + if target != "leftcol" or user.is_superuser: + return [] + if currentpage == "identities": + names = ["mailboxes_limit", "mailbox_aliases_limit"] + if user.has_perm("admin.add_domain"): + names += ["domain_admins_limit"] + else: + names = [ + tpl[0] for tpl in LimitTemplates().templates + if tpl[0] not in ["domain_admins_limit", "mailboxes_limit", + "mailbox_aliases_limit"] + and (len(tpl) == 3 or tpl[3] == user.group) + ] + + limits = user.limitspool.limit_set.filter(name__in=names, maxvalue__gt=0) + if len(limits) == 0: + return [] + return [render_to_string("limits/poolusage.html", dict(limits=limits))] + + +@events.observe("ExtraAccountForm") +def extra_account_form(user, account=None): + if not user.group in ["SuperAdmins", "Resellers"]: + return [] + if account is not None and \ + not account.group in ["Resellers", "DomainAdmins"]: + return [] + + return [ + dict( + id="resources", title=_("Resources"), cls=ResourcePoolForm + ) + ] + + +@events.observe("CheckExtraAccountForm") +def check_form_access(account, form): + if form["id"] != "resources": + return [True] + if not account.belongs_to_group("Resellers") and \ + not account.belongs_to_group("DomainAdmins"): + return [False] + return [True] + + +@events.observe("FillAccountInstances") +def fill_account_instances(user, account, instances): + if not user.is_superuser and not user.belongs_to_group("Resellers"): + return + if not account.belongs_to_group("Resellers") and \ + not account.belongs_to_group("DomainAdmins"): + return + instances["resources"] = account + + +@events.observe("GetStaticContent") +def get_static_content(caller, user): + if caller not in ['domains', 'identities']: + return [] + if user.group == "SimpleUsers": + return [] + return [""" + +"""] diff --git a/modoboa/extensions/postfix_autoreply/__init__.py b/modoboa/extensions/postfix_autoreply/__init__.py index fdbebf511..c28dd67e0 100644 --- a/modoboa/extensions/postfix_autoreply/__init__.py +++ b/modoboa/extensions/postfix_autoreply/__init__.py @@ -6,13 +6,20 @@ This module provides a way to integrate Modoboa auto-reply functionality into Postfix. """ +import sys from django.utils.translation import ugettext_lazy from modoboa.lib import events, parameters from modoboa.core.extensions import ModoExtension, exts_pool -from .models import Transport, Alias +from modoboa.extensions.postfix_autoreply.models import ( + Transport, Alias +) class PostfixAutoreply(ModoExtension): + """ + Auto-reply (vacation) functionality using Postfix. + + """ name = "postfix_autoreply" label = "Postfix autoreply" version = "1.0" @@ -38,14 +45,12 @@ class PostfixAutoreply(ModoExtension): def load(self): from modoboa.extensions.postfix_autoreply.app_settings import ParametersForm parameters.register(ParametersForm, ugettext_lazy("Automatic replies")) + from modoboa.extensions.postfix_autoreply import general_callbacks + if 'modoboa.extensions.postfix_autoreply.general_callbacks' in sys.modules: + reload(general_callbacks) def destroy(self): - events.unregister("DomainCreated", onDomainCreated) - events.unregister("DomainDeleted", onDomainDeleted) - events.unregister("MailboxCreated", onMailboxCreated) - events.unregister("MailboxDeleted", onMailboxDeleted) - events.unregister("MailboxModified", onModifyMailbox) - events.unregister("UserMenuDisplay", menu) + events.unregister_extension() parameters.unregister() exts_pool.register_extension(PostfixAutoreply) @@ -55,87 +60,3 @@ exts_pool.register_extension(PostfixAutoreply) def extra_routes(): return [(r'^user/autoreply/$', 'modoboa.extensions.postfix_autoreply.views.autoreply'), ] - - -@events.observe("ExtraUprefsJS") -def extra_js(user): - return ["""function autoreply_cb() { - $('#id_untildate').datepicker({format: 'yyyy-mm-dd', language: '%s'}); -} -""" % parameters.get_user(user, "LANG", app="core") - ] - - -@events.observe("UserMenuDisplay") -def menu(target, user): - if target != "uprefs_menu": - return [] - if not user.mailbox_set.count(): - return [] - return [ - {"name": "autoreply", - "class": "ajaxlink", - "url": "autoreply/", - "label": ugettext_lazy("Auto-reply message")} - ] - - -@events.observe("DomainCreated") -def onDomainCreated(user, domain): - transport = Transport() - transport.domain = "autoreply.%s" % domain.name - transport.method = "autoreply:" - transport.save() - - -@events.observe("DomainModified") -def onDomainModified(domain): - if domain.oldname == domain.name: - return - Transport.objects.filter(domain="autoreply.%s" % domain.oldname) \ - .update(domain="autoreply.%s" % domain.name) - for al in Alias.objects.filter(full_address__contains="@%s" % domain.oldname): - new_address = al.full_address.replace("@%s" % domain.oldname, "@%s" % domain.name) - al.full_address = new_address - al.autoreply_address = "%s@autoreply.%s" % (new_address, domain.name) - al.save() - - -@events.observe("DomainDeleted") -def onDomainDeleted(domain): - Transport.objects.filter(domain="autoreply.%s" % domain.name).delete() - - -@events.observe("MailboxCreated") -def onMailboxCreated(user, mailbox): - alias = Alias() - alias.full_address = mailbox.full_address - alias.autoreply_address = \ - "%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name) - alias.save() - - -@events.observe("MailboxDeleted") -def onMailboxDeleted(mailboxes): - from modoboa.extensions.admin.models import Mailbox - - if isinstance(mailboxes, Mailbox): - mailboxes = [mailboxes] - for mailbox in mailboxes: - try: - alias = Alias.objects.get(full_address=mailbox.full_address) - except Alias.DoesNotExist: - pass - else: - alias.delete() - - -@events.observe("MailboxModified") -def onModifyMailbox(mailbox, oldmailbox): - if oldmailbox.full_address == mailbox.full_address: - return - alias = Alias.objects.get(full_address=oldmailbox.full_address) - alias.full_address = mailbox.full_address - alias.autoreply_address = \ - "%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name) - alias.save() diff --git a/modoboa/extensions/postfix_autoreply/general_callbacks.py b/modoboa/extensions/postfix_autoreply/general_callbacks.py new file mode 100644 index 000000000..c28de5e9c --- /dev/null +++ b/modoboa/extensions/postfix_autoreply/general_callbacks.py @@ -0,0 +1,87 @@ +from django.utils.translation import ugettext_lazy +from modoboa.lib import events, parameters +from modoboa.extensions.postfix_autoreply.models import Transport, Alias + + +@events.observe("ExtraUprefsJS") +def extra_js(user): + return ["""function autoreply_cb() { + $('#id_untildate').datepicker({format: 'yyyy-mm-dd', language: '%s'}); +} +""" % parameters.get_user(user, "LANG", app="core") + ] + + +@events.observe("UserMenuDisplay") +def menu(target, user): + if target != "uprefs_menu": + return [] + if not user.mailbox_set.count(): + return [] + return [ + {"name": "autoreply", + "class": "ajaxlink", + "url": "autoreply/", + "label": ugettext_lazy("Auto-reply message")} + ] + + +@events.observe("DomainCreated") +def onDomainCreated(user, domain): + transport = Transport() + transport.domain = "autoreply.%s" % domain.name + transport.method = "autoreply:" + transport.save() + + +@events.observe("DomainModified") +def onDomainModified(domain): + if domain.oldname == domain.name: + return + Transport.objects.filter(domain="autoreply.%s" % domain.oldname) \ + .update(domain="autoreply.%s" % domain.name) + for al in Alias.objects.filter(full_address__contains="@%s" % domain.oldname): + new_address = al.full_address.replace("@%s" % domain.oldname, "@%s" % domain.name) + al.full_address = new_address + al.autoreply_address = "%s@autoreply.%s" % (new_address, domain.name) + al.save() + + +@events.observe("DomainDeleted") +def onDomainDeleted(domain): + Transport.objects.filter(domain="autoreply.%s" % domain.name).delete() + + +@events.observe("MailboxCreated") +def onMailboxCreated(user, mailbox): + alias = Alias() + alias.full_address = mailbox.full_address + alias.autoreply_address = \ + "%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name) + alias.save() + + +@events.observe("MailboxDeleted") +def onMailboxDeleted(mailboxes): + from modoboa.extensions.admin.models import Mailbox + + if isinstance(mailboxes, Mailbox): + mailboxes = [mailboxes] + for mailbox in mailboxes: + try: + alias = Alias.objects.get(full_address=mailbox.full_address) + except Alias.DoesNotExist: + pass + else: + alias.delete() + + +@events.observe("MailboxModified") +def onModifyMailbox(mailbox, oldmailbox): + if oldmailbox.full_address == mailbox.full_address: + return + alias = Alias.objects.get(full_address=oldmailbox.full_address) + alias.full_address = mailbox.full_address + alias.autoreply_address = \ + "%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name) + alias.save() diff --git a/modoboa/extensions/postfix_relay_domains/__init__.py b/modoboa/extensions/postfix_relay_domains/__init__.py index 10aba023a..dfe3fff0d 100644 --- a/modoboa/extensions/postfix_relay_domains/__init__.py +++ b/modoboa/extensions/postfix_relay_domains/__init__.py @@ -1,4 +1,5 @@ # coding: utf-8 +import sys from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext_lazy @@ -88,10 +89,16 @@ class PostfixRelayDomains(ModoExtension): ) events.declare(extension_events) from modoboa.extensions.postfix_relay_domains import general_callbacks + if 'modoboa.extensions.postfix_relay_domains.general_callbacks' in sys.modules: + reload(general_callbacks) if exts_pool.is_extension_enabled('limits'): import limits_callbacks + if 'modoboa.extensions.postfix_relay_domains.limits_callbacks' in sys.modules: + reload(limits_callbacks) if exts_pool.is_extension_enabled('amavis'): import amavis_callbacks + if 'modoboa.extensions.postfix_relay_domains.amavis_callbacks' in sys.modules: + reload(amavis_callbacks) def destroy(self): events.unregister_extension() diff --git a/modoboa/extensions/sievefilters/__init__.py b/modoboa/extensions/sievefilters/__init__.py index c5b88c95f..76cade1f7 100644 --- a/modoboa/extensions/sievefilters/__init__.py +++ b/modoboa/extensions/sievefilters/__init__.py @@ -1,6 +1,6 @@ # coding: utf-8 -from django.utils.translation import ugettext as _, ugettext_lazy -from django.core.urlresolvers import reverse +import sys +from django.utils.translation import ugettext_lazy from modoboa.lib import events, parameters from modoboa.core.extensions import ModoExtension, exts_pool @@ -16,38 +16,12 @@ class SieveFilters(ModoExtension): from .app_settings import ParametersForm, UserSettings parameters.register(ParametersForm, ugettext_lazy("Sieve filters")) parameters.register(UserSettings, ugettext_lazy("Message filters")) + from modoboa.extensions.sievefilters import general_callbacks + if 'modoboa.extensions.sievefilters.general_callbacks' in sys.modules: + reload(general_callbacks) def destroy(self): - events.unregister("UserMenuDisplay", menu) + events.unregister_extension() parameters.unregister() exts_pool.register_extension(SieveFilters) - - -@events.observe("UserMenuDisplay") -def menu(target, user): - if target != "options_menu": - return [] - if not user.mailbox_set.count(): - return [] - return [ - {"name": "sievefilters", - "label": _("Message filters"), - "url": reverse("modoboa.extensions.sievefilters.views.index"), - "img": "icon-check"} - ] - - -@events.observe("UserLogout") -def userlogout(request): - from .lib import SieveClient - - if not request.user.mailbox_set.count(): - return - try: - sc = SieveClient(user=request.user.username, - password=request.session["password"]) - except Exception: - pass - else: - sc.logout() diff --git a/modoboa/extensions/sievefilters/general_callbacks.py b/modoboa/extensions/sievefilters/general_callbacks.py new file mode 100644 index 000000000..0ce7a381b --- /dev/null +++ b/modoboa/extensions/sievefilters/general_callbacks.py @@ -0,0 +1,32 @@ +from django.utils.translation import ugettext as _ +from django.core.urlresolvers import reverse +from modoboa.lib import events + + +@events.observe("UserMenuDisplay") +def menu(target, user): + if target != "options_menu": + return [] + if not user.mailbox_set.count(): + return [] + return [ + {"name": "sievefilters", + "label": _("Message filters"), + "url": reverse("modoboa.extensions.sievefilters.views.index"), + "img": "icon-check"} + ] + + +@events.observe("UserLogout") +def userlogout(request): + from .lib import SieveClient + + if not request.user.mailbox_set.count(): + return + try: + sc = SieveClient(user=request.user.username, + password=request.session["password"]) + except Exception: + pass + else: + sc.logout() diff --git a/modoboa/extensions/stats/__init__.py b/modoboa/extensions/stats/__init__.py index 7197cc01c..e08b12b55 100644 --- a/modoboa/extensions/stats/__init__.py +++ b/modoboa/extensions/stats/__init__.py @@ -7,8 +7,8 @@ This module provides support to retrieve statistics from postfix log : sent, received, bounced, rejected """ -from django.core.urlresolvers import reverse -from django.utils.translation import ugettext as _, ugettext_lazy +import sys +from django.utils.translation import ugettext_lazy from modoboa.lib import events, parameters from modoboa.core.extensions import ModoExtension, exts_pool @@ -17,7 +17,9 @@ class Stats(ModoExtension): name = "stats" label = "Statistics" version = "1.0" - description = ugettext_lazy("Graphical statistics about emails traffic using RRDtool") + description = ugettext_lazy( + "Graphical statistics about emails traffic using RRDtool" + ) needs_media = True def load(self): @@ -26,27 +28,12 @@ class Stats(ModoExtension): parameters.register( ParametersForm, ugettext_lazy("Graphical statistics") ) + from modoboa.extensions.stats import general_callbacks + if 'modoboa.extensions.stats.general_callbacks' in sys.modules: + reload(general_callbacks) def destroy(self): - events.unregister("AdminMenuDisplay", menu) + events.unregister_extension() + parameters.unregister() exts_pool.register_extension(Stats) - - -@events.observe("UserMenuDisplay") -def menu(target, user): - if target != "top_menu" or user.group == "SimpleUsers": - return [] - return [ - {"name": "stats", - "label": _("Statistics"), - "url": reverse('fullindex')} - ] - - -@events.observe("GetGraphSets") -def get_default_graph_sets(): - from modoboa.extensions.stats.graph_templates import MailTraffic - - gset = MailTraffic() - return {gset.html_id: gset} diff --git a/modoboa/extensions/stats/general_callbacks.py b/modoboa/extensions/stats/general_callbacks.py new file mode 100644 index 000000000..ced60145c --- /dev/null +++ b/modoboa/extensions/stats/general_callbacks.py @@ -0,0 +1,22 @@ +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ +from modoboa.lib import events + + +@events.observe("UserMenuDisplay") +def menu(target, user): + if target != "top_menu" or user.group == "SimpleUsers": + return [] + return [ + {"name": "stats", + "label": _("Statistics"), + "url": reverse('fullindex')} + ] + + +@events.observe("GetGraphSets") +def get_default_graph_sets(): + from modoboa.extensions.stats.graph_templates import MailTraffic + + gset = MailTraffic() + return {gset.html_id: gset} diff --git a/modoboa/extensions/webmail/__init__.py b/modoboa/extensions/webmail/__init__.py index 8b336b8b1..597f85375 100644 --- a/modoboa/extensions/webmail/__init__.py +++ b/modoboa/extensions/webmail/__init__.py @@ -1,4 +1,5 @@ # coding: utf-8 +import sys from django.utils.translation import ugettext as _, ugettext_lazy from django.core.urlresolvers import reverse from modoboa.lib import events, parameters @@ -17,43 +18,12 @@ class Webmail(ModoExtension): parameters.register(ParametersForm, "Webmail") parameters.register(UserSettings, "Webmail") + from modoboa.extensions.webmail import general_callbacks + if 'modoboa.extensions.webmail.general_callbacks' in sys.modules: + reload(general_callbacks) def destroy(self): - events.unregister("UserMenuDisplay", menu) + events.unregister_extension() parameters.unregister() exts_pool.register_extension(Webmail) - - -@events.observe("UserMenuDisplay") -def menu(target, user): - if target != "top_menu": - return [] - if not user.mailbox_set.count(): - return [] - return [ - {"name": "webmail", - "label": _("Webmail"), - "url": reverse("modoboa.extensions.webmail.views.index")}, - ] - - -@events.observe("UserLogout") -def userlogout(request): - from .lib import IMAPconnector - from .exceptions import ImapError - - if not request.user.mailbox_set.count(): - return - try: - m = IMAPconnector(user=request.user.username, - password=request.session["password"]) - except Exception: - # TODO silent exception are bad : we should at least log it - return - - # The following statement may fail under Python 2.6... - try: - m.logout() - except ImapError: - pass diff --git a/modoboa/extensions/webmail/general_callbacks.py b/modoboa/extensions/webmail/general_callbacks.py new file mode 100644 index 000000000..affbcf2bf --- /dev/null +++ b/modoboa/extensions/webmail/general_callbacks.py @@ -0,0 +1,37 @@ +from django.utils.translation import ugettext as _ +from django.core.urlresolvers import reverse +from modoboa.lib import events + + +@events.observe("UserMenuDisplay") +def menu(target, user): + if target != "top_menu": + return [] + if not user.mailbox_set.count(): + return [] + return [ + {"name": "webmail", + "label": _("Webmail"), + "url": reverse("modoboa.extensions.webmail.views.index")}, + ] + + +@events.observe("UserLogout") +def userlogout(request): + from .lib import IMAPconnector + from .exceptions import ImapError + + if not request.user.mailbox_set.count(): + return + try: + m = IMAPconnector(user=request.user.username, + password=request.session["password"]) + except Exception: + # TODO silent exception are bad : we should at least log it + return + + # The following statement may fail under Python 2.6... + try: + m.logout() + except ImapError: + pass From c526a7c90bd6cfab043e1fd5e71b738a511dc28e Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 2 Nov 2013 15:36:18 +0100 Subject: [PATCH 13/46] wip: relay domains. * Documentation update * Few fixes see #491 --- doc/extending/events.rst | 455 ++++++++++++++++-- modoboa/extensions/admin/__init__.py | 1 - modoboa/extensions/admin/models/domain.py | 4 + .../admin/templatetags/admin_tags.py | 12 +- .../postfix_relay_domains/__init__.py | 1 + .../general_callbacks.py | 7 - .../postfix_relay_domains/models.py | 4 + modoboa/lib/events.py | 1 - 8 files changed, 414 insertions(+), 71 deletions(-) diff --git a/doc/extending/events.rst b/doc/extending/events.rst index 083ed7750..69987bd59 100644 --- a/doc/extending/events.rst +++ b/doc/extending/events.rst @@ -11,10 +11,10 @@ Introduction Modoboa provides a simple API to interact with events. It understands two kinds of events: -* Those that return a value -* Those that return nothing +* Those returning a value +* Those returning nothing -Listening to a specific event is achieved as follow:: +Listening to a specific event is achieved as follows:: from modoboa.lib import events @@ -41,12 +41,32 @@ To stop listening to as specific event, you must use the The parameters are the same than those used with ``register``. +To unregister all events declared by a specific extension, use the +``unregister_extension`` function:: + + events.unregister_extension([name]) + +``name`` is the extension's name but it is optional. Leave it empty to +let the function guess the name. + Read further to get a complete list and description of all available events. **************** Supported events **************** +AccountAutoCreated +================== + +Raised when a new account is automatically created (example: LDAP +synchronization). + +*Callback prototype*:: + + def callback(account): pass + +* ``account`` is the newly created account (``User`` instance) + AccountCreated ============== @@ -69,6 +89,32 @@ Raised when an existing account is deleted. * ``account`` is the account that is going to be deleted * ``byuser`` is the adminstrator deleting ``account`` + +AccountExported +=============== + +Raised when an account is exported to CSV. + +*Callback prototype*:: + + def callback(account): pass + +* ``account`` is the account being exported + +Must return a list of values to include in the export. + +AccountImported +=============== + +Raised when an account is imported from CSV. + +*Callback prototype*:: + + def callback(user, account, row): pass + +* ``user`` is the user importing the account +* ``account`` is the account being imported +* ``row`` is a list containing what remains from the CSV definition AccountModified =============== @@ -127,6 +173,21 @@ Raised just before a user tries to create a new object. Return ``True`` or ``False`` to indicate if this user can respectively create or not create a new ``Domain`` object. +CheckDomainName +=============== + +Raised before the unicity of a domain name is checked. By default, +modoboa prevents duplicate names between domains and domain aliases +but extensions have the possibility to extend this rule using this +event. + +*Callback prototype*:: + + def callback(): pass + +Must return a list of 2uple, each one containing a model class and an +associated label. + CheckExtraAccountForm ===================== @@ -144,52 +205,6 @@ this account is concerned by a specific form. Callbacks listening to this event must return a list containing one Boolean. -DomainCreated -============= - -Raised when a new domain is created. - -*Callback prototype*:: - - def callback(user, domain): pass - -* ``user`` corresponds to the ``User`` object creating the domain (its owner) -* ``domain`` is a ``Domain`` instance - -MailboxCreated -============== - -Raised when a new mailbox is created. - -*Callback prototype*:: - - def callback(user, mailbox): pass - -* ``user`` is the new mailbox's owner (``User`` instance) -* ``mailbox`` is the new mailbox (``Mailbox`` instance) - -DomainDeleted -============= - -Raised when an existing domain is about to be deleted. - -*Callback prototype*:: - - def callback(domain): pass - -* ``domain`` is a ``Domain`` instance - -MailboxDeleted -============== - -Raised when an existing mailbox is about to be deleted. - -*Callback prototype*:: - - def callback(mailbox): pass - -* ``mailbox`` is a ``Mailbox`` instance - DomainAliasCreated ================== @@ -213,6 +228,29 @@ Raised when an existing domain alias is about to be deleted. * ``domain_alias`` is a ``DomainAlias`` instance +DomainCreated +============= + +Raised when a new domain is created. + +*Callback prototype*:: + + def callback(user, domain): pass + +* ``user`` corresponds to the ``User`` object creating the domain (its owner) +* ``domain`` is a ``Domain`` instance + +DomainDeleted +============= + +Raised when an existing domain is about to be deleted. + +*Callback prototype*:: + + def callback(domain): pass + +* ``domain`` is a ``Domain`` instance + DomainModified ============== @@ -223,7 +261,20 @@ Raised when a domain has been modified. def callback(domain): pass * ``domain`` is the modified ``Domain`` instance, it contains an extra - ``oldname`` field which contains the old domain name + ``oldname`` field which contains the old name + +DomainOwnershipRemoved +====================== + +Raised before the ownership of a domain is removed from its original +creator. + +*Callback prototype*:: + + def callback(owner, domain): pass + +* ``owner`` is the original creator +* ``domain`` is the ``Domain`` instance being modified ExtDisabled =========== @@ -289,6 +340,37 @@ Let extensions add extra content into the admin panel. Callbacks listening to this event must return a list of string. +ExtraDomainEntries +================== + +Raised to request extra entries to display inside the *domains* +listing. + +*Callback prototype*:: + + def callback(user, domfilter, searchquery, **extrafilters): pass + +* ``user`` is the ``User`` instance corresponding to the currently + logged in user +* ``domfilter`` is a string indicating which domain type the user needs +* ``searchquery`` is a string containing a search query +* ``extrafilters`` is a set of keyword arguments that may contain additional filters + +Must return a valid ``QuerySet``. + +ExtraDomainFilters +================== + +Raised to request extra filters for the *domains* listing page. For +example, the *postfix_relay_domains* extension let users filter +entries based on service types. + +*Callback prototype*:: + + def callback(): pass + +Must return a list of valid filter names (string). + ExtraDomainForm =============== @@ -304,6 +386,59 @@ tabs). * ``domain`` is the domain beeing modified (``Domain`` instance) +Callbacks listening to the event must return a list of dictionnaries, +each one must contain at least three keys:: + + {"id" : "", + "title" : "", + "cls" : TheFormClassName} + +ExtraDomainImportHelp +===================== + +Raised to request extra help text to display inside the domain import +form. + +*Callback prototype*:: + + def callback(): pass + +Must return a list a string. + +ExtraDomainMenuEntries +====================== + +Raised to request extra entries to include in the left menu of the +*domains* listing page. + +*Callback prototype*:: + + def callback(user): pass + +* ``user`` is the ``User`` instance corresponding to the currently + logged in user + +Must return a list of dictionaries. Each dictionary must contain at +least three keys:: + + {"name": "
", + "label": "", + "url": ""} + +ExtraRelayDomainForm +==================== + +Let extensions add new forms to the relay domain edition form (the one +with tabs). + +*Callback prototype*:: + + def callback(user, rdomain): pass + +* ``user`` is the ``User`` instance corresponding to the currently + logged in user +* ``rdomain`` is the relay domain being modified (``RelayDomain`` instance) + Callbacks listening to the event must return a list of dictionnaries, each one must contain at least three keys:: @@ -344,6 +479,24 @@ forms. * ``domain`` is the ``Domain`` instance beeing modified +* ``instances`` is a dictionnary where the callback will add + information needed to fill a specific form + +FillRelayDomainInstances +======================== + +When a relay domain is being modified, this event is raised to fill extra +forms. + +*Callback prototype*:: + + def callback(user, rdomain, instances): pass + +* ``user`` is the ``User`` instance corresponding to the currently + logged in user + +* ``rdomain`` is the ``RelayDomain`` instance being modified + * ``instances`` is a dictionnary where the callback will add information needed to fill a specific form @@ -364,6 +517,73 @@ Some places in the interface let plugins add their own announcement Callbacks listening to this event must return a list of string. +GetDomainActions +================ + +Raised to request the list of actions available for the *domains* +listing entry being displayed. + +*Callback prototype*:: + + def callback(user, rdomain): pass + +* ``user`` is the ``User`` instance corresponding to the currently + logged in user +* ``rdomain`` is the ``RelayDomain`` instance being displayed + +Must return a list of dictionaries, each dictionary containing at +least the following entries:: + + {"name": "", + "url": "", + "title": "", + "img": ""} + +GetDomainModifyLink +=================== + +Raised to request the modification url of the *domains* listing entry +being displayed. + +*Callback prototype*:: + + def callback(domain): pass + +* ``domain`` is a model instance (``RelayDomain`` for example) + +Must return a dictionary containing at least the following entry:: + + {'url': ''} + +GetExtraLimitTemplates +====================== + +Raised to request extra limit templates. For example, the +*postfix_relay_domains* extension define a template to limit the +number of relay domains an administrator can create. + +*Callback prototype*:: + + def callback(): pass + +Must return a list of set. Each set must contain at least three entries:: + + [('', '', '')] + +GetExtraParameters +================== + +Raised to request extra parameters for a given parameters form. + +*Callback prototype*:: + + def callback(application, level): pass + +* ``application`` is the name of the form's application (ie. admin, amavis, etc.) +* ``level`` is the form's level: ``A`` for admin or ``U`` for user + +Must return a dictionary. Each entry must be a valid Django form field. + .. _getextraroles: GetExtraRoles @@ -397,6 +617,27 @@ template but need javascript stuff. Callbacks listening to this event must return a list of string. +ImportObject +============ + +Raised to request the function handling an object being imported from CSV. + +*Callback prototype*:: + + def callback(objtype): pass + +``objtype`` is the type of object being imported + +Must return a list of function. A valid import function must respect +the following prototype:: + + def import_function(user, row, formopts): pass + +* ``user`` is the ``User`` instance corresponding to the currently + logged in user +* ``row`` is a string containing the object's definition (CSV format) +* ``formopts`` is a dictionary that may contain options + MailboxAliasCreated =================== @@ -420,8 +661,31 @@ Raised when an existing mailbox alias is about to be deleted. * ``mailbox_alias`` is an ``Alias`` instance -ModifyMailbox -============= +MailboxCreated +============== + +Raised when a new mailbox is created. + +*Callback prototype*:: + + def callback(user, mailbox): pass + +* ``user`` is the new mailbox's owner (``User`` instance) +* ``mailbox`` is the new mailbox (``Mailbox`` instance) + +MailboxDeleted +============== + +Raised when an existing mailbox is about to be deleted. + +*Callback prototype*:: + + def callback(mailbox): pass + +* ``mailbox`` is a ``Mailbox`` instance + +MailboxModified +=============== Raised when an existing mailbox is modified. @@ -506,3 +770,90 @@ contain at least the following keys:: {"name" : "a_name_without_spaces", "label" : _("The menu label"), "url" : reverse("your_view")} + +RelayDomainAliasCreated +======================= + +Raised when a new relay domain alias is created. + +*Callback prototype*:: + + def callback(user, rdomain_alias): pass + +* ``user`` is the new relay domain alias owner (``User`` instance) +* ``rdomain_alias`` is the new relay domain alias (``DomainAlias`` instance) + +RelayDomainAliasDeleted +======================= + +Raised when an existing relay domain alias is about to be deleted. + +*Callback prototype*:: + + def callback(rdomain_alias): pass + +* ``rdomain_alias`` is a ``RelayDomainAlias`` instance + +RelayDomainCreated +================== + +Raised when a new relay domain is created. + +*Callback prototype*:: + + def callback(user, rdomain): pass + +* ``user`` corresponds to the ``User`` object creating the relay domain (its owner) +* ``rdomain`` is a ``RelayDomain`` instance + +RelayDomainDeleted +================== + +Raised when an existing relay domain is about to be deleted. + +*Callback prototype*:: + + def callback(rdomain): pass + +* ``rdomain`` is a ``RelayDomain`` instance + +RelayDomainModified +=================== + +Raised when a relay domain has been modified. + +*Callback prototype*:: + + def callback(rdomain): pass + +* ``rdomain`` is the modified ``RelayDomain`` instance, it contains an + extra ``oldname`` field which contains the old name + +RoleChanged +=========== + +Raised when the role of an account is about to be changed. + +*Callback prototype*:: + + def callback(account, role): pass + +* ``account`` is the account being modified +* ``role`` is the new role (string) + +UserCanSetRole +============== + +Raised to check if a user is allowed to set a given role to an +account. + +*Callback prototype*:: + + def callback(account, role): pass + +* ``user`` is the ``User`` instance corresponding to the currently + logged in user +* ``role`` is the role ``user`` tries to set + +Must return a list containing ``True`` or ``False`` to indicate if +this user can is allowed to set ``role``. diff --git a/modoboa/extensions/admin/__init__.py b/modoboa/extensions/admin/__init__.py index 0589b630b..8a7629a80 100644 --- a/modoboa/extensions/admin/__init__.py +++ b/modoboa/extensions/admin/__init__.py @@ -19,7 +19,6 @@ admin_events = [ "ExtraDomainFilters", "GetDomainActions", "GetDomainModifyLink", - "GetDomainAliasQuerySet", "CheckDomainName", "DomainAliasCreated", diff --git a/modoboa/extensions/admin/models/domain.py b/modoboa/extensions/admin/models/domain.py index 70166b9b4..95940dda6 100644 --- a/modoboa/extensions/admin/models/domain.py +++ b/modoboa/extensions/admin/models/domain.py @@ -70,6 +70,10 @@ class Domain(AdminObject): """ return [oa.user for oa in self.owners.filter(user__is_superuser=False)] + @property + def aliases(self): + return self.domainalias_set + def add_admin(self, account): """Add a new administrator for this domain diff --git a/modoboa/extensions/admin/templatetags/admin_tags.py b/modoboa/extensions/admin/templatetags/admin_tags.py index 7b9a7c5f7..3f934ddce 100644 --- a/modoboa/extensions/admin/templatetags/admin_tags.py +++ b/modoboa/extensions/admin/templatetags/admin_tags.py @@ -194,18 +194,10 @@ def domain_aliases(domain): :param domain: :rtype: str """ - if domain.__class__.__name__ == "Domain": - qset = domain.domainalias_set - else: - qset = events.raiseQueryEvent('GetDomainAliasQuerySet', domain) - if not qset: - return '---' - qset = qset[0] - - if not qset.count(): + if not domain.aliases.count(): return '---' res = '' - for alias in qset.all(): + for alias in domain.aliases.all(): res += '%s
' % alias.name return res diff --git a/modoboa/extensions/postfix_relay_domains/__init__.py b/modoboa/extensions/postfix_relay_domains/__init__.py index dfe3fff0d..d3451781e 100644 --- a/modoboa/extensions/postfix_relay_domains/__init__.py +++ b/modoboa/extensions/postfix_relay_domains/__init__.py @@ -10,6 +10,7 @@ from .models import RelayDomain, RelayDomainAlias, Service extension_events = [ "RelayDomainCreated", "RelayDomainDeleted", + "RelayDomainModified", "RelayDomainAliasCreated", "RelayDomainAliasDeleted", "ExtraRelayDomainForm", diff --git a/modoboa/extensions/postfix_relay_domains/general_callbacks.py b/modoboa/extensions/postfix_relay_domains/general_callbacks.py index d7fa11bc6..6c9861d36 100644 --- a/modoboa/extensions/postfix_relay_domains/general_callbacks.py +++ b/modoboa/extensions/postfix_relay_domains/general_callbacks.py @@ -71,13 +71,6 @@ def rdomain_modify_link(domain): } -@events.observe('GetDomainAliasQuerySet') -def get_da_query_set(domain): - if domain.__class__.__name__ != 'RelayDomain': - return [] - return [domain.relaydomainalias_set] - - @events.observe('GetDomainActions') def rdomain_actions(user, domain): if not isinstance(domain, RelayDomain): diff --git a/modoboa/extensions/postfix_relay_domains/models.py b/modoboa/extensions/postfix_relay_domains/models.py index 1c45cbffb..360810cb3 100644 --- a/modoboa/extensions/postfix_relay_domains/models.py +++ b/modoboa/extensions/postfix_relay_domains/models.py @@ -105,6 +105,10 @@ class RelayDomain(AdminObject): "type": "srv", "color": "info"} ] + @property + def aliases(self): + return self.relaydomainalias_set + def __str__(self): return self.name diff --git a/modoboa/lib/events.py b/modoboa/lib/events.py index cf565e370..e5baac544 100644 --- a/modoboa/lib/events.py +++ b/modoboa/lib/events.py @@ -125,7 +125,6 @@ def unregister_extension(extension=None): """ if extension is None: extension = guess_extension_name() - print extension for evt, values in callbacks.items(): for name in values.keys(): if extension in name: From e8f59ecf7b51c81177ef874c5bb09ae70fae6d19 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 2 Nov 2013 15:50:12 +0100 Subject: [PATCH 14/46] wip: relay domains. Documentation update. see #491 --- doc/getting_started/plugins.rst | 14 ++++++++++++++ doc/getting_started/upgrade.rst | 3 +++ 2 files changed, 17 insertions(+) diff --git a/doc/getting_started/plugins.rst b/doc/getting_started/plugins.rst index cb47c879f..1bc6c2def 100644 --- a/doc/getting_started/plugins.rst +++ b/doc/getting_started/plugins.rst @@ -32,6 +32,20 @@ associated value to -1. Default limits applied to new administrators can be changed through the *Modoboa > Parameters > Limits* page. +***************************** +Postfix relay domains support +***************************** + +This plugin adds the support for relay domains using postfix. This +feature is useful when the MTA managed by Modoboa is not the final +destination of one or many domains. + +If activated, two new objects will be available from the *domains* +listing page: *relay domain* and *relay domain alias*. + +This extension is compatible with the *amavis* and *limits* +ones. Resellers will be able to create both new objects. + .. _amavis_frontend: ******************** diff --git a/doc/getting_started/upgrade.rst b/doc/getting_started/upgrade.rst index dfde23e49..d5c6e1583 100644 --- a/doc/getting_started/upgrade.rst +++ b/doc/getting_started/upgrade.rst @@ -53,6 +53,9 @@ Finally, follow the common procedure:: 1.1.0: FIXME ============ +#. Add ``'modoboa.extensions.postfix_relay_domains'`` to + ``MODOBOA_APPS`` + Due to code refactoring, some modifications need to be done into :file:`settings.py`: From 14a026e2bf0b4b14189acb767949883eaf953482 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 8 Nov 2013 10:55:22 +0100 Subject: [PATCH 15/46] Properly display sieve filter errors. --- .../templates/sievefilters/index.html | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/modoboa/extensions/sievefilters/templates/sievefilters/index.html b/modoboa/extensions/sievefilters/templates/sievefilters/index.html index 817d6fd51..a8442ffab 100644 --- a/modoboa/extensions/sievefilters/templates/sievefilters/index.html +++ b/modoboa/extensions/sievefilters/templates/sievefilters/index.html @@ -14,6 +14,27 @@ var history = new History({ var cond_templates = null; var action_templates = null; +function display_form_errors(data) { + $.each(data.form_errors, function(id, value) { + var fullid = "id_" + (data.prefix ? data.prefix + "-" : "") + id; + var $widget = $("#" + formid + " #" + fullid); + var ulid = fullid + "-error"; + var $ul = $("#" + ulid); + + if (!$ul.length) { + $ul = $("", { + "class": "errors", + "id": ulid + }).append($("
  • ", { + html: value[0] + }); + $widget.after($ul); + } else { + $ul.find('li').html(value[0]); + } + }); +} + function filterform_cb() { cond_templates = fetch_templates("{% url 'modoboa.extensions.sievefilters.views.get_templates' 'condition' %}", cond_templates); action_templates = fetch_templates("{% url 'modoboa.extensions.sievefilters.views.get_templates' 'action' %}", action_templates); @@ -40,7 +61,8 @@ function filterform_cb() { simple_ajax_form_post(e, { formid: 'filterform', reload_mode: 'history', - extradata: counters + extradata: counters, + error_cb: display_filter_errors }); }); } From d9650a3dca7373aa9a719f3e8357588a02a905f3 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 8 Nov 2013 10:58:29 +0100 Subject: [PATCH 16/46] It seems I pushed the wrong version :p --- .../templates/sievefilters/index.html | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/modoboa/extensions/sievefilters/templates/sievefilters/index.html b/modoboa/extensions/sievefilters/templates/sievefilters/index.html index a8442ffab..63ac35146 100644 --- a/modoboa/extensions/sievefilters/templates/sievefilters/index.html +++ b/modoboa/extensions/sievefilters/templates/sievefilters/index.html @@ -14,24 +14,22 @@ var history = new History({ var cond_templates = null; var action_templates = null; -function display_form_errors(data) { +function display_filter_errors(data) { + $("ul.errors").remove(); $.each(data.form_errors, function(id, value) { - var fullid = "id_" + (data.prefix ? data.prefix + "-" : "") + id; - var $widget = $("#" + formid + " #" + fullid); - var ulid = fullid + "-error"; - var $ul = $("#" + ulid); - - if (!$ul.length) { - $ul = $("", { - "class": "errors", - "id": ulid - }).append($("
  • ", { - html: value[0] - }); - $widget.after($ul); - } else { - $ul.find('li').html(value[0]); + if (id.indexOf('cond') && id.indexOf('action')) { + return; } + var fullid = "id_" + (data.prefix ? data.prefix + "-" : "") + id; + var $widget = $("#" + fullid); + var ulid = fullid + "-error"; + var $ul = $("