mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Merge branch 'master' into feature/radicale
Conflicts: modoboa/extensions/amavis/app_settings.py tests/install_from_scratch.py
This commit is contained in:
@@ -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": "<notification entry ID>",
|
||||
"url": "<associated URL>",
|
||||
"text": "<text to display>"}
|
||||
|
||||
If your notification needs a counter, you can specify it by adding the
|
||||
two following entries in the dictionary:
|
||||
|
||||
{"counter": <associated counter>,
|
||||
"level": "<info|success|warning|error>"}
|
||||
|
||||
UserLogin
|
||||
=========
|
||||
|
||||
@@ -227,7 +227,7 @@ Here is a sample nginx configuration::
|
||||
location <modoba's root url>/ {
|
||||
include uwsgi_params;
|
||||
uwsgi_pass <uwsgi port>;
|
||||
uwsgi_param UWSGI_SCRIPT <modoboa instance name>.wsgi:application
|
||||
uwsgi_param UWSGI_SCRIPT <modoboa instance name>.wsgi:application;
|
||||
uwsgi_param UWSGI_SCHEME https;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
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.lib import parameters, events
|
||||
|
||||
base_events = [
|
||||
BASE_EVENTS = [
|
||||
"CanCreate",
|
||||
|
||||
"AccountCreated",
|
||||
@@ -51,7 +55,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 +67,22 @@ 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.
|
||||
"""
|
||||
from modoboa.core.utils import new_version_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",
|
||||
}]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
@@ -175,6 +187,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',
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
{% load i18n core_tags lib_tags %}
|
||||
<h2>{% trans "Information" %} <small>{% trans "Various information about Modoboa" %}</small></h2><hr>
|
||||
{% if new_version %}
|
||||
<div class="alert alert-block alert-success">
|
||||
<h4>{% trans "An update is available!" %}</h4>
|
||||
{% blocktrans with version=new_version.version changelog_url=new_version.changelog_url %}
|
||||
Modoboa <strong>{{ version }}</strong> is available for download. The list of all changes is available <a href="{{ changelog_url }}" target="_blank">here</a>.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="alert alert-info">Modoboa <strong>{% get_version %}</strong> {% trans "is currently in use." %}</div>
|
||||
|
||||
<h3>{% trans "Active users" %}</h3>
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Utility functions.
|
||||
"""
|
||||
import os
|
||||
import pkg_resources
|
||||
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
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/")
|
||||
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()
|
||||
version = Version(resp["version"], DEFAULT_NUMBER_BITS)
|
||||
if version <= Version(local_version, DEFAULT_NUMBER_BITS):
|
||||
return None
|
||||
return resp
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -50,12 +50,6 @@ class ParametersForm(AdminParametersForm):
|
||||
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"),
|
||||
initial="no",
|
||||
|
||||
@@ -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 (
|
||||
@@ -53,35 +54,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("""<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
var poller = new Poller("{{ url }}", {
|
||||
interval: {{ interval }},
|
||||
success_cb: function(data) {
|
||||
var $link = $("#nbrequests");
|
||||
var $maincounter = $("#alerts-counter");
|
||||
|
||||
if (data.requests > 0) {
|
||||
$maincounter.html(data.requests);
|
||||
$link.children("span").html(data.requests);
|
||||
$maincounter.closest('div').removeClass('hidden');
|
||||
} else {
|
||||
$maincounter.closest('div').addClass('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
""")
|
||||
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("""<script type="text/javascript">
|
||||
$(document).bind('domform_init', function() {
|
||||
@@ -95,35 +67,27 @@ $(document).bind('domform_init', function() {
|
||||
|
||||
|
||||
@events.observe("TopNotifications")
|
||||
def display_requests(user):
|
||||
from .sql_listing import get_wrapper
|
||||
def check_for_pending_requests(user, include_all):
|
||||
"""
|
||||
Check if release requests are pending.
|
||||
"""
|
||||
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()
|
||||
if not nbrequests:
|
||||
return [{"id": "nbrequests", "counter": 0}] if include_all \
|
||||
else []
|
||||
|
||||
url = reverse("modoboa.extensions.amavis.views.index")
|
||||
url += "#listing/?viewrequests=1"
|
||||
tpl = Template("""<ul class="nav pull-right {{ css }}">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="icon-white icon-bell"></i> <span id="alerts-counter" class="label label-important">{{ nbrequests }}</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a id="nbrequests" href="{{ url }}">
|
||||
<span class="label label-important">{{ nbrequests }}</span> {{ label }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>""")
|
||||
css = "hidden" if nbrequests == 0 else ""
|
||||
return [tpl.render(Context(dict(
|
||||
label=_("Pending requests"), url=url, css=css,
|
||||
nbrequests=nbrequests
|
||||
)))]
|
||||
return [{
|
||||
"id": "nbrequests", "url": url, "text": _("Pending requests"),
|
||||
"counter": nbrequests, "level": "important"
|
||||
}]
|
||||
|
||||
|
||||
def send_amavis_form():
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
"""
|
||||
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):
|
||||
"""Apply 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):
|
||||
"""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):
|
||||
"""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)
|
||||
messages = 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 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.
|
||||
|
||||
: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_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."""
|
||||
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):
|
||||
"""Retrieve the content of a message.
|
||||
"""
|
||||
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 = []
|
||||
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):
|
||||
"""Apply search filters using additional criterias.
|
||||
|
||||
"""
|
||||
if crit == "to":
|
||||
self._where.append(
|
||||
"convert_from(maddr.email, 'UTF8') LIKE '%%%s%%'"
|
||||
% pattern)
|
||||
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],
|
||||
tables=['maddr']
|
||||
)
|
||||
return qset.all()[0]
|
||||
|
||||
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",
|
||||
"convert_from(maddr.email, 'UTF8') ~ '%s'" % regexp],
|
||||
tables=['maddr']
|
||||
)
|
||||
|
||||
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)
|
||||
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):
|
||||
"""Retrieve the content of a message."""
|
||||
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)
|
||||
@@ -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])
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@ urlpatterns = patterns(
|
||||
(r'^listing/$', '_listing'),
|
||||
(r'^getmailcontent/(?P<mail_id>[\w\-\+]+)/$', 'getmailcontent'),
|
||||
(r'^process/$', 'process'),
|
||||
(r'^nbrequests/$', 'nbrequests'),
|
||||
(r'^delete/(?P<mail_id>[\w\-\+]+)/$', 'delete'),
|
||||
(r'^release/(?P<mail_id>[\w\-\+]+)/$', 'release'),
|
||||
(r'^(?P<mail_id>[\w\-\+]+)/$', 'viewmail'),
|
||||
|
||||
@@ -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("""
|
||||
<iframe src="{{ url }}" id="mailcontent"></iframe>
|
||||
@@ -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
|
||||
@@ -289,9 +265,3 @@ def process(request):
|
||||
if request.POST["action"] == "delete":
|
||||
return delete(request, ids)
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(lambda u: u.group != 'SimpleUsers')
|
||||
def nbrequests(request):
|
||||
result = get_wrapper().get_pending_requests(request.user)
|
||||
return render_to_json_response({'requests': result})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
+10
-5
@@ -9,7 +9,7 @@ import inspect
|
||||
import re
|
||||
|
||||
|
||||
def exec_cmd(cmd, sudo_user=None, **kwargs):
|
||||
def exec_cmd(cmd, sudo_user=None, capture_output=True, **kwargs):
|
||||
"""Execute a shell command.
|
||||
|
||||
Run a command using the current user. Set :keyword:`sudo_user` if
|
||||
@@ -22,10 +22,15 @@ def exec_cmd(cmd, sudo_user=None, **kwargs):
|
||||
"""
|
||||
if sudo_user is not None:
|
||||
cmd = "sudo -u %s %s" % (sudo_user, cmd)
|
||||
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, **kwargs)
|
||||
output = p.communicate()[0]
|
||||
return p.returncode, output
|
||||
kwargs["shell"] = True
|
||||
if capture_output:
|
||||
kwargs.update(stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
p = subprocess.Popen(cmd, **kwargs)
|
||||
output = p.communicate()[0]
|
||||
return p.returncode, output
|
||||
p = subprocess.Popen(cmd, **kwargs)
|
||||
p.wait()
|
||||
return p.returncode, None
|
||||
|
||||
|
||||
def guess_extension_name():
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Top notifications poller.
|
||||
*/
|
||||
|
||||
var TopNotifications = function(options) {
|
||||
this.initialize(options);
|
||||
};
|
||||
|
||||
TopNotifications.prototype = {
|
||||
constructor: TopNotifications,
|
||||
|
||||
defaults: {
|
||||
container_id: "#top_notifications",
|
||||
global_counter_id: "#alerts-counter",
|
||||
url: null,
|
||||
interval: 1000
|
||||
},
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
initialize: function(options) {
|
||||
this.options = $.extend({}, this.defaults, options);
|
||||
this.poller = new Poller(this.options.url, {
|
||||
interval: this.options.interval,
|
||||
success_cb: $.proxy(this.update, this)
|
||||
});
|
||||
this.$container = $(this.options.container_id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Build a notification entry content.
|
||||
*/
|
||||
build_link_content: function(data) {
|
||||
var content = "";
|
||||
|
||||
if (data.counter) {
|
||||
content = "<span class='label label-" +
|
||||
data.level + "'>" + data.counter + "</span> " +
|
||||
data.text;
|
||||
} else {
|
||||
content = "<i class='icon-info-sign'></i> " + data.text;
|
||||
}
|
||||
return content;
|
||||
},
|
||||
|
||||
/**
|
||||
* Append an entry to the notification list.
|
||||
*/
|
||||
append_entry: function(data) {
|
||||
if ((data.counter !== undefined && data.counter === 0) ||
|
||||
(data.counter === undefined && data.text === undefined)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $li = $("<li />");
|
||||
|
||||
$li.append(
|
||||
$("<a />", {
|
||||
id: data.id, href: data.url,
|
||||
html: this.build_link_content(data)
|
||||
})
|
||||
);
|
||||
this.$container.find(".dropdown-menu").append($li);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an existing entry in the notification list.
|
||||
*/
|
||||
update_entry: function(data) {
|
||||
var $link = $("#" + data.id);
|
||||
|
||||
if ((data.counter !== undefined && data.counter === 0) ||
|
||||
(data.counter === undefined && data.text === undefined)) {
|
||||
$link.parent().remove();
|
||||
return;
|
||||
}
|
||||
$link.html(this.build_link_content(data));
|
||||
},
|
||||
|
||||
/**
|
||||
* Poller callback.
|
||||
*/
|
||||
update: function(data) {
|
||||
for (var cpt = 0; cpt < data.length; cpt++) {
|
||||
var $link = $("#" + data[cpt].id);
|
||||
if (!$link.length) {
|
||||
this.append_entry(data[cpt]);
|
||||
} else {
|
||||
this.update_entry(data[cpt]);
|
||||
}
|
||||
}
|
||||
var $entries = this.$container.find(".dropdown-menu li");
|
||||
if ($entries.length) {
|
||||
this.$container.removeClass("hidden");
|
||||
$(this.options.global_counter_id).html($entries.length);
|
||||
} else {
|
||||
this.$container.addClass("hidden");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<ul id="top_notifications" class="nav pull-right{% if not top_notifications %} hidden{% endif %}">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="icon-white icon-bell"></i> <span id="alerts-counter" class="label label-important">{{ top_notifications|length }}</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
{% for notification in top_notifications %}
|
||||
<li>
|
||||
<a id="{{ notification.id }}" href="{{ notification.url }}">
|
||||
{% if notification.counter %}
|
||||
<span class="label label-{{ notification.level }}">{{ notification.counter }}</span> {{ notification.text|safe }}
|
||||
{% else %}
|
||||
<i class="icon-info-sign"></i> {{ notification.text|safe }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/bootstrap.css" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/bootstrap-responsive.css" />
|
||||
<script src="{{ STATIC_URL }}js/bootstrap.js" type="text/javascript"></script>
|
||||
<script src="{{ STATIC_URL }}js/bootstrap-datepicker.js" type="text/javascript"></script>
|
||||
<script src="{{ STATIC_URL }}js/bootstrap-datetimepicker.js" type="text/javascript"></script>
|
||||
{% url "django.views.i18n.javascript_catalog" as catalog_url %}
|
||||
<script type="text/javascript" src="{{ catalog_url }}"></script>
|
||||
<style>
|
||||
|
||||
@@ -20,10 +20,19 @@
|
||||
<script src="{{ STATIC_URL }}js/confirmation.js" type="text/javascript"></script>
|
||||
<script src="{{ STATIC_URL }}js/notify.js" type="text/javascript"></script>
|
||||
<script src="{{ STATIC_URL }}js/poller.js" type="text/javascript"></script>
|
||||
<script src="{{ STATIC_URL }}js/top_notifications.js" type="text/javascript"></script>
|
||||
{% url "django.views.i18n.javascript_catalog" as catalog_url %}
|
||||
<script type="text/javascript" src="{{ catalog_url }}"></script>
|
||||
<script type="text/javascript">
|
||||
var top_notifications;
|
||||
|
||||
set_static_url("{{ STATIC_URL }}");
|
||||
$(document).ready(function() {
|
||||
top_notifications = new TopNotifications({
|
||||
url: "{% url 'check_top_notifications' %}",
|
||||
interval: {{ notifications_check_interval }}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% extra_static_content "top" user %}
|
||||
{% block html_head %}{% endblock %}
|
||||
@@ -48,7 +57,7 @@
|
||||
<ul class="nav pull-right">
|
||||
{% if selection %}{% user_menu user selection %}{% else %}{% user_menu user "" %}{% endif %}
|
||||
</ul>
|
||||
<div class="pull-right">{% load_notifications user %}</div>
|
||||
<div class="pull-right">{% include "common/top_notifications.html" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,3 +10,5 @@ argparse
|
||||
rfc6266
|
||||
factory-boy<2.4
|
||||
passlib
|
||||
-e git+https://github.com/tonioo/django-versionfield.git#egg=django-versionfield
|
||||
requests
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import os
|
||||
import tempfile
|
||||
import pexpect
|
||||
import shutil
|
||||
import unittest
|
||||
|
||||
from modoboa.lib.sysutils import exec_cmd
|
||||
|
||||
|
||||
@@ -20,7 +20,11 @@ class DeployTest(unittest.TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
path = os.path.join(self.workdir, self.projname)
|
||||
code, output = exec_cmd("python manage.py test core lib admin limits postfix_relay_domains radicale", cwd=path)
|
||||
code, output = exec_cmd(
|
||||
"python manage.py test core lib admin limits postfix_relay_domains radicale",
|
||||
capture_output=False,
|
||||
cwd=path
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
|
||||
def test_standard(self):
|
||||
|
||||
Reference in New Issue
Block a user