From cf84644fabfb98dd8801751dc17ebd8ed7fa44de Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 26 Jul 2014 15:09:02 +0200 Subject: [PATCH 01/11] New feature: check for new versions. fix #551 --- doc/extending/events.rst | 20 +++- modoboa/core/__init__.py | 28 ++++- modoboa/core/app_settings.py | 13 +++ .../core/commands/templates/settings.py.tpl | 14 +++ modoboa/core/context_processors.py | 18 ++++ modoboa/core/templates/core/information.html | 8 ++ modoboa/core/templatetags/core_tags.py | 6 -- modoboa/core/urls.py | 11 +- modoboa/core/utils.py | 32 ++++++ modoboa/core/views/admin.py | 16 ++- modoboa/extensions/amavis/app_settings.py | 8 +- .../extensions/amavis/general_callbacks.py | 61 ++--------- modoboa/extensions/amavis/urls.py | 1 - modoboa/extensions/amavis/views.py | 7 -- modoboa/static/js/top_notifications.js | 101 ++++++++++++++++++ .../templates/common/top_notifications.html | 20 ++++ modoboa/templates/nlayout.html | 11 +- requirements.txt | 1 + 18 files changed, 293 insertions(+), 83 deletions(-) create mode 100644 modoboa/core/context_processors.py create mode 100644 modoboa/core/utils.py create mode 100644 modoboa/static/js/top_notifications.js create mode 100644 modoboa/templates/common/top_notifications.html diff --git a/doc/extending/events.rst b/doc/extending/events.rst index 2741bd421..90bcef511 100644 --- a/doc/extending/events.rst +++ b/doc/extending/events.rst @@ -736,16 +736,30 @@ this user is disabled). TopNotifications ================ -Let extensions add custom content into the top bar. +Lets extensions subscribe to the global notification service (located +inside the top bar). *Callback prototype*:: - def callback(user): pass + def callback(user, include_all): pass * ``user`` is a ``User`` instance corresponding to the currently logged in user +* ``include_all`` is a boolean indicating if empty notifications must + be included into the result or not -Callbacks listening to this event must return a list of string. +Callbacks listening to this event must return a list of dictionary, +each dictionary containing at least the following entries:: + + {"id": "", + "url": "", + "text": ""} + +If your notification needs a counter, you can specify it by adding the +two following entries in the dictionary: + + {"counter": , + "level": ""} UserLogin ========= diff --git a/modoboa/core/__init__.py b/modoboa/core/__init__.py index ec4208b38..bd9ba2318 100644 --- a/modoboa/core/__init__.py +++ b/modoboa/core/__init__.py @@ -1,7 +1,12 @@ -from django.utils.translation import ugettext_lazy +import os + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _, ugettext_lazy + +from modoboa.core.utils import new_version_available from modoboa.lib import parameters, events -base_events = [ +BASE_EVENTS = [ "CanCreate", "AccountCreated", @@ -51,7 +56,7 @@ def load_core_settings(): parameters.register(GeneralParametersForm, ugettext_lazy("General")) parameters.register(UserSettings, ugettext_lazy("General")) - events.declare(base_events) + events.declare(BASE_EVENTS) @events.observe("ExtDisabled") @@ -63,3 +68,20 @@ def unset_default_topredirection(extension): topredirection = parameters.get_admin("DEFAULT_TOP_REDIRECTION") if topredirection == extension.name: parameters.save_admin("DEFAULT_TOP_REDIRECTION", "core") + + +@events.observe("TopNotifications") +def check_for_new_version(user, include_all): + """ + Check if a new version of Modoboa is available. + """ + if not user.is_superuser: + return [] + if new_version_available() is None: + return [{"id": "newversionavailable"}] if include_all else [] + return [{ + "id": "newversionavailable", + "url": reverse("admin_index") + "#info/", + "text": _("New Modoboa version available"), + "level": "info", + }] diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 3b3a77df6..1b01fe34e 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -171,6 +171,19 @@ class GeneralParametersForm(parameters.AdminParametersForm): sep3 = SeparatorField(label=ugettext_lazy("Miscellaneous")) + top_notifications_check_interval = forms.IntegerField( + label=_("Top notifications check interval"), + initial=30, + help_text=_("Interval between two top notification checks") + ) + + check_new_versions = YesNoField( + label=ugettext_lazy("Check for new versions"), + initial="yes", + help_text=ugettext_lazy( + "Automatically checks if newer versions are available") + ) + log_maximum_age = forms.IntegerField( label=ugettext_lazy("Maximum log record age"), initial=365, diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index c0df35122..07e01a342 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -103,6 +103,18 @@ TEMPLATE_LOADERS = ( # 'django.template.loaders.eggs.Loader', ) +TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.contrib.auth.context_processors.auth', + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.core.context_processors.media', + 'django.core.context_processors.static', + 'django.core.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + + 'modoboa.core.context_processors.top_notifications', +) + MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -174,6 +186,8 @@ SOUTH_TESTS_MIGRATE = False #DOVECOT_LOOKUP_PATH = ('/path/to/dovecot', ) +MODOBOA_API_URL = 'https://api.modoboa.org/1/' + AUTHENTICATION_BACKENDS = ( 'modoboa.lib.authbackends.SimpleBackend', ) diff --git a/modoboa/core/context_processors.py b/modoboa/core/context_processors.py new file mode 100644 index 000000000..218d6bf6e --- /dev/null +++ b/modoboa/core/context_processors.py @@ -0,0 +1,18 @@ +""" +Custom context processors. +""" +from modoboa.lib import events, parameters + + +def top_notifications(request): + """ + A context processor to include top notifications. + """ + if request.user.is_anonymous(): + return {} + return { + "notifications_check_interval": + int(parameters.get_admin("TOP_NOTIFICATIONS_CHECK_INTERVAL")) * 1000, + "top_notifications": events.raiseQueryEvent( + "TopNotifications", request.user, False) + } diff --git a/modoboa/core/templates/core/information.html b/modoboa/core/templates/core/information.html index f74c13139..23f6264c2 100644 --- a/modoboa/core/templates/core/information.html +++ b/modoboa/core/templates/core/information.html @@ -1,5 +1,13 @@ {% load i18n core_tags lib_tags %}

{% trans "Information" %} {% trans "Various information about Modoboa" %}


+{% if new_version %} +
+

{% trans "An update is available!" %}

+ {% blocktrans with version=new_version.version changelog_url=new_version.changelog_url %} + Modoboa {{ version }} is available for download. The list of all changes is available here. + {% endblocktrans %} +
+{% endif %}
Modoboa {% get_version %} {% trans "is currently in use." %}

{% trans "Active users" %}

diff --git a/modoboa/core/templatetags/core_tags.py b/modoboa/core/templatetags/core_tags.py index e1b5ebce1..73aac4899 100644 --- a/modoboa/core/templatetags/core_tags.py +++ b/modoboa/core/templatetags/core_tags.py @@ -211,12 +211,6 @@ def load_optionalmenu(user): ) -@register.simple_tag -def load_notifications(user): - content = events.raiseQueryEvent("TopNotifications", user) - return "".join(content) - - @register.simple_tag def display_messages(msgs): text = "" diff --git a/modoboa/core/urls.py b/modoboa/core/urls.py index c677f73c4..5db970d7c 100644 --- a/modoboa/core/urls.py +++ b/modoboa/core/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import patterns +from django.conf.urls import patterns, url urlpatterns = patterns( @@ -7,13 +7,18 @@ urlpatterns = patterns( (r'^accounts/login/$', 'modoboa.core.views.auth.dologin'), (r'^accounts/logout/$', 'modoboa.core.views.auth.dologout'), - (r'^core/$', 'modoboa.core.views.admin.viewsettings'), + url(r'^core/$', 'modoboa.core.views.admin.viewsettings', + name="admin_index"), (r'^core/parameters/$', 'modoboa.core.views.admin.viewparameters'), (r'^core/parameters/save/$', 'modoboa.core.views.admin.saveparameters'), (r'^core/extensions/$', 'modoboa.core.views.admin.viewextensions'), (r'^core/extensions/save/$', 'modoboa.core.views.admin.saveextensions'), - (r'^core/info/$', 'modoboa.core.views.admin.information'), + url(r'^core/info/$', 'modoboa.core.views.admin.information', + name="information"), (r'^core/logs/$', 'modoboa.core.views.admin.logs'), + url(r'^core/top_notifications/check/$', + 'modoboa.core.views.admin.check_top_notifications', + name='check_top_notifications'), (r'^user/$', 'modoboa.core.views.user.index'), (r'^user/preferences/$', 'modoboa.core.views.user.preferences'), diff --git a/modoboa/core/utils.py b/modoboa/core/utils.py new file mode 100644 index 000000000..200bd41e9 --- /dev/null +++ b/modoboa/core/utils.py @@ -0,0 +1,32 @@ +""" +Utility functions. +""" +import os +import pkg_resources + +import requests + +from django.conf import settings + +from versionfield.constants import DEFAULT_NUMBER_BITS +from versionfield.version import Version + +from modoboa.lib import parameters + + +def new_version_available(): + """Check if a new version of Modoboa is available.""" + if parameters.get_admin("CHECK_NEW_VERSIONS") == "no": + return None + local_version = pkg_resources.get_distribution("modoboa").version + url = os.path.join(settings.MODOBOA_API_URL, "current_version/") + resp = requests.get(url, params={ + "client_version": pkg_resources.get_distribution("modoboa").version + }) + if resp.status_code != 200: + return None + resp = resp.json() + version = Version(resp["version"], DEFAULT_NUMBER_BITS) + if version <= Version(local_version, DEFAULT_NUMBER_BITS): + return None + return resp diff --git a/modoboa/core/views/admin.py b/modoboa/core/views/admin.py index 26f97e0a9..e2b12671b 100644 --- a/modoboa/core/views/admin.py +++ b/modoboa/core/views/admin.py @@ -4,12 +4,13 @@ from django.utils.translation import ugettext as _ from django.contrib.auth.decorators import ( login_required, user_passes_test ) -from modoboa.lib import parameters +from modoboa.lib import events, parameters from modoboa.lib.webutils import ( _render_to_string, render_to_json_response ) from modoboa.lib.listing import get_sort_order, get_listing_page from modoboa.core.models import Extension, Log +from modoboa.core.utils import new_version_available from modoboa.core.tables import ExtensionsTable @@ -94,7 +95,9 @@ def saveextensions(request): @user_passes_test(lambda u: u.is_superuser) def information(request, tplname="core/information.html"): return render_to_json_response({ - "content": render_to_string(tplname, {}) + "content": render_to_string(tplname, { + "new_version": new_version_available() + }), }) @@ -118,3 +121,12 @@ def logs(request, tplname="core/logs.html"): "page": page.number, "paginbar": pagination_bar(page), }) + +@login_required +def check_top_notifications(request): + """ + AJAX service to check for new top notifications to display. + """ + return render_to_json_response( + events.raiseQueryEvent("TopNotifications", request.user, True) + ) diff --git a/modoboa/extensions/amavis/app_settings.py b/modoboa/extensions/amavis/app_settings.py index 9d933476c..9e777e281 100644 --- a/modoboa/extensions/amavis/app_settings.py +++ b/modoboa/extensions/amavis/app_settings.py @@ -17,7 +17,7 @@ class ParametersForm(AdminParametersForm): ) sep1 = SeparatorField(label=_("Messages releasing")) - + released_msgs_cleanup = YesNoField( label=_("Remove released messages"), initial="no", @@ -49,12 +49,6 @@ class ParametersForm(AdminParametersForm): initial="/var/amavis/amavisd.sock", help_text=_("Path to the PDP server socket (if unix mode)") ) - - check_requests_interval = forms.IntegerField( - label=_("Check requests interval"), - initial=30, - help_text=_("Interval between two release requests checks") - ) user_can_release = YesNoField( label=_("Allow direct release"), diff --git a/modoboa/extensions/amavis/general_callbacks.py b/modoboa/extensions/amavis/general_callbacks.py index 1246c7202..b86ab7cf8 100644 --- a/modoboa/extensions/amavis/general_callbacks.py +++ b/modoboa/extensions/amavis/general_callbacks.py @@ -53,35 +53,6 @@ def extra_static_content(caller, user): if user.group == "SimpleUsers": return [] - if caller == 'top' and parameters.get_admin("USER_CAN_RELEASE") == 'no': - 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"), - }) - )] - if caller == 'domains': tpl = Template(""" + {% url "django.views.i18n.javascript_catalog" as catalog_url %} {% extra_static_content "top" user %} {% block html_head %}{% endblock %} @@ -46,7 +55,7 @@ -
{% load_notifications user %}
+
{% include "common/top_notifications.html" %}
diff --git a/requirements.txt b/requirements.txt index 9c7b48790..01c6c8f36 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ argparse rfc6266 factory-boy passlib +-e git+https://github.com/tonioo/django-versionfield.git#egg=django-versionfield From 311cc6bd7be4cfeeebbec262b42cb01e2ebc764c Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 27 Jul 2014 11:38:37 +0200 Subject: [PATCH 02/11] Early imports can be dangerous :p see #551 --- modoboa/core/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modoboa/core/__init__.py b/modoboa/core/__init__.py index bd9ba2318..5621d3a0d 100644 --- a/modoboa/core/__init__.py +++ b/modoboa/core/__init__.py @@ -3,7 +3,6 @@ import os from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _, ugettext_lazy -from modoboa.core.utils import new_version_available from modoboa.lib import parameters, events BASE_EVENTS = [ @@ -75,6 +74,8 @@ def check_for_new_version(user, include_all): """ Check if a new version of Modoboa is available. """ + from modoboa.core.utils import new_version_available + if not user.is_superuser: return [] if new_version_available() is None: From e6b041b8dc9227a427af2b965332b7d5fbb55c5b Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 27 Jul 2014 12:16:46 +0200 Subject: [PATCH 03/11] Catch requests exceptions. see #551 --- modoboa/core/utils.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/modoboa/core/utils.py b/modoboa/core/utils.py index 200bd41e9..2b2ea13b5 100644 --- a/modoboa/core/utils.py +++ b/modoboa/core/utils.py @@ -5,6 +5,7 @@ import os import pkg_resources import requests +from requests.exceptions import RequestException from django.conf import settings @@ -20,9 +21,12 @@ def new_version_available(): return None local_version = pkg_resources.get_distribution("modoboa").version url = os.path.join(settings.MODOBOA_API_URL, "current_version/") - resp = requests.get(url, params={ - "client_version": pkg_resources.get_distribution("modoboa").version - }) + try: + resp = requests.get(url, params={ + "client_version": pkg_resources.get_distribution("modoboa").version + }) + except RequestException: + return None if resp.status_code != 200: return None resp = resp.json() From d0ad28ff054315186ee223c31f11363d20e11be3 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 27 Jul 2014 20:08:02 +0200 Subject: [PATCH 04/11] Regroup all queries in one place. see #589 --- .../extensions/amavis/general_callbacks.py | 5 +- modoboa/extensions/amavis/lib.py | 6 +- .../amavis/management/commands/amnotify.py | 7 +- modoboa/extensions/amavis/sql_connector.py | 291 ++++++++++++++++++ modoboa/extensions/amavis/sql_listing.py | 250 +-------------- modoboa/extensions/amavis/views.py | 68 ++-- modoboa/lib/email_listing.py | 10 - modoboa/lib/webutils.py | 10 + 8 files changed, 343 insertions(+), 304 deletions(-) create mode 100644 modoboa/extensions/amavis/sql_connector.py diff --git a/modoboa/extensions/amavis/general_callbacks.py b/modoboa/extensions/amavis/general_callbacks.py index 1246c7202..ce2f76e91 100644 --- a/modoboa/extensions/amavis/general_callbacks.py +++ b/modoboa/extensions/amavis/general_callbacks.py @@ -1,6 +1,7 @@ from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ from django.template import Template, Context + from modoboa.lib import events, parameters from modoboa.extensions.admin.models import DomainAlias from modoboa.extensions.amavis.lib import ( @@ -96,12 +97,12 @@ $(document).bind('domform_init', function() { @events.observe("TopNotifications") def display_requests(user): - from .sql_listing import get_wrapper + from .sql_connector import get_connector if parameters.get_admin("USER_CAN_RELEASE") == "yes" \ or user.group == "SimpleUsers": return [] - nbrequests = get_wrapper().get_pending_requests(user) + nbrequests = get_connector(user=user).get_pending_requests() url = reverse("modoboa.extensions.amavis.views.index") url += "#listing/?viewrequests=1" diff --git a/modoboa/extensions/amavis/lib.py b/modoboa/extensions/amavis/lib.py index 9a9ed39bd..20f02a6c3 100644 --- a/modoboa/extensions/amavis/lib.py +++ b/modoboa/extensions/amavis/lib.py @@ -75,7 +75,7 @@ recipient=%s """ % (mailid, secretid, recipient)) answer = self.sock.recv(1024) answer = self.decode(answer) - if re.search("250 [\d\.]+ Ok", answer): + if re.search(r"250 [\d\.]+ Ok", answer): return True return False @@ -88,7 +88,9 @@ class QuarantineNavigationParameters(NavigationParameters): super(QuarantineNavigationParameters, self).__init__( request, 'quarantine_navparams' ) - self.parameters += [('msgtype', None, False)] + self.parameters += [ + ('msgtype', None, False), ('viewrequests', None, False) + ] def back_to_listing(self): """Return the current listing URL. diff --git a/modoboa/extensions/amavis/management/commands/amnotify.py b/modoboa/extensions/amavis/management/commands/amnotify.py index 54846b12e..1ed96f37d 100644 --- a/modoboa/extensions/amavis/management/commands/amnotify.py +++ b/modoboa/extensions/amavis/management/commands/amnotify.py @@ -1,20 +1,23 @@ #!/usr/bin/env python # coding: utf-8 from optparse import make_option + from django import db from django.core.management.base import BaseCommand, CommandError from django.template.loader import render_to_string from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ + from modoboa.core.models import User from modoboa.lib import parameters from modoboa.lib.emailutils import sendmail_simple from modoboa.extensions.admin.models import Domain + from modoboa.extensions.amavis import Amavis from modoboa.extensions.amavis.models import ( Msgrcpt ) -from modoboa.extensions.amavis.sql_listing import get_wrapper +from modoboa.extensions.amavis.sql_connector import get_connector class Command(BaseCommand): @@ -75,7 +78,7 @@ class Command(BaseCommand): if not da.mailbox_set.count(): continue rcpt = da.mailbox_set.all()[0].full_address - reqs = get_wrapper().get_domains_pending_requests( + reqs = get_connector().get_domains_pending_requests( Domain.objects.get_for_admin(da) ) if reqs.count(): diff --git a/modoboa/extensions/amavis/sql_connector.py b/modoboa/extensions/amavis/sql_connector.py new file mode 100644 index 000000000..12637781f --- /dev/null +++ b/modoboa/extensions/amavis/sql_connector.py @@ -0,0 +1,291 @@ +""" +SQL connector module. +""" +from django.db.models import Q + +from modoboa.extensions.admin.models import Domain +from modoboa.lib.dbutils import db_type +from modoboa.lib.webutils import static_url + +from .models import Quarantine, Msgrcpt, Maddr + + +class SQLconnector(object): + + """ + This class handles all database operations. + """ + + ORDER_TRANSLATION_TABLE = { + "type": "content", + "score": "bspam_level", + "date": "mail__time_num", + "subject": "mail__subject", + "from": "mail__from_addr", + "to": "rid__email" + } + + QUARANTINE_FIELDS = [ + "content", + "bspam_level", + "rs", + "rid__email", + "mail__from_addr", + "mail__subject", + "mail__mail_id", + "mail__time_num", + ] + + def __init__(self, user=None, navparams=None): + """Constructor.""" + self.user = user + self.navparams = navparams + self.messages = None + + self._messages_count = None + + def _exec(self, query, args): + """Execute a raw SQL query. + + :param string query: query to execute + :param list args: a list of arguments to replace in :kw:`query` + """ + from django.db import connections, transaction + + cursor = connections['amavis'].cursor() + cursor.execute(query, args) + transaction.commit_unless_managed(using='amavis') + + def _apply_msgrcpt_filters(self, flt): + """Return filters based on user's role. + + """ + if self.user.group == 'SimpleUsers': + rcpts = [self.user.email] \ + + self.user.mailbox_set.all()[0].alias_addresses + flt &= Q(rid__email__in=rcpts) + elif not self.user.is_superuser: + doms = Domain.objects.get_for_admin(self.user) + regexp = "(%s)" % '|'.join([dom.name for dom in doms]) + flt &= Q(rid__email__regex=regexp) + + def _apply_extra_search_filter(self, crit, pattern): + if crit == "to": + return Q(rid__email__contains=pattern) + return None + + def _apply_extra_select_filters(self, messages): + pass + + def _get_quarantine_content(self): + """Fetch quarantine content. + + Filters: rs, rid, content + """ + flt = Q(rs__in=[' ', 'V', 'R', 'p']) \ + if self.navparams.get('viewrequests', '0') != '1' else Q(rs='p') + self._apply_msgrcpt_filters(flt) + pattern = self.navparams.get("pattern", "") + if pattern: + criteria = self.navparams.get('criteria') + if criteria == "both": + criteria = "from_addr,subject,to" + search_flt = None + for crit in criteria.split(","): + if crit == "from_addr": + nfilter = Q(mail__from_addr__contains=pattern) + elif crit == "subject": + nfilter = Q(mail__subject__contains=pattern) + else: + nfilter = self._apply_extra_search_filter(crit, pattern) + if nfilter is None: + continue + search_flt = nfilter \ + if search_flt is None else search_flt | nfilter + if search_flt: + flt &= search_flt + msgtype = self.navparams.get('msgtype', None) + if msgtype is not None: + flt &= Q(content=msgtype) + + flt &= Q( + mail__in=Quarantine.objects.filter(chunk_ind=1).values("mail_id") + ) + + messages = Msgrcpt.objects.select_related().filter(flt) + self._apply_extra_select_filters(messages) + return messages + + def messages_count(self, **kwargs): + """Return the total number of messages living in the quarantine. + + We also store the built queryset for a later use. + """ + if self.user is None or self.navparams is None: + return None + if self._messages_count is None: + self.messages = self._get_quarantine_content() + self.messages = self.messages.values(*self.QUARANTINE_FIELDS) + + order = self.navparams.get("order") + if order is not None: + sign = "" + if order[0] == "-": + sign = "-" + order = order[1:] + order = self.ORDER_TRANSLATION_TABLE[order] + self.messages = self.messages.order_by(sign + order) + + self._messages_count = self.messages.count() + + return self._messages_count + + def fetch(self, start=None, stop=None, **kwargs): + """Fetch a range of messages from the internal cache. + + """ + emails = [] + for qm in self.messages[start - 1:stop]: + m = {"from": qm["mail__from_addr"], + "to": qm["rid__email"], + "subject": qm["mail__subject"], + "mailid": qm["mail__mail_id"], + "date": qm["mail__time_num"], + "type": qm["content"], + "score": qm["bspam_level"]} + rs = qm["rs"] + if rs == 'D': + continue + elif rs in ['', ' ']: + m["class"] = "unseen" + elif rs == 'R': + m["img_rstatus"] = static_url("pics/release.png") + elif rs == 'p': + m["class"] = "pending" + emails.append(m) + return emails + + def set_msgrcpt_status(self, address, mailid, status): + """Change the status (rs field) of a message recipient. + + :param string status: status + """ + addr = Maddr.objects.get(email=address) + self._exec( + "UPDATE msgrcpt SET rs=%s WHERE mail_id=%s AND rid=%s", + [status, mailid, addr.id] + ) + + def get_recipient_messages(self, address, mailids): + return Msgrcpt.objects.filter(mail__in=mailids, rid__email=address) + + def get_domains_pending_requests(self, domains): + regexp = "(%s)" % '|'.join([dom.name for dom in domains]) + return Msgrcpt.objects.filter(rs='p', rid__email__regex=regexp) + + def get_pending_requests(self): + """Return the number of requests currently pending. + + :param user: a ``User`` instance + """ + rq = Q(rs='p') + if not self.user.is_superuser: + doms = Domain.objects.get_for_admin(self.user) + if not doms.count(): + return 0 + regexp = "(%s)" % '|'.join([dom.name for dom in doms]) + doms_q = Q(rid__email__regex=regexp) + rq &= doms_q + return Msgrcpt.objects.filter(rq).count() + + def get_mail_content(self, mailid): + return Quarantine.objects.filter(mail=mailid) + + +class PgSQLconnector(SQLconnector): + + """ + The postgres version. + + Make use of ``QuerySet.extra`` and postgres ``convert_from`` + function to let the quarantine manager work as expected ! + """ + + def _apply_msgrcpt_filters(self, flt): + """Return filters based on user's role. + + """ + self._where = ["U0.rid=maddr.id"] + if self.user.group == 'SimpleUsers': + rcpts = [self.user.email] \ + + self.user.mailbox_set.all()[0].alias_addresses + self._where.append( + "convert_from(maddr.email, 'UTF8') IN (%s)" \ + % (','.join(["'%s'" % rcpt for rcpt in rcpts]))) + elif not self.user.is_superuser: + doms = Domain.objects.get_for_admin(self.user) + regexp = "(%s)" % '|'.join([dom.name for dom in doms]) + self._where.append( + "convert_from(maddr.email, 'UTF8') ~ '%s'" % regexp) + + def _apply_extra_search_filter(self, crit, pattern): + if crit == "to": + self._where.append( + "convert_from(maddr.email, 'UTF8') LIKE '%%%s%%'" + % pattern) + return None + + def _apply_extra_select_filters(self, messages): + messages = messages.extra(where=self._where, tables=['maddr']) + + def get_recipient_message(self, address, mailid): + qset = Msgrcpt.objects.filter(mail=mailid).extra( + where=["msgrcpt.rid=maddr.id", + "convert_from(maddr.email, 'UTF8') = '%s'" % address], + tables=['maddr'] + ) + return qset.all()[0] + + def get_recipient_messages(self, address, mailids): + return Msgrcpt.objects.filter(mail__in=mailids).extra( + where=["U0.rid=maddr.id", + "convert_from(maddr.email, 'UTF8') = '%s'" % address], + tables=['maddr'] + ) + + def get_domains_pending_requests(self, domains): + regexp = "(%s)" % '|'.join([dom.name for dom in domains]) + return Msgrcpt.objects.filter(rs='p').extra( + where=["msgrcpt.rid=maddr.id", + "convert_from(maddr.email, 'UTF8') ~ '%s'" % regexp], + tables=['maddr'] + ) + + def get_pending_requests(self): + rq = Q(rs='p') + if not self.user.is_superuser: + doms = Domain.objects.get_for_admin(self.user) + if not doms.count(): + return 0 + regexp = "(%s)" % '|'.join([dom.name for dom in doms]) + return Msgrcpt.objects.filter(rq).extra( + where=["msgrcpt.rid=maddr.id", + "convert_from(maddr.email, 'UTF8') ~ '%s'" % (regexp,)], + tables=['maddr'] + ).count() + return Msgrcpt.objects.filter(rq).count() + + def get_mail_content(self, mailid): + return Quarantine.objects.filter(mail=mailid).extra( + select={'mail_text': "convert_from(mail_text, 'UTF8')"} + ) + + +def get_connector(**kwargs): + """Return the appropriate *connector* class. + + The result depends on the DB engine in use. + """ + if db_type("amavis") == 'postgres': + return PgSQLconnector(**kwargs) + return SQLconnector(**kwargs) diff --git a/modoboa/extensions/amavis/sql_listing.py b/modoboa/extensions/amavis/sql_listing.py index ac1b378ed..c8cbaf122 100644 --- a/modoboa/extensions/amavis/sql_listing.py +++ b/modoboa/extensions/amavis/sql_listing.py @@ -1,16 +1,14 @@ # coding: utf-8 -import re from datetime import datetime + from django.template.loader import render_to_string from django.utils.translation import ugettext_lazy -from django.db.models import Q + from modoboa.lib import tables -from modoboa.lib.webutils import static_url -from modoboa.lib.email_listing import MBconnector, EmailListing +from modoboa.lib.email_listing import EmailListing from modoboa.lib.emailutils import Email -from modoboa.lib.dbutils import db_type -from modoboa.extensions.admin.models import Domain -from .models import Quarantine, Msgrcpt + +from .sql_connector import get_connector class Qtable(tables.Table): @@ -60,238 +58,6 @@ class Qtable(tables.Table): return datetime.fromtimestamp(value) -class SQLconnector(MBconnector): - order_translation_table = { - "type": "mail__msgrcpt__content", - "score": "mail__msgrcpt__bspam_level", - "date": "mail__time_num", - "subject": "mail__subject", - "from": "mail__from_addr", - "to": "mail__msgrcpt__rid__email" - } - - def __init__(self, mail_ids=None, filter=None): - self.count = None - self.messages = None - self.mail_ids = mail_ids - self.filter = filter - - def messages_count(self, **kwargs): - if self.count is None: - filter = Q(chunk_ind=1) - if self.mail_ids is not None: - filter &= Q(mail__in=self.mail_ids) - if self.filter: - filter &= self.filter - self.messages = Quarantine.objects.filter(filter).values( - "mail__from_addr", - "mail__msgrcpt__rid__email", - "mail__subject", - "mail__mail_id", - "mail__time_num", - "mail__msgrcpt__content", - "mail__msgrcpt__bspam_level", - "mail__msgrcpt__rs" - ) - if "order" in kwargs: - order = kwargs["order"] - sign = "" - if order[0] == "-": - sign = "-" - order = order[1:] - order = self.order_translation_table[order] - self.messages = self.messages.order_by(sign + order) - - self.count = self.messages.count() - return self.count - - def fetch(self, start=None, stop=None, **kwargs): - emails = [] - for qm in self.messages[start - 1:stop]: - m = {"from": qm["mail__from_addr"], - "to": qm["mail__msgrcpt__rid__email"], - "subject": qm["mail__subject"], - "mailid": qm["mail__mail_id"], - "date": qm["mail__time_num"], - "type": qm["mail__msgrcpt__content"], - "score": qm["mail__msgrcpt__bspam_level"]} - rs = qm["mail__msgrcpt__rs"] - if rs == 'D': - continue - elif rs in ['', ' ']: - m["class"] = "unseen" - elif rs == 'R': - m["img_rstatus"] = static_url("pics/release.png") - elif rs == 'p': - m["class"] = "pending" - emails.append(m) - return emails - - -class SQLWrapper(object): - """A simple SQL wrapper. - - This wrapper has been added just to answer the *Postgres bytea - fields* issue :p - - The base class doesn't add anything special but defines the method - (and so piece of SQL queries) that should be overloaded when - Postgres is in use. - - See ``PgWrapper`` for the real mess... - """ - - def _exec(self, query, args): - """Execute a raw SQL query. - - :param string query: query to execute - :param list args: a list of arguments to replace in :kw:`query` - """ - from django.db import connections, transaction - - cursor = connections['amavis'].cursor() - cursor.execute(query, args) - transaction.commit_unless_managed(using='amavis') - - def get_mails(self, request, rcptfilter=None): - """Retrieve all messages visible by a user. - - Simple users can only see the messages - - :rtype: QuerySet - """ - q = Q(rs='p') \ - if request.GET.get("viewrequests", None) == "1" else ~Q(rs='D') - if request.user.group == 'SimpleUsers': - rcpts = [request.user.email] \ - + request.user.mailbox_set.all()[0].alias_addresses - q &= Q(rid__email__in=rcpts) - elif not request.user.is_superuser: - doms = Domain.objects.get_for_admin(request.user) - regexp = "(%s)" % '|'.join([dom.name for dom in doms]) - doms_q = Q(rid__email__regex=regexp) - q &= doms_q - if rcptfilter is not None: - q &= Q(rid__email__contains=rcptfilter) - return Msgrcpt.objects.filter(q).values("mail_id") - - def get_recipient_message(self, address, mailid): - return Msgrcpt.objects.get(mail=mailid, rid__email=address) - - def set_msgrcpt_status(self, address, mailid, status): - """Change the status (rs field) of a message recipient. - - :param string status: status - """ - from modoboa.extensions.amavis.models import Maddr - - addr = Maddr.objects.get(email=address) - self._exec( - "UPDATE msgrcpt SET rs=%s WHERE mail_id=%s AND rid=%s", - [status, mailid, addr.id] - ) - - def get_recipient_messages(self, address, mailids): - return Msgrcpt.objects.filter(mail__in=mailids, rid__email=address) - - def get_domains_pending_requests(self, domains): - regexp = "(%s)" % '|'.join([dom.name for dom in domains]) - return Msgrcpt.objects.filter(rs='p', rid__email__regex=regexp) - - def get_pending_requests(self, user): - """Return the number of current pending requests - - :param user: a ``User`` instance - """ - rq = Q(rs='p') - if not user.is_superuser: - doms = Domain.objects.get_for_admin(user) - if not doms.count(): - return 0 - regexp = "(%s)" % '|'.join([dom.name for dom in doms]) - doms_q = Q(rid__email__regex=regexp) - rq &= doms_q - return Msgrcpt.objects.filter(rq).count() - - def get_mail_content(self, mailid): - return Quarantine.objects.filter(mail=mailid) - - -class PgWrapper(SQLWrapper): - """The postgres wrapper - - Make use of ``QuerySet.extra`` and postgres ``convert_from`` - function to let the quarantine manager work as expected ! - """ - - def get_mails(self, request, rcptfilter=None): - q = Q(rs='p') \ - if request.GET.get("viewrequests", None) == "1" else ~Q(rs='D') - where = ["U0.rid=maddr.id"] - if request.user.group == 'SimpleUsers': - rcpts = [request.user.email] \ - + request.user.mailbox_set.all()[0].alias_addresses - where.append("convert_from(maddr.email, 'UTF8') IN (%s)" \ - % (','.join(["'%s'" % rcpt for rcpt in rcpts]))) - elif not request.user.is_superuser: - doms = Domain.objects.get_for_admin(request.user) - regexp = "(%s)" % '|'.join([dom.name for dom in doms]) - where.append("convert_from(maddr.email, 'UTF8') ~ '%s'" % regexp) - if rcptfilter is not None: - where.append("convert_from(maddr.email, 'UTF8') LIKE '%%%s%%'" - % rcptfilter) - return Msgrcpt.objects.filter(q)\ - .extra(where=where, tables=['maddr']).values("mail_id") - - def get_recipient_message(self, address, mailid): - qset = Msgrcpt.objects.filter(mail=mailid).extra( - where=["msgrcpt.rid=maddr.id", "convert_from(maddr.email, 'UTF8') = '%s'" % address], - tables=['maddr'] - ) - return qset.all()[0] - - def get_recipient_messages(self, address, mailids): - return Msgrcpt.objects.filter(mail__in=mailids).extra( - where=["U0.rid=maddr.id", "convert_from(maddr.email, 'UTF8') = '%s'" % address], - tables=['maddr'] - ) - - def get_domains_pending_requests(self, domains): - regexp = "(%s)" % '|'.join([dom.name for dom in domains]) - return Msgrcpt.objects.filter(rs='p').extra( - where=["msgrcpt.rid=maddr.id", "convert_from(maddr.email, 'UTF8') ~ '%s'" % regexp], - tables=['maddr'] - ) - - def get_pending_requests(self, user): - rq = Q(rs='p') - if not user.is_superuser: - doms = Domain.objects.get_for_admin(user) - if not doms.count(): - return 0 - regexp = "(%s)" % '|'.join([dom.name for dom in doms]) - return Msgrcpt.objects.filter(rq).extra( - where=["msgrcpt.rid=maddr.id", "convert_from(maddr.email, 'UTF8') ~ '%s'" % (regexp,)], - tables=['maddr'] - ).count() - return Msgrcpt.objects.filter(rq).count() - - def get_mail_content(self, mailid): - return Quarantine.objects.filter(mail=mailid).extra( - select={'mail_text': "convert_from(mail_text, 'UTF8')"} - ) - - -def get_wrapper(): - """Return the appropriate *Wrapper class - - The result depends on the DB engine in use. - """ - if db_type("amavis") == 'postgres': - return PgWrapper() - return SQLWrapper() - - class SQLlisting(EmailListing): tpl = "amavis/index.html" tbltype = Qtable @@ -299,8 +65,8 @@ class SQLlisting(EmailListing): defcallback = "updatelisting" reset_wm_url = True - def __init__(self, user, msgs, filter, **kwargs): - self.mbc = SQLconnector(msgs, filter) + def __init__(self, user, **kwargs): + self.mbc = get_connector(user=user, navparams=kwargs["navparams"]) super(SQLlisting, self).__init__(**kwargs) self.show_listing_headers = True @@ -333,7 +99,7 @@ class SQLemail(Email): import email if self._msg is None: - qmails = get_wrapper().get_mail_content(self.mailid) + qmails = get_connector().get_mail_content(self.mailid) self._msg = email.message_from_string( "".join([qm.mail_text for qm in qmails]) ) diff --git a/modoboa/extensions/amavis/views.py b/modoboa/extensions/amavis/views.py index 8eb7da2fb..31514820a 100644 --- a/modoboa/extensions/amavis/views.py +++ b/modoboa/extensions/amavis/views.py @@ -7,7 +7,6 @@ from django.utils.translation import ugettext as _, ungettext from django.core.urlresolvers import reverse from django.contrib.auth.decorators \ import login_required, user_passes_test -from django.db.models import Q from modoboa.lib import parameters from modoboa.lib.exceptions import BadRequest from modoboa.lib.webutils import ( @@ -19,8 +18,9 @@ from modoboa.extensions.amavis.templatetags.amavis_tags import ( quar_menu, viewm_menu ) from .lib import selfservice, AMrelease, QuarantineNavigationParameters -from .sql_listing import SQLlisting, SQLemail, get_wrapper from .models import Msgrcpt +from .sql_connector import get_connector +from .sql_listing import SQLlisting, SQLemail def empty_quarantine(request): @@ -31,45 +31,21 @@ def empty_quarantine(request): @login_required def _listing(request): - flt = None - rcptfilter = None - msgs = None - if not request.user.is_superuser and request.user.group != 'SimpleUsers': if not Domain.objects.get_for_admin(request.user).count(): return empty_quarantine(request) navparams = QuarantineNavigationParameters(request) navparams.store() - pattern = navparams.get('pattern', '') - if pattern: - criteria = navparams.get('criteria') - if criteria == "both": - criteria = "from_addr,subject,to" - for c in criteria.split(","): - if c == "from_addr": - nfilter = Q(mail__from_addr__contains=pattern) - elif c == "subject": - nfilter = Q(mail__subject__contains=pattern) - elif c == "to": - rcptfilter = pattern - continue - else: - raise BadRequest("unsupported search criteria %s" % c) - flt = nfilter if flt is None else flt | nfilter - msgtype = navparams.get('msgtype', None) - if msgtype is not None: - nfilter = Q(mail__msgrcpt__content=msgtype) - flt = flt | nfilter if flt is not None else nfilter - msgs = get_wrapper().get_mails(request, rcptfilter) - page = navparams.get('page') lst = SQLlisting( - request.user, msgs, flt, - navparams=request.session["quarantine_navparams"], - elems_per_page=int(parameters.get_user(request.user, "MESSAGES_PER_PAGE")) + request.user, + navparams=navparams, + elems_per_page=int( + parameters.get_user(request.user, "MESSAGES_PER_PAGE") + ) ) - page = lst.paginator.getpage(page) + page = lst.paginator.getpage(navparams.get('page')) if not page: return empty_quarantine(request) @@ -129,7 +105,7 @@ def viewmail(request, mail_id): if request.user.mailbox_set.count(): mb = Mailbox.objects.get(user=request.user) if rcpt == mb.full_address or rcpt in mb.alias_addresses: - get_wrapper().set_msgrcpt_status(rcpt, mail_id, 'V') + get_connector().set_msgrcpt_status(rcpt, mail_id, 'V') content = Template(""" @@ -143,7 +119,7 @@ def viewmail(request, mail_id): @login_required def viewheaders(request, mail_id): content = "" - for qm in get_wrapper().get_mail_content(mail_id): + for qm in get_connector().get_mail_content(mail_id): content += qm.mail_text msg = email.message_from_string(content) return render(request, 'amavis/viewheader.html', { @@ -165,7 +141,7 @@ def delete_selfservice(request, mail_id): if rcpt is None: raise BadRequest(_("Invalid request")) try: - get_wrapper().set_msgrcpt_status(rcpt, mail_id, 'D') + get_connector().set_msgrcpt_status(rcpt, mail_id, 'D') except Msgrcpt.DoesNotExist: raise BadRequest(_("Invalid request")) return render_to_json_response(_("Message deleted")) @@ -178,7 +154,7 @@ def delete(request, mail_id): :param str mail_id: message unique identifier """ mail_id = check_mail_id(request, mail_id) - wrapper = get_wrapper() + connector = get_connector() mb = Mailbox.objects.get(user=request.user) \ if request.user.group == 'SimpleUsers' else None for mid in mail_id: @@ -186,7 +162,7 @@ def delete(request, mail_id): if mb is not None and r != mb.full_address \ and not r in mb.alias_addresses: continue - wrapper.set_msgrcpt_status(r, i, 'D') + connector.set_msgrcpt_status(r, i, 'D') message = ungettext("%(count)d message deleted successfully", "%(count)d messages deleted successfully", len(mail_id)) % {"count": len(mail_id)} @@ -201,21 +177,21 @@ def release_selfservice(request, mail_id): secret_id = request.GET.get("secret_id", None) if rcpt is None or secret_id is None: raise BadRequest(_("Invalid request")) - wrapper = get_wrapper() + connector = get_connector() try: - msgrcpt = wrapper.get_recipient_message(rcpt, mail_id) + msgrcpt = connector.get_recipient_message(rcpt, mail_id) except Msgrcpt.DoesNotExist: raise BadRequest(_("Invalid request")) if secret_id != msgrcpt.mail.secret_id: raise BadRequest(_("Invalid request")) if parameters.get_admin("USER_CAN_RELEASE") == "no": - wrapper.set_msgrcpt_status(rcpt, mail_id, 'p') + connector.set_msgrcpt_status(rcpt, mail_id, 'p') msg = _("Request sent") else: amr = AMrelease() result = amr.sendreq(mail_id, secret_id, rcpt) if result: - wrapper.set_msgrcpt_status(rcpt, mail_id, 'R') + connector.set_msgrcpt_status(rcpt, mail_id, 'R') msg = _("Message released") else: raise BadRequest(result) @@ -230,7 +206,7 @@ def release(request, mail_id): """ mail_id = check_mail_id(request, mail_id) msgrcpts = [] - wrapper = get_wrapper() + connector = get_connector() mb = Mailbox.objects.get(user=request.user) \ if request.user.group == 'SimpleUsers' else None for mid in mail_id: @@ -238,10 +214,10 @@ def release(request, mail_id): if mb is not None and r != mb.full_address \ and not r in mb.alias_addresses: continue - msgrcpts += [wrapper.get_recipient_message(r, i)] + msgrcpts += [connector.get_recipient_message(r, i)] if mb is not None and parameters.get_admin("USER_CAN_RELEASE") == "no": for msgrcpt in msgrcpts: - wrapper.set_msgrcpt_status( + connector.set_msgrcpt_status( msgrcpt.rid.email, msgrcpt.mail.mail_id, 'p' ) message = ungettext("%(count)d request sent", @@ -259,7 +235,7 @@ def release(request, mail_id): rcpt.mail.mail_id, rcpt.mail.secret_id, rcpt.rid.email ) if result: - wrapper.set_msgrcpt_status(rcpt.rid.email, rcpt.mail.mail_id, 'R') + connector.set_msgrcpt_status(rcpt.rid.email, rcpt.mail.mail_id, 'R') else: error = result break @@ -293,5 +269,5 @@ def process(request): @login_required @user_passes_test(lambda u: u.group != 'SimpleUsers') def nbrequests(request): - result = get_wrapper().get_pending_requests(request.user) + result = get_connector(user=request.user).get_pending_requests() return render_to_json_response({'requests': result}) diff --git a/modoboa/lib/email_listing.py b/modoboa/lib/email_listing.py index 8f7ff64c9..8d4c6e904 100644 --- a/modoboa/lib/email_listing.py +++ b/modoboa/lib/email_listing.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import re from django.conf import settings from django.template.loader import render_to_string from django.template import Template, Context @@ -8,15 +7,6 @@ from django.utils.translation import ugettext as _ attached_map = {} -class MBconnector(object): - def __init__(self, address, port): - self.address = address - self.port = port - - def messages_count(self, **kwargs): - pass - - class Page(object): def __init__(self, pageid, id_start, id_stop, items, items_per_page, has_previous, has_next, diff --git a/modoboa/lib/webutils.py b/modoboa/lib/webutils.py index 9b6ecccf1..ac882535c 100644 --- a/modoboa/lib/webutils.py +++ b/modoboa/lib/webutils.py @@ -175,6 +175,16 @@ class NavigationParameters(object): self.parameters = [('pattern', '', True), ('criteria', 'from_addr', False)] + def __getitem__(self, key): + """ + """ + return self.request.session[self.sessionkey][key] + + def __contains__(self, key): + """ + """ + return key in self.request.session[self.sessionkey] + def store(self): """Store navigation parameters into session. """ From f481c5e97e51021aa78cea1230871f026f02e239 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 29 Jul 2014 19:54:15 +0200 Subject: [PATCH 05/11] Few fixes in the postgres backend. see #589 --- modoboa/extensions/amavis/sql_connector.py | 8 ++++---- modoboa/lib/emailutils.py | 4 ---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/modoboa/extensions/amavis/sql_connector.py b/modoboa/extensions/amavis/sql_connector.py index 12637781f..27aa849cb 100644 --- a/modoboa/extensions/amavis/sql_connector.py +++ b/modoboa/extensions/amavis/sql_connector.py @@ -75,7 +75,7 @@ class SQLconnector(object): return None def _apply_extra_select_filters(self, messages): - pass + return messages def _get_quarantine_content(self): """Fetch quarantine content. @@ -113,7 +113,7 @@ class SQLconnector(object): ) messages = Msgrcpt.objects.select_related().filter(flt) - self._apply_extra_select_filters(messages) + messages = self._apply_extra_select_filters(messages) return messages def messages_count(self, **kwargs): @@ -215,7 +215,7 @@ class PgSQLconnector(SQLconnector): """Return filters based on user's role. """ - self._where = ["U0.rid=maddr.id"] + self._where = [] if self.user.group == 'SimpleUsers': rcpts = [self.user.email] \ + self.user.mailbox_set.all()[0].alias_addresses @@ -236,7 +236,7 @@ class PgSQLconnector(SQLconnector): return None def _apply_extra_select_filters(self, messages): - messages = messages.extra(where=self._where, tables=['maddr']) + return messages.extra(where=self._where) def get_recipient_message(self, address, mailid): qset = Msgrcpt.objects.filter(mail=mailid).extra( diff --git a/modoboa/lib/emailutils.py b/modoboa/lib/emailutils.py index 63b6bab67..234259e5d 100644 --- a/modoboa/lib/emailutils.py +++ b/modoboa/lib/emailutils.py @@ -34,10 +34,6 @@ class Email(object): self._msg = None self._body = None - if not mformat in self.contents or self.contents[mformat] == "": - # Fallback - self.mformat = mformat == "html" and "plain" or "html" - @property def msg(self): """Return an email.message object. From 41e87ff85bea88dee5506587de0b327f8cbcab05 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 29 Jul 2014 21:51:57 +0200 Subject: [PATCH 06/11] Adding some comments. see #589 --- modoboa/extensions/amavis/sql_connector.py | 44 ++++++++++++++-------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/modoboa/extensions/amavis/sql_connector.py b/modoboa/extensions/amavis/sql_connector.py index 27aa849cb..bd2758ca9 100644 --- a/modoboa/extensions/amavis/sql_connector.py +++ b/modoboa/extensions/amavis/sql_connector.py @@ -57,7 +57,7 @@ class SQLconnector(object): transaction.commit_unless_managed(using='amavis') def _apply_msgrcpt_filters(self, flt): - """Return filters based on user's role. + """Apply filters based on user's role. """ if self.user.group == 'SimpleUsers': @@ -70,11 +70,17 @@ class SQLconnector(object): flt &= Q(rid__email__regex=regexp) def _apply_extra_search_filter(self, crit, pattern): + """Apply search filters using additional criterias. + + """ if crit == "to": return Q(rid__email__contains=pattern) return None def _apply_extra_select_filters(self, messages): + """Just a hook to apply additional filters to the queryset. + + """ return messages def _get_quarantine_content(self): @@ -165,6 +171,11 @@ class SQLconnector(object): emails.append(m) return emails + def get_recipient_message(self, address, mailid): + """Retrieve a message for a given recipient. + """ + return Msgrcpt.objects.get(mail=mailid, rid__email=address) + def set_msgrcpt_status(self, address, mailid, status): """Change the status (rs field) of a message recipient. @@ -176,18 +187,14 @@ class SQLconnector(object): [status, mailid, addr.id] ) - def get_recipient_messages(self, address, mailids): - return Msgrcpt.objects.filter(mail__in=mailids, rid__email=address) - def get_domains_pending_requests(self, domains): + """Retrieve pending release requests for a list of domains. + """ regexp = "(%s)" % '|'.join([dom.name for dom in domains]) return Msgrcpt.objects.filter(rs='p', rid__email__regex=regexp) def get_pending_requests(self): - """Return the number of requests currently pending. - - :param user: a ``User`` instance - """ + """Return the number of requests currently pending.""" rq = Q(rs='p') if not self.user.is_superuser: doms = Domain.objects.get_for_admin(self.user) @@ -199,6 +206,8 @@ class SQLconnector(object): return Msgrcpt.objects.filter(rq).count() def get_mail_content(self, mailid): + """Retrieve the content of a message. + """ return Quarantine.objects.filter(mail=mailid) @@ -229,6 +238,9 @@ class PgSQLconnector(SQLconnector): "convert_from(maddr.email, 'UTF8') ~ '%s'" % regexp) def _apply_extra_search_filter(self, crit, pattern): + """Apply search filters using additional criterias. + + """ if crit == "to": self._where.append( "convert_from(maddr.email, 'UTF8') LIKE '%%%s%%'" @@ -236,9 +248,14 @@ class PgSQLconnector(SQLconnector): return None def _apply_extra_select_filters(self, messages): + """Just a hook to apply additional filters to the queryset. + + """ return messages.extra(where=self._where) def get_recipient_message(self, address, mailid): + """Retrieve a message for a given recipient. + """ qset = Msgrcpt.objects.filter(mail=mailid).extra( where=["msgrcpt.rid=maddr.id", "convert_from(maddr.email, 'UTF8') = '%s'" % address], @@ -246,14 +263,9 @@ class PgSQLconnector(SQLconnector): ) return qset.all()[0] - def get_recipient_messages(self, address, mailids): - return Msgrcpt.objects.filter(mail__in=mailids).extra( - where=["U0.rid=maddr.id", - "convert_from(maddr.email, 'UTF8') = '%s'" % address], - tables=['maddr'] - ) - def get_domains_pending_requests(self, domains): + """Retrieve pending release requests for a list of domains. + """ regexp = "(%s)" % '|'.join([dom.name for dom in domains]) return Msgrcpt.objects.filter(rs='p').extra( where=["msgrcpt.rid=maddr.id", @@ -262,6 +274,7 @@ class PgSQLconnector(SQLconnector): ) def get_pending_requests(self): + """Return the number of requests currently pending.""" rq = Q(rs='p') if not self.user.is_superuser: doms = Domain.objects.get_for_admin(self.user) @@ -276,6 +289,7 @@ class PgSQLconnector(SQLconnector): return Msgrcpt.objects.filter(rq).count() def get_mail_content(self, mailid): + """Retrieve the content of a message.""" return Quarantine.objects.filter(mail=mailid).extra( select={'mail_text': "convert_from(mail_text, 'UTF8')"} ) From 130bf68621146f6c7ea036da5476b28fc957c49d Mon Sep 17 00:00:00 2001 From: Alexandre Anriot Date: Fri, 8 Aug 2014 17:19:54 +0200 Subject: [PATCH 07/11] - Fix typo. --- modoboa/extensions/admin/app_settings.py | 2 +- modoboa/locale/cs_CZ/LC_MESSAGES/django.po | 2 +- modoboa/locale/de/LC_MESSAGES/django.po | 2 +- modoboa/locale/en/LC_MESSAGES/django.po | 2 +- modoboa/locale/es/LC_MESSAGES/django.po | 2 +- modoboa/locale/fr/LC_MESSAGES/django.po | 2 +- modoboa/locale/it/LC_MESSAGES/django.po | 2 +- modoboa/locale/nl_NL/LC_MESSAGES/django.po | 2 +- modoboa/locale/pt_PT/LC_MESSAGES/django.po | 2 +- modoboa/locale/ru/LC_MESSAGES/django.po | 2 +- modoboa/locale/sv/LC_MESSAGES/django.po | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/modoboa/extensions/admin/app_settings.py b/modoboa/extensions/admin/app_settings.py index 8daebb4ef..160fd4548 100644 --- a/modoboa/extensions/admin/app_settings.py +++ b/modoboa/extensions/admin/app_settings.py @@ -19,7 +19,7 @@ class AdminParametersForm(parameters.AdminParametersForm): ) mailboxes_owner = forms.CharField( - label=ugettext_lazy("Mailboxes ower"), + label=ugettext_lazy("Mailboxes owner"), initial="vmail", help_text=ugettext_lazy("The UNIX account who owns mailboxes on the filesystem") ) diff --git a/modoboa/locale/cs_CZ/LC_MESSAGES/django.po b/modoboa/locale/cs_CZ/LC_MESSAGES/django.po index fb86dd814..a990bed64 100644 --- a/modoboa/locale/cs_CZ/LC_MESSAGES/django.po +++ b/modoboa/locale/cs_CZ/LC_MESSAGES/django.po @@ -661,7 +661,7 @@ msgstr "" "přejmenovány nebo odstraněny v Modoboa" #: extensions/admin/app_settings.py:20 -msgid "Mailboxes ower" +msgid "Mailboxes owner" msgstr "Vlastník schránek" #: extensions/admin/app_settings.py:22 diff --git a/modoboa/locale/de/LC_MESSAGES/django.po b/modoboa/locale/de/LC_MESSAGES/django.po index bd96b891d..309061009 100644 --- a/modoboa/locale/de/LC_MESSAGES/django.po +++ b/modoboa/locale/de/LC_MESSAGES/django.po @@ -658,7 +658,7 @@ msgid "Rename or remove mailboxes on the filesystem when they get renamed or rem msgstr "Mailboxen auch in Dateisystem umbenennen oder entfernen wenn sie in Modoboa umbenannt oder entfernt werden" #: extensions/admin/app_settings.py:20 -msgid "Mailboxes ower" +msgid "Mailboxes owner" msgstr "Mailbox-Besitzer" #: extensions/admin/app_settings.py:22 diff --git a/modoboa/locale/en/LC_MESSAGES/django.po b/modoboa/locale/en/LC_MESSAGES/django.po index b29f5c96a..099630df0 100644 --- a/modoboa/locale/en/LC_MESSAGES/django.po +++ b/modoboa/locale/en/LC_MESSAGES/django.po @@ -623,7 +623,7 @@ msgid "" msgstr "" #: extensions/admin/app_settings.py:20 -msgid "Mailboxes ower" +msgid "Mailboxes owner" msgstr "" #: extensions/admin/app_settings.py:22 diff --git a/modoboa/locale/es/LC_MESSAGES/django.po b/modoboa/locale/es/LC_MESSAGES/django.po index b19b5dad3..67ff3ab25 100644 --- a/modoboa/locale/es/LC_MESSAGES/django.po +++ b/modoboa/locale/es/LC_MESSAGES/django.po @@ -663,7 +663,7 @@ msgstr "" "renombrados o eliminados dentro de Modoboa" #: extensions/admin/app_settings.py:20 -msgid "Mailboxes ower" +msgid "Mailboxes owner" msgstr "Propietario de los mailboxes" #: extensions/admin/app_settings.py:22 diff --git a/modoboa/locale/fr/LC_MESSAGES/django.po b/modoboa/locale/fr/LC_MESSAGES/django.po index 83efa32f0..f26cdbb63 100644 --- a/modoboa/locale/fr/LC_MESSAGES/django.po +++ b/modoboa/locale/fr/LC_MESSAGES/django.po @@ -661,7 +661,7 @@ msgstr "" "renommées ou supprimées dans Modoboa" #: extensions/admin/app_settings.py:20 -msgid "Mailboxes ower" +msgid "Mailboxes owner" msgstr "Propriétaire des boites" #: extensions/admin/app_settings.py:22 diff --git a/modoboa/locale/it/LC_MESSAGES/django.po b/modoboa/locale/it/LC_MESSAGES/django.po index 5a0c0f658..214b2548f 100644 --- a/modoboa/locale/it/LC_MESSAGES/django.po +++ b/modoboa/locale/it/LC_MESSAGES/django.po @@ -651,7 +651,7 @@ msgstr "" "o rimosse in Modoboa" #: extensions/admin/app_settings.py:20 -msgid "Mailboxes ower" +msgid "Mailboxes owner" msgstr "Proprietario delle caselle postali" #: extensions/admin/app_settings.py:22 diff --git a/modoboa/locale/nl_NL/LC_MESSAGES/django.po b/modoboa/locale/nl_NL/LC_MESSAGES/django.po index 9843651eb..8800d6907 100644 --- a/modoboa/locale/nl_NL/LC_MESSAGES/django.po +++ b/modoboa/locale/nl_NL/LC_MESSAGES/django.po @@ -622,7 +622,7 @@ msgid "" msgstr "Hernoem of verwijder mailboxen op het bestandssysteem wanner ze hernoemd of verwijderd worden in Modoboa." #: extensions/admin/app_settings.py:20 -msgid "Mailboxes ower" +msgid "Mailboxes owner" msgstr "Eigenaar van mailboxen" #: extensions/admin/app_settings.py:22 diff --git a/modoboa/locale/pt_PT/LC_MESSAGES/django.po b/modoboa/locale/pt_PT/LC_MESSAGES/django.po index 3a55652fe..c31e9f87f 100644 --- a/modoboa/locale/pt_PT/LC_MESSAGES/django.po +++ b/modoboa/locale/pt_PT/LC_MESSAGES/django.po @@ -627,7 +627,7 @@ msgid "" msgstr "Renomear ou remover as caixas de correio no sistema de ficheiros quando estas são renomeadas no Modoboa" #: extensions/admin/app_settings.py:20 -msgid "Mailboxes ower" +msgid "Mailboxes owner" msgstr "Detentor das Caixas de Correio (Owner)" #: extensions/admin/app_settings.py:22 diff --git a/modoboa/locale/ru/LC_MESSAGES/django.po b/modoboa/locale/ru/LC_MESSAGES/django.po index 989217ef3..f1d8082af 100644 --- a/modoboa/locale/ru/LC_MESSAGES/django.po +++ b/modoboa/locale/ru/LC_MESSAGES/django.po @@ -646,7 +646,7 @@ msgstr "" "переименованы в Modoboa" #: extensions/admin/app_settings.py:20 -msgid "Mailboxes ower" +msgid "Mailboxes owner" msgstr "Почтовые ящики закончились" #: extensions/admin/app_settings.py:22 diff --git a/modoboa/locale/sv/LC_MESSAGES/django.po b/modoboa/locale/sv/LC_MESSAGES/django.po index 33a7d4ec7..fa777cc99 100644 --- a/modoboa/locale/sv/LC_MESSAGES/django.po +++ b/modoboa/locale/sv/LC_MESSAGES/django.po @@ -657,7 +657,7 @@ msgstr "" "eller tas bort inom Modoboa" #: extensions/admin/app_settings.py:20 -msgid "Mailboxes ower" +msgid "Mailboxes owner" msgstr "Brevlådors ägare" #: extensions/admin/app_settings.py:22 From 965808a706b45854eb785e4ddb5e5dd82fe92155 Mon Sep 17 00:00:00 2001 From: Alexandre Anriot Date: Wed, 13 Aug 2014 10:16:06 +0200 Subject: [PATCH 08/11] - Missing ";" in doc/integration/web_servers.rst. --- doc/integration/web_servers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/integration/web_servers.rst b/doc/integration/web_servers.rst index c70cee60b..eff8e2d5e 100644 --- a/doc/integration/web_servers.rst +++ b/doc/integration/web_servers.rst @@ -227,7 +227,7 @@ Here is a sample nginx configuration:: location / { include uwsgi_params; uwsgi_pass ; - uwsgi_param UWSGI_SCRIPT .wsgi:application + uwsgi_param UWSGI_SCRIPT .wsgi:application; uwsgi_param UWSGI_SCHEME https; } } From 897d55b58e78ccdaf82031bebf2832a9bb4e7733 Mon Sep 17 00:00:00 2001 From: Alexandre Anriot Date: Wed, 13 Aug 2014 11:44:29 +0200 Subject: [PATCH 09/11] bootstrap-datepicker.js -> bootstrap-datetimepicker.js --- modoboa/templates/layout_simple.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modoboa/templates/layout_simple.html b/modoboa/templates/layout_simple.html index a9fdec651..3b60f0242 100644 --- a/modoboa/templates/layout_simple.html +++ b/modoboa/templates/layout_simple.html @@ -7,7 +7,7 @@ - + {% url "django.views.i18n.javascript_catalog" as catalog_url %}