Remove amavis extension.

This commit is contained in:
Antoine Nguyen
2015-02-11 09:54:58 +01:00
parent 6bce1a559f
commit fe8030ac87
35 changed files with 0 additions and 3320 deletions
-162
View File
@@ -1,162 +0,0 @@
# coding: utf-8
from django import forms
from django.utils.translation import ugettext_lazy as _
from modoboa.lib.form_utils import SeparatorField, YesNoField, InlineRadioSelect
from modoboa.lib.parameters import AdminParametersForm, UserParametersForm
class ParametersForm(AdminParametersForm):
"""Extension settings."""
app = "amavis"
qsettings_sep = SeparatorField(label=_("Quarantine settings"))
max_messages_age = forms.IntegerField(
label=_("Maximum message age"),
initial=14,
help_text=_(
"Quarantine messages maximum age (in days) before deletion"
)
)
sep1 = SeparatorField(label=_("Messages releasing"))
released_msgs_cleanup = YesNoField(
label=_("Remove released messages"),
initial="no",
help_text=_("Remove messages marked as released while cleaning up "
"the database")
)
am_pdp_mode = forms.ChoiceField(
label=_("Amavis connection mode"),
choices=[("inet", "inet"), ("unix", "unix")],
initial="unix",
help_text=_("Mode used to access the PDP server"),
widget=InlineRadioSelect(attrs={"type": "checkbox"})
)
am_pdp_host = forms.CharField(
label=_("PDP server address"),
initial="localhost",
help_text=_("PDP server address (if inet mode)"),
widget=forms.TextInput(attrs={"class": "form-control"})
)
am_pdp_port = forms.IntegerField(
label=_("PDP server port"),
initial=9998,
help_text=_("PDP server port (if inet mode)")
)
am_pdp_socket = forms.CharField(
label=_("PDP server socket"),
initial="/var/amavis/amavisd.sock",
help_text=_("Path to the PDP server socket (if unix mode)")
)
user_can_release = YesNoField(
label=_("Allow direct release"),
initial="no",
help_text=_("Allow users to directly release their messages")
)
self_service = YesNoField(
label=_("Enable self-service mode"),
initial="no",
help_text=_("Activate the 'self-service' mode")
)
notifications_sender = forms.EmailField(
label=_("Notifications sender"),
initial="notification@modoboa.org",
help_text=_("The e-mail address used to send notitications")
)
lsep = SeparatorField(label=_("Manual learning"))
manual_learning = YesNoField(
label=_("Enable manual learning"),
initial="yes",
help_text=_(
"Allow super administrators to manually train Spamassassin"
)
)
sa_is_local = YesNoField(
label=_("Is Spamassassin local?"),
initial="yes",
help_text=_(
"Tell if Spamassassin is running on the same server than modoboa"
)
)
default_user = forms.CharField(
label=_("Default user"),
initial="amavis",
help_text=_(
"Name of the user owning the default bayesian database"
)
)
spamd_address = forms.CharField(
label=_("Spamd address"),
initial="127.0.0.1",
help_text=_("The IP address where spamd can be reached")
)
spamd_port = forms.IntegerField(
label=_("Spamd port"),
initial=783,
help_text=_("The TCP port spamd is listening on")
)
domain_level_learning = YesNoField(
label=_("Enable per-domain manual learning"),
initial="no",
help_text=_(
"Allow domain administrators to train Spamassassin "
"(within dedicated per-domain databases)"
)
)
user_level_learning = YesNoField(
label=_("Enable per-user manual learning"),
initial="no",
help_text=_(
"Allow simple users to personally train Spamassassin "
"(within a dedicated database)"
)
)
visibility_rules = {
"am_pdp_host": "am_pdp_mode=inet",
"am_pdp_port": "am_pdp_mode=inet",
"am_pdp_socket": "am_pdp_mode=unix",
"sa_is_local": "manual_learning=yes",
"default_user": "manual_learning=yes",
"spamd_address": "sa_is_local=no",
"spamd_port": "sa_is_local=no",
"domain_level_learning": "manual_learning=yes",
"user_level_learning": "manual_learning=yes"
}
class UserSettings(UserParametersForm):
"""Per-user settings."""
app = "amavis"
dsep = SeparatorField(label=_("Display"))
messages_per_page = forms.IntegerField(
initial=40,
label=_("Number of displayed emails per page"),
help_text=_("Set the maximum number of messages displayed in a page")
)
-30
View File
@@ -1,30 +0,0 @@
class AmavisRouter(object):
"""A router to control all database operations on models in
the amavis application"""
def db_for_read(self, model, **hints):
"Point all operations on amavis models to 'amavis'"
if model._meta.app_label == 'amavis':
return 'amavis'
return None
def db_for_write(self, model, **hints):
"Point all operations on amavis models to 'amavis'"
if model._meta.app_label == 'amavis':
return 'amavis'
return None
def allow_relation(self, obj1, obj2, **hints):
"Allow any relation if a model in amavis is involved"
if obj1._meta.app_label == 'amavis' \
or obj2._meta.app_label == 'amavis':
return True
return None
def allow_syncdb(self, db, model):
"Make sure the amavis app only appears on the 'amavis' db"
if db == 'amavis':
return model._meta.app_label == 'amavis'
elif model._meta.app_label == 'amavis':
return False
return None
@@ -1 +0,0 @@
[{"pk": 1, "model": "amavis.users", "fields": {"priority": 7, "policy": 1, "email": "@demo.local", "fullname": "demo.local"}}]
-97
View File
@@ -1,97 +0,0 @@
# coding: utf-8
"""
Amavis forms.
"""
from django import forms
from django.utils.translation import ugettext as _
from modoboa.lib import parameters
from modoboa.lib.form_utils import InlineRadioSelect
from .models import Policy, Users
class DomainPolicyForm(forms.ModelForm):
spam_subject_tag2_act = forms.BooleanField()
class Meta:
model = Policy
fields = ('bypass_virus_checks', 'bypass_spam_checks',
'spam_tag2_level', 'spam_subject_tag2',
'spam_kill_level', 'bypass_banned_checks')
widgets = {
'bypass_virus_checks': InlineRadioSelect(),
'bypass_spam_checks': InlineRadioSelect(),
'spam_tag2_level': forms.TextInput(
attrs={'class': 'form-control'}),
'spam_kill_level': forms.TextInput(
attrs={'class': 'form-control'}),
'spam_subject_tag2': forms.TextInput(
attrs={'class': 'form-control'}),
'bypass_banned_checks': InlineRadioSelect(),
}
def __init__(self, *args, **kwargs):
if "instance" in kwargs:
self.domain = kwargs["instance"]
try:
policy = Users.objects.get(
email="@%s" % self.domain.name).policy
kwargs["instance"] = policy
except (Users.DoesNotExist, Policy.DoesNotExist):
del kwargs["instance"]
super(DomainPolicyForm, self).__init__(*args, **kwargs)
for field in self.fields.keys():
self.fields[field].required = False
def save(self, user, commit=True):
policy = super(DomainPolicyForm, self).save(commit=False)
for field in ['bypass_spam_checks', 'bypass_virus_checks',
'bypass_banned_checks']:
if getattr(policy, field) == '':
setattr(policy, field, None)
if self.cleaned_data['spam_subject_tag2_act']:
policy.spam_subject_tag2 = None
if commit:
policy.save()
try:
u = Users.objects.get(fullname=policy.policy_name)
except Users.DoesNotExist:
u = Users.objects.get(email="@%s" % self.domain.name)
u.policy = policy
policy.save()
return policy
class LearningRecipientForm(forms.Form):
"""A form to select the recipient of a learning request."""
recipient = forms.ChoiceField(
label=None, choices=[]
)
ltype = forms.ChoiceField(
label="", choices=[("spam", "spam"), ("ham", "ham")],
widget=forms.widgets.HiddenInput
)
selection = forms.CharField(
label="", widget=forms.widgets.HiddenInput)
def __init__(self, user, *args, **kwargs):
"""Constructor."""
super(LearningRecipientForm, self).__init__(*args, **kwargs)
choices = []
if user.group == "SuperAdmins":
choices.append(("global", _("Global database")))
domain_level_learning = parameters.get_admin(
"DOMAIN_LEVEL_LEARNING") == "yes"
user_level_learning = parameters.get_admin(
"USER_LEVEL_LEARNING") == "yes"
if domain_level_learning:
choices.append(("domain", _("Domain's database")))
if user_level_learning:
choices.append(("user", _("User's database")))
self.fields["recipient"].choices = choices
@@ -1,211 +0,0 @@
"""General event callbacks."""
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 (
create_user_and_policy, update_user_and_policy, delete_user_and_policy,
create_user_and_use_policy, delete_user
)
from .lib import manual_learning_enabled
from .models import Policy, Users
def create_relay_domains_records():
"""Create records for existing relay domains."""
from modoboa.extensions.postfix_relay_domains.models import RelayDomain
for rdom in RelayDomain.objects.all():
policy = create_user_and_policy("@{0}".format(rdom.name))
for rdomalias in rdom.relaydomainalias_set.all():
rdomalias_pattern = "@{0}".format(rdomalias.name)
create_user_and_use_policy(rdomalias_pattern, policy)
@events.observe("InitialDataLoaded")
def load_relay_domains_dependant_data(name):
"""Load relay domains dependant data."""
if name != "postfix_relay_domains":
return
create_relay_domains_records()
@events.observe("UserMenuDisplay")
def menu(target, user):
if target == "top_menu":
return [
{"name": "quarantine",
"label": _("Quarantine"),
"url": reverse('amavis:index')}
]
return []
@events.observe("DomainCreated")
def on_domain_created(user, domain):
create_user_and_policy("@{0}".format(domain.name))
@events.observe("DomainModified")
def on_domain_modified(domain):
update_user_and_policy(
"@{0}".format(domain.oldname),
"@{0}".format(domain.name)
)
@events.observe("DomainDeleted")
def on_domain_deleted(domain):
delete_user_and_policy("@{0}".format(domain.name))
@events.observe("DomainAliasCreated")
def on_domain_alias_created(user, domainalias):
create_user_and_use_policy(
"@{0}".format(domainalias.name),
"@{0}".format(domainalias.target.name)
)
@events.observe("DomainAliasDeleted")
def on_domain_alias_deleted(domainaliases):
if isinstance(domainaliases, DomainAlias):
domainaliases = [domainaliases]
for domainalias in domainaliases:
delete_user("@{0}".format(domainalias.name))
@events.observe("MailboxModified")
def on_mailbox_modified(mailbox):
"""Update amavis records if address has changed."""
if parameters.get_admin("MANUAL_LEARNING") == "no" or \
not hasattr(mailbox, "old_full_address") or \
mailbox.full_address == mailbox.old_full_address:
return
user = Users.objects.select_related.get(email=mailbox.old_full_address)
full_address = mailbox.full_address
user.email = full_address
user.policy.policy_name = full_address[:32]
user.policy.sa_username = full_address
user.policy.save()
user.save()
@events.observe("MailboxDeleted")
def on_mailbox_deleted(mailbox):
"""Clean amavis database when a mailbox is removed."""
if parameters.get_admin("MANUAL_LEARNING") == "no":
return
delete_user_and_policy("@{0}".format(mailbox.full_address))
@events.observe("MailboxAliasCreated")
def on_mailboxalias_created(user, alias):
"""Create amavis record for the new alias.
FIXME: how to deal with distibution lists ?
"""
if not manual_learning_enabled(user) or alias.type != "alias":
return
try:
policy = Policy.objects.get(
policy_name=alias.mboxes.all()[0].full_address
)
except Policy.DoesNotExist:
return
else:
email = alias.full_address
Users.objects.create(
email=email, policy=policy, fullname=email, priority=7
)
@events.observe("MailboxAliasDeleted")
def on_mailboxalias_deleted(alias):
"""Clean amavis database when an alias is removed."""
if parameters.get_admin("MANUAL_LEARNING") == "no":
return
if Users.objects.filter(email=alias.full_address).exists():
Users.objects.delete(email=alias.full_address)
@events.observe("GetStaticContent")
def extra_static_content(caller, st_type, user):
if user.group == "SimpleUsers" or st_type != "js":
return []
if caller == 'domains':
tpl = Template("""<script type="text/javascript">
$(document).bind('domform_init', function() {
activate_widget.call($('#id_spam_subject_tag2_act'));
});
</script>
""")
return [tpl.render(Context({}))]
return []
@events.observe("TopNotifications")
def check_for_pending_requests(request, include_all):
"""
Check if release requests are pending.
"""
from .sql_connector import get_connector
if parameters.get_admin("USER_CAN_RELEASE") == "yes" \
or request.user.group == "SimpleUsers":
return []
nbrequests = get_connector(user=request.user).get_pending_requests()
if not nbrequests:
return [{"id": "nbrequests", "counter": 0}] if include_all \
else []
url = reverse("amavis:index")
url += "#listing/?viewrequests=1"
return [{
"id": "nbrequests", "url": url, "text": _("Pending requests"),
"counter": nbrequests, "level": "danger"
}]
def send_amavis_form():
"""
"""
from .forms import DomainPolicyForm
return [{
'id': 'amavis', 'title': _("Content filter"), 'cls': DomainPolicyForm,
'formtpl': 'amavis/domain_content_filter.html'
}]
@events.observe("ExtraDomainForm")
def extra_domain_form(user, domain):
if not user.has_perm("admin.view_domains"):
return []
return send_amavis_form()
@events.observe('ExtraRelayDomainForm')
def extra_relaydomain_form(user, rdomain):
if not user.has_perm("postfix_relay_domains.add_relaydomain"):
return []
return send_amavis_form()
@events.observe("FillDomainInstances")
def fill_domain_instances(user, domain, instances):
if not user.has_perm("admin.view_domains"):
return
instances["amavis"] = domain
@events.observe("FillRelayDomainInstances")
def fill_relaydomain_instances(user, rdomain, instances):
if not user.has_perm("postfix_relay_domains.add_relaydomain"):
return
instances["amavis"] = rdomain
-345
View File
@@ -1,345 +0,0 @@
# coding: utf-8
from functools import wraps
import re
import socket
import string
import struct
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
from modoboa.extensions.admin.models import Mailbox, Alias
from modoboa.lib import parameters
from modoboa.lib.email_utils import split_mailbox
from modoboa.lib.exceptions import InternalError
from modoboa.lib.sysutils import exec_cmd
from modoboa.lib.web_utils import NavigationParameters
from .models import Users, Policy
def selfservice(ssfunc=None):
"""Decorator used to expose views to the 'self-service' feature
The 'self-service' feature allows users to act on quarantined
messages without beeing authenticated.
This decorator only acts as a 'router'.
:param ssfunc: the function to call if the 'self-service'
pre-requisites are satisfied
"""
def decorator(f):
@wraps(f)
def wrapped_f(request, *args, **kwargs):
if request.user.is_authenticated():
return f(request, *args, **kwargs)
if parameters.get_admin("SELF_SERVICE") == "no":
from django.contrib.auth.views import redirect_to_login
return redirect_to_login(
reverse("amavis:index")
)
return ssfunc(request, *args, **kwargs)
return wrapped_f
return decorator
class AMrelease(object):
def __init__(self):
mode = parameters.get_admin("AM_PDP_MODE")
try:
if mode == "inet":
host = parameters.get_admin('AM_PDP_HOST')
port = parameters.get_admin('AM_PDP_PORT')
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((host, int(port)))
else:
path = parameters.get_admin('AM_PDP_SOCKET')
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.connect(path)
except socket.error, err:
raise InternalError(
_("Connection to amavis failed: %s" % str(err))
)
def decode(self, answer):
def repl(match):
return struct.pack("B", string.atoi(match.group(0)[1:], 16))
return re.sub(r"%([0-9a-fA-F]{2})", repl, answer)
def __del__(self):
self.sock.close()
def sendreq(self, mailid, secretid, recipient, *others):
self.sock.send("""request=release
mail_id=%s
secret_id=%s
quar_type=Q
recipient=%s
""" % (mailid, secretid, recipient))
answer = self.sock.recv(1024)
answer = self.decode(answer)
if re.search(r"250 [\d\.]+ Ok", answer):
return True
return False
class SpamassassinClient(object):
"""A stupid spamassassin client."""
def __init__(self, user, recipient_db):
"""Constructor."""
self._sa_is_local = parameters.get_admin("SA_IS_LOCAL")
self._default_username = parameters.get_admin("DEFAULT_USER")
self._recipient_db = recipient_db
self._setup_cache = {}
self._username_cache = []
if user.group == "SimpleUsers":
user_level_learning = parameters.get_admin("USER_LEVEL_LEARNING")
if user_level_learning == "yes":
self._username = user.email
else:
self._username = None
self.error = None
if self._sa_is_local == "yes":
self._learn_cmd = "sa-learn --{0} --no-sync -u {1}"
self._learn_cmd_kwargs = {}
self._expected_exit_codes = [0]
else:
self._learn_cmd = "spamc -d {0} -p {1}".format(
parameters.get_admin("SPAMD_ADDRESS"),
parameters.get_admin("SPAMD_PORT")
)
self._learn_cmd += " -L {0} -u {1}"
self._learn_cmd_kwargs = {}
self._expected_exit_codes = [5, 6]
def _get_mailbox_from_rcpt(self, rcpt):
"""Retrieve a mailbox from a recipient address."""
local_part, domname = split_mailbox(rcpt)
try:
mailbox = Mailbox.objects.select_related("domain").get(
address=local_part, domain__name=domname)
except Mailbox.DoesNotExist:
try:
alias = Alias.objects.select_related("domain").get(
address=local_part, domain__name=domname)
except Alias.DoesNotExist:
raise InternalError(_("No recipient found"))
if alias.type != "alias":
return None
mailbox = alias.mboxes.all()[0]
return mailbox
def _learn(self, rcpt, msg, mtype):
"""Internal method to call the learning command."""
if self._username is None:
if self._recipient_db == "global":
username = self._default_username
else:
mbox = self._get_mailbox_from_rcpt(rcpt)
if mbox is None:
username = self._default_username
if self._recipient_db == "domain":
username = mbox.domain.name
if username not in self._setup_cache and \
setup_manual_learning_for_domain(mbox.domain):
self._setup_cache[username] = True
else:
username = mbox.full_address
if username not in self._setup_cache and \
setup_manual_learning_for_mbox(mbox):
self._setup_cache[username] = True
else:
username = self._username
if username not in self._username_cache:
self._username_cache.append(username)
cmd = self._learn_cmd.format(mtype, username)
code, output = exec_cmd(cmd, pinput=msg, **self._learn_cmd_kwargs)
if code in self._expected_exit_codes:
return True
self.error = output
return False
def learn_spam(self, rcpt, msg):
"""Learn new spam."""
return self._learn(rcpt, msg, "spam")
def learn_ham(self, rcpt, msg):
"""Learn new ham."""
return self._learn(rcpt, msg, "ham")
def done(self):
"""Call this method at the end of the processing."""
if self._sa_is_local:
for username in self._username_cache:
exec_cmd("sa-learn -u {0} --sync".format(username),
**self._learn_cmd_kwargs)
class QuarantineNavigationParameters(NavigationParameters):
"""
Specific NavigationParameters subclass for the quarantine.
"""
def __init__(self, request):
super(QuarantineNavigationParameters, self).__init__(
request, 'quarantine_navparams'
)
self.parameters += [
('pattern', '', False),
('criteria', 'from_addr', False),
('msgtype', None, False),
('viewrequests', None, False)
]
def _store_page(self):
"""Specific method to store the current page."""
if self.request.GET.get("reset_page", None) or "page" not in self:
self["page"] = 1
else:
page = self.request.GET.get("page", None)
if page is not None:
self["page"] = int(page)
def back_to_listing(self):
"""Return the current listing URL.
Looks into the user's session and the current request to build
the URL.
:return: a string
"""
url = "listing"
params = []
navparams = self.request.session[self.sessionkey]
if "page" in navparams:
params += ["page=%s" % navparams["page"]]
if "order" in navparams:
params += ["sort_order=%s" % navparams["order"]]
params += ["%s=%s" % (p[0], navparams[p[0]])
for p in self.parameters if p[0] in navparams]
if params:
url += "?%s" % ("&".join(params))
return url
def create_user_and_policy(name, priority=7):
"""Create records.
Create two records (a user and a policy) using :keyword:`name` as
an identifier.
:param str name: name
:return: the new ``Policy`` object
"""
if Users.objects.filter(email=name).exists():
return Policy.objects.get(policy_name=name[:32])
policy = Policy.objects.create(policy_name=name[:32])
Users.objects.create(
email=name, fullname=name, priority=priority, policy=policy
)
return policy
def create_user_and_use_policy(name, policy, priority=7):
"""Create a *users* record and use an existing policy.
:param str name: user record name
:param str policy: string or Policy instance
"""
if isinstance(policy, basestring):
policy = Policy.objects.get(policy_name=policy[:32])
Users.objects.get_or_create(
email=name, fullname=name, priority=priority, policy=policy
)
def update_user_and_policy(oldname, newname):
"""Update records.
:param str oldname: old name
:param str newname: new name
"""
if oldname == newname:
return
u = Users.objects.get(email=oldname)
u.email = newname
u.fullname = newname
u.policy.policy_name = newname[:32]
u.policy.save()
u.save()
def delete_user_and_policy(name):
"""Delete records.
:param str name: identifier
"""
try:
u = Users.objects.get(email=name)
except Users.DoesNotExist:
return
u.policy.delete()
u.delete()
def delete_user(name):
"""Delete a *users* record.
:param str name: user record name
"""
try:
Users.objects.get(email=name).delete()
except Users.DoesNotExist:
pass
def manual_learning_enabled(user):
"""Check if manual learning is enabled or not.
Also check for :kw:`user` if necessary.
:return: True if learning is enabled, False otherwise.
"""
manual_learning = parameters.get_admin("MANUAL_LEARNING") == "yes"
if manual_learning and user.group != 'SuperAdmins':
domain_level_learning = parameters.get_admin(
"DOMAIN_LEVEL_LEARNING") == "yes"
user_level_learning = parameters.get_admin(
"USER_LEVEL_LEARNING") == "yes"
if user.has_perm("admin.view_domains"):
manual_learning = domain_level_learning or user_level_learning
else:
manual_learning = user_level_learning
return manual_learning
def setup_manual_learning_for_domain(domain):
"""Setup manual learning if necessary.
:return: True if learning has been setup, False otherwise
"""
if Policy.objects.filter(sa_username=domain.name).exists():
return False
policy = Policy.objects.get(policy_name=domain.name[:32])
policy.sa_username = domain.name
policy.save()
return True
def setup_manual_learning_for_mbox(mbox):
"""Setup manual learning if necessary.
:return: True if learning has been setup, False otherwise
"""
result = False
pname = mbox.full_address[:32]
if not Policy.objects.filter(policy_name=pname).exists():
policy = create_user_and_policy(pname)
for alias in mbox.alias_addresses:
create_user_and_use_policy(alias, policy)
result = True
return result
@@ -1,94 +0,0 @@
#!/usr/bin/env python
# coding: utf-8
from optparse import make_option
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.management.commands import CloseConnectionMixin
from modoboa.core.models import User
from modoboa.lib import parameters
from modoboa.lib.email_utils import sendmail_simple
from modoboa.extensions.admin.models import Domain
from modoboa.extensions.amavis.models import (
Msgrcpt
)
from modoboa.extensions.amavis.sql_connector import get_connector
class Command(BaseCommand, CloseConnectionMixin):
help = 'Amavis notification tool'
sender = None
baseurl = None
listingurl = None
option_list = BaseCommand.option_list + (
make_option("--baseurl", type="string", default=None,
help="The scheme and hostname used to access Modoboa"),
make_option("--smtp_host", type="string", default="localhost",
help="The address of the SMTP server used to send notifications"),
make_option("--smtp_port", type="int", default=25,
help="The listening port of the SMTP server used to send notifications"),
make_option("--verbose", action="store_true",
help="Activate verbose mode")
)
def handle(self, *args, **options):
if options["baseurl"] is None:
raise CommandError("You must provide the --baseurl option")
self.options = options
self.notify_admins_pending_requests()
def send_pr_notification(self, rcpt, reqs):
if self.options["verbose"]:
print "Sending notification to %s" % rcpt
total = reqs.count()
reqs = reqs.all()[:10]
content = render_to_string(
"amavis/notifications/pending_requests.html", dict(
total=total, requests=reqs,
baseurl=self.baseurl, listingurl=self.listingurl
)
)
status, msg = sendmail_simple(
self.sender, rcpt,
subject=_("[modoboa] Pending release requests"),
content=content,
server=self.options["smtp_host"],
port=self.options["smtp_port"]
)
if not status:
print msg
def notify_admins_pending_requests(self):
self.sender = parameters.get_admin("NOTIFICATIONS_SENDER",
app="amavis")
self.baseurl = self.options["baseurl"].strip("/")
self.listingurl = self.baseurl \
+ reverse("amavis:_mail_list") \
+ "?viewrequests=1"
for da in User.objects.filter(groups__name="DomainAdmins"):
if not da.mailbox_set.count():
continue
rcpt = da.mailbox_set.all()[0].full_address
reqs = get_connector().get_domains_pending_requests(
Domain.objects.get_for_admin(da)
)
if reqs.count():
self.send_pr_notification(rcpt, reqs)
reqs = Msgrcpt.objects.filter(rs='p')
if not reqs.count():
if self.options["verbose"]:
print "No release request currently pending"
return
for su in User.objects.filter(is_superuser=True):
if not su.mailbox_set.count():
continue
rcpt = su.mailbox_set.all()[0].full_address
self.send_pr_notification(rcpt, reqs)
@@ -1,68 +0,0 @@
#!/usr/bin/env python
# coding: utf-8
import time
from optparse import make_option
from django.core.management.base import BaseCommand
from modoboa.core.management.commands import CloseConnectionMixin
from modoboa.extensions.amavis.models import (
Msgrcpt, Msgs, Maddr
)
from modoboa.lib import parameters
class Command(BaseCommand, CloseConnectionMixin):
args = ''
help = 'Amavis quarantine cleanup'
option_list = BaseCommand.option_list + (
make_option('--debug',
action='store_true',
default=False,
help='Activate debug output'),
make_option('--verbose',
action='store_true',
default=False,
help='Display informational messages')
)
def __vprint(self, msg):
if not self.verbose:
return
print msg
def handle(self, *args, **options):
if options["debug"]:
import logging
l = logging.getLogger("django.db.backends")
l.setLevel(logging.DEBUG)
l.addHandler(logging.StreamHandler())
self.verbose = options["verbose"]
max_messages_age = int(parameters.get_admin("MAX_MESSAGES_AGE",
app="amavis"))
flags = ['D']
if parameters.get_admin("RELEASED_MSGS_CLEANUP",
app="amavis") == "yes":
flags += ['R']
self.__vprint("Deleting marked messages...")
ids = Msgrcpt.objects.filter(rs__in=flags).values("mail_id").distinct()
for msg in Msgs.objects.filter(mail_id__in=ids):
if not msg.msgrcpt_set.exclude(rs__in=flags).count():
msg.delete()
self.__vprint(
"Deleting messages older than %d days..." % max_messages_age)
limit = int(time.time()) - (max_messages_age * 24 * 3600)
Msgs.objects.filter(time_num__lt=limit).delete()
self.__vprint("Deleting unreferenced e-mail addresses...")
for maddr in Maddr.objects.all():
if not maddr.msgs_set.count() and not maddr.msgrcpt_set.count():
maddr.delete()
self.__vprint("Done.")
@@ -1,187 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='Maddr',
fields=[
('partition_tag', models.IntegerField(unique=True, null=True, blank=True)),
('id', models.BigIntegerField(serialize=False, primary_key=True)),
('email', models.CharField(unique=True, max_length=255)),
('domain', models.CharField(max_length=765)),
],
options={
'db_table': 'maddr',
'managed': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Mailaddr',
fields=[
('id', models.IntegerField(serialize=False, primary_key=True)),
('priority', models.IntegerField()),
('email', models.CharField(unique=True, max_length=255)),
],
options={
'db_table': 'mailaddr',
'managed': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Msgs',
fields=[
('partition_tag', models.IntegerField(null=True, blank=True)),
('mail_id', models.CharField(max_length=12, serialize=False, primary_key=True)),
('secret_id', models.CharField(max_length=12, blank=True)),
('am_id', models.CharField(max_length=60)),
('time_num', models.IntegerField()),
('time_iso', models.CharField(max_length=48)),
('policy', models.CharField(max_length=765, blank=True)),
('client_addr', models.CharField(max_length=765, blank=True)),
('size', models.IntegerField()),
('originating', models.CharField(max_length=3)),
('content', models.CharField(max_length=1, blank=True)),
('quar_type', models.CharField(max_length=1, blank=True)),
('quar_loc', models.CharField(max_length=255, blank=True)),
('dsn_sent', models.CharField(max_length=3, blank=True)),
('spam_level', models.FloatField(null=True, blank=True)),
('message_id', models.CharField(max_length=765, blank=True)),
('from_addr', models.CharField(max_length=765, blank=True)),
('subject', models.CharField(max_length=765, blank=True)),
('host', models.CharField(max_length=765)),
],
options={
'db_table': 'msgs',
'managed': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Msgrcpt',
fields=[
('partition_tag', models.IntegerField(null=True, blank=True)),
('mail', models.ForeignKey(primary_key=True, serialize=False, to='amavis.Msgs')),
('rseqnum', models.IntegerField(default=0)),
('is_local', models.CharField(max_length=3)),
('content', models.CharField(max_length=3)),
('ds', models.CharField(max_length=3)),
('rs', models.CharField(max_length=3)),
('bl', models.CharField(max_length=3, blank=True)),
('wl', models.CharField(max_length=3, blank=True)),
('bspam_level', models.FloatField(null=True, blank=True)),
('smtp_resp', models.CharField(max_length=765, blank=True)),
],
options={
'db_table': 'msgrcpt',
'managed': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Policy',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('policy_name', models.CharField(max_length=32, blank=True)),
('virus_lover', models.CharField(max_length=3, null=True, blank=True)),
('spam_lover', models.CharField(max_length=3, null=True, blank=True)),
('unchecked_lover', models.CharField(max_length=3, null=True, blank=True)),
('banned_files_lover', models.CharField(max_length=3, null=True, blank=True)),
('bad_header_lover', models.CharField(max_length=3, null=True, blank=True)),
('bypass_virus_checks', models.CharField(default=b'', choices=[(b'N', 'yes'), (b'Y', 'no'), (b'', 'default')], max_length=3, help_text="Bypass virus checks or not. Choose 'default' to use global settings.", null=True, verbose_name='Virus filter')),
('bypass_spam_checks', models.CharField(default=b'', choices=[(b'N', 'yes'), (b'Y', 'no'), (b'', 'default')], max_length=3, help_text="Bypass spam checks or not. Choose 'default' to use global settings.", null=True, verbose_name='Spam filter')),
('bypass_banned_checks', models.CharField(default=b'', choices=[(b'N', 'yes'), (b'Y', 'no'), (b'', 'default')], max_length=3, help_text="Bypass banned checks or not. Choose 'default' to use global settings.", null=True, verbose_name='Banned filter')),
('bypass_header_checks', models.CharField(max_length=3, null=True, blank=True)),
('virus_quarantine_to', models.CharField(max_length=192, null=True, blank=True)),
('spam_quarantine_to', models.CharField(max_length=192, null=True, blank=True)),
('banned_quarantine_to', models.CharField(max_length=192, null=True, blank=True)),
('unchecked_quarantine_to', models.CharField(max_length=192, null=True, blank=True)),
('bad_header_quarantine_to', models.CharField(max_length=192, null=True, blank=True)),
('clean_quarantine_to', models.CharField(max_length=192, null=True, blank=True)),
('archive_quarantine_to', models.CharField(max_length=192, null=True, blank=True)),
('spam_tag_level', models.FloatField(null=True, blank=True)),
('spam_tag2_level', models.FloatField(null=True, blank=True)),
('spam_tag3_level', models.FloatField(null=True, blank=True)),
('spam_kill_level', models.FloatField(null=True, blank=True)),
('spam_dsn_cutoff_level', models.FloatField(null=True, blank=True)),
('spam_quarantine_cutoff_level', models.FloatField(null=True, blank=True)),
('addr_extension_virus', models.CharField(max_length=192, null=True, blank=True)),
('addr_extension_spam', models.CharField(max_length=192, null=True, blank=True)),
('addr_extension_banned', models.CharField(max_length=192, null=True, blank=True)),
('addr_extension_bad_header', models.CharField(max_length=192, null=True, blank=True)),
('warnvirusrecip', models.CharField(max_length=3, null=True, blank=True)),
('warnbannedrecip', models.CharField(max_length=3, null=True, blank=True)),
('warnbadhrecip', models.CharField(max_length=3, null=True, blank=True)),
('newvirus_admin', models.CharField(max_length=192, null=True, blank=True)),
('virus_admin', models.CharField(max_length=192, null=True, blank=True)),
('banned_admin', models.CharField(max_length=192, null=True, blank=True)),
('bad_header_admin', models.CharField(max_length=192, null=True, blank=True)),
('spam_admin', models.CharField(max_length=192, null=True, blank=True)),
('spam_subject_tag', models.CharField(max_length=192, null=True, blank=True)),
('spam_subject_tag2', models.CharField(default=None, max_length=192, blank=True, help_text="Modify spam subject using the specified text. Choose 'default' to use global settings.", null=True, verbose_name='Spam marker')),
('spam_subject_tag3', models.CharField(max_length=192, null=True, blank=True)),
('message_size_limit', models.IntegerField(null=True, blank=True)),
('banned_rulenames', models.CharField(max_length=192, null=True, blank=True)),
('disclaimer_options', models.CharField(max_length=192, null=True, blank=True)),
('forward_method', models.CharField(max_length=192, null=True, blank=True)),
('sa_userconf', models.CharField(max_length=192, null=True, blank=True)),
('sa_username', models.CharField(max_length=192, null=True, blank=True)),
],
options={
'db_table': 'policy',
'managed': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Quarantine',
fields=[
('partition_tag', models.IntegerField(null=True, blank=True)),
('mail', models.ForeignKey(primary_key=True, serialize=False, to='amavis.Msgs')),
('chunk_ind', models.IntegerField()),
('mail_text', models.TextField()),
],
options={
'ordering': ['-mail__time_num'],
'db_table': 'quarantine',
'managed': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Users',
fields=[
('id', models.AutoField(serialize=False, primary_key=True)),
('priority', models.IntegerField()),
('email', models.CharField(unique=True, max_length=255)),
('fullname', models.CharField(max_length=765, blank=True)),
],
options={
'db_table': 'users',
'managed': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Wblist',
fields=[
('rid', models.IntegerField(serialize=False, primary_key=True)),
('sid', models.IntegerField(primary_key=True)),
('wb', models.CharField(max_length=30)),
],
options={
'db_table': 'wblist',
'managed': False,
},
bases=(models.Model,),
),
]
-236
View File
@@ -1,236 +0,0 @@
# This is an auto-generated Django model module.
# You'll have to do the following manually to clean this up:
# * Rearrange models' order
# * Make sure each model has one field with primary_key=True
# Feel free to rename the models, but don't rename db_table values
# or field names.
#
# Also note: You'll have to insert the output of
# 'django-admin.py sqlcustom [appname]'into your database.
#
# Original Amavis version : 2.6.2
from django.db import models
from django.utils.translation import ugettext_lazy
class Maddr(models.Model):
partition_tag = models.IntegerField(unique=True, null=True, blank=True)
id = models.BigIntegerField(primary_key=True)
email = models.CharField(unique=True, max_length=255)
domain = models.CharField(max_length=765)
class Meta:
db_table = u'maddr'
managed = False
class Mailaddr(models.Model):
id = models.IntegerField(primary_key=True)
priority = models.IntegerField()
email = models.CharField(unique=True, max_length=255)
class Meta:
db_table = u'mailaddr'
managed = False
class Msgs(models.Model):
partition_tag = models.IntegerField(null=True, blank=True)
mail_id = models.CharField(max_length=12, primary_key=True)
secret_id = models.CharField(max_length=12, blank=True)
am_id = models.CharField(max_length=60)
time_num = models.IntegerField()
time_iso = models.CharField(max_length=48)
sid = models.ForeignKey(Maddr, db_column='sid')
policy = models.CharField(max_length=765, blank=True)
client_addr = models.CharField(max_length=765, blank=True)
size = models.IntegerField()
originating = models.CharField(max_length=3)
content = models.CharField(max_length=1, blank=True)
quar_type = models.CharField(max_length=1, blank=True)
quar_loc = models.CharField(max_length=255, blank=True)
dsn_sent = models.CharField(max_length=3, blank=True)
spam_level = models.FloatField(null=True, blank=True)
message_id = models.CharField(max_length=765, blank=True)
from_addr = models.CharField(max_length=765, blank=True)
subject = models.CharField(max_length=765, blank=True)
host = models.CharField(max_length=765)
class Meta:
db_table = u'msgs'
managed = False
unique_together = ('partition_tag', 'mail_id')
class Msgrcpt(models.Model):
partition_tag = models.IntegerField(null=True, blank=True)
mail = models.ForeignKey(Msgs, primary_key=True)
rid = models.ForeignKey(Maddr, db_column='rid')
rseqnum = models.IntegerField(default=0)
is_local = models.CharField(max_length=3)
content = models.CharField(max_length=3)
ds = models.CharField(max_length=3)
rs = models.CharField(max_length=3)
bl = models.CharField(max_length=3, blank=True)
wl = models.CharField(max_length=3, blank=True)
bspam_level = models.FloatField(null=True, blank=True)
smtp_resp = models.CharField(max_length=765, blank=True)
class Meta:
db_table = u'msgrcpt'
managed = False
unique_together = ("partition_tag", "mail", "rseqnum")
class Policy(models.Model):
policy_name = models.CharField(max_length=32, blank=True)
virus_lover = models.CharField(max_length=3, blank=True, null=True)
spam_lover = models.CharField(max_length=3, blank=True, null=True)
unchecked_lover = models.CharField(max_length=3, blank=True, null=True)
banned_files_lover = models.CharField(max_length=3, blank=True, null=True)
bad_header_lover = models.CharField(max_length=3, blank=True, null=True)
bypass_virus_checks = models.CharField(
ugettext_lazy("Virus filter"), default='', null=True,
choices=(('N', ugettext_lazy('yes')),
('Y', ugettext_lazy('no')),
('', ugettext_lazy('default'))),
max_length=3,
help_text=ugettext_lazy(
"Bypass virus checks or not. Choose 'default' to use global "
"settings."
)
)
bypass_spam_checks = models.CharField(
ugettext_lazy("Spam filter"), default='', null=True,
choices=(('N', ugettext_lazy('yes')),
('Y', ugettext_lazy('no')),
('', ugettext_lazy('default'))),
max_length=3,
help_text=ugettext_lazy(
"Bypass spam checks or not. Choose 'default' to use global "
"settings."
)
)
bypass_banned_checks = models.CharField(
ugettext_lazy("Banned filter"), default='', null=True,
choices=(('N', ugettext_lazy('yes')),
('Y', ugettext_lazy('no')),
('', ugettext_lazy('default'))),
max_length=3,
help_text=ugettext_lazy(
"Bypass banned checks or not. Choose 'default' to use global "
"settings."
)
)
bypass_header_checks = models.CharField(max_length=3, blank=True, null=True)
virus_quarantine_to = models.CharField(
max_length=192,
blank=True,
null=True)
spam_quarantine_to = models.CharField(max_length=192, blank=True, null=True)
banned_quarantine_to = models.CharField(
max_length=192,
blank=True,
null=True)
unchecked_quarantine_to = models.CharField(
max_length=192,
blank=True,
null=True)
bad_header_quarantine_to = models.CharField(
max_length=192,
blank=True,
null=True)
clean_quarantine_to = models.CharField(
max_length=192,
blank=True,
null=True)
archive_quarantine_to = models.CharField(
max_length=192,
blank=True,
null=True)
spam_tag_level = models.FloatField(null=True, blank=True)
spam_tag2_level = models.FloatField(null=True, blank=True)
spam_tag3_level = models.FloatField(null=True, blank=True)
spam_kill_level = models.FloatField(null=True, blank=True)
spam_dsn_cutoff_level = models.FloatField(null=True, blank=True)
spam_quarantine_cutoff_level = models.FloatField(null=True, blank=True)
addr_extension_virus = models.CharField(
max_length=192,
blank=True,
null=True)
addr_extension_spam = models.CharField(
max_length=192,
blank=True,
null=True)
addr_extension_banned = models.CharField(
max_length=192,
blank=True,
null=True)
addr_extension_bad_header = models.CharField(
max_length=192,
blank=True,
null=True)
warnvirusrecip = models.CharField(max_length=3, blank=True, null=True)
warnbannedrecip = models.CharField(max_length=3, blank=True, null=True)
warnbadhrecip = models.CharField(max_length=3, blank=True, null=True)
newvirus_admin = models.CharField(max_length=192, blank=True, null=True)
virus_admin = models.CharField(max_length=192, blank=True, null=True)
banned_admin = models.CharField(max_length=192, blank=True, null=True)
bad_header_admin = models.CharField(max_length=192, blank=True, null=True)
spam_admin = models.CharField(max_length=192, blank=True, null=True)
spam_subject_tag = models.CharField(max_length=192, blank=True, null=True)
spam_subject_tag2 = models.CharField(
ugettext_lazy("Spam marker"), default=None,
max_length=192, blank=True, null=True,
help_text=ugettext_lazy(
"Modify spam subject using the specified text. Choose 'default' to "
"use global settings."
)
)
spam_subject_tag3 = models.CharField(max_length=192, blank=True, null=True)
message_size_limit = models.IntegerField(null=True, blank=True)
banned_rulenames = models.CharField(max_length=192, blank=True, null=True)
disclaimer_options = models.CharField(max_length=192, blank=True, null=True)
forward_method = models.CharField(max_length=192, blank=True, null=True)
sa_userconf = models.CharField(max_length=192, blank=True, null=True)
sa_username = models.CharField(max_length=192, blank=True, null=True)
class Meta:
db_table = u'policy'
managed = False
class Quarantine(models.Model):
partition_tag = models.IntegerField(null=True, blank=True)
mail = models.ForeignKey(Msgs, primary_key=True)
chunk_ind = models.IntegerField()
mail_text = models.BinaryField()
class Meta:
db_table = u'quarantine'
managed = False
ordering = ["-mail__time_num"]
unique_together = ("partition_tag", "mail", "chunk_ind")
class Users(models.Model):
id = models.AutoField(primary_key=True)
priority = models.IntegerField()
policy = models.ForeignKey(Policy)
email = models.CharField(unique=True, max_length=255)
fullname = models.CharField(max_length=765, blank=True)
class Meta:
db_table = u'users'
managed = False
class Wblist(models.Model):
rid = models.IntegerField(primary_key=True)
sid = models.IntegerField(primary_key=True)
wb = models.CharField(max_length=30)
class Meta:
db_table = u'wblist'
managed = False
@@ -1,52 +0,0 @@
# coding: utf-8
"""
Amavis management frontend.
Provides:
* SQL quarantine management
* Per-domain settings
"""
from django.utils.translation import ugettext_lazy
from modoboa.core.extensions import ModoExtension, exts_pool
from modoboa.extensions.admin.models import Domain
from modoboa.lib import parameters
from . import general_callbacks
from .lib import create_user_and_policy, create_user_and_use_policy
class Amavis(ModoExtension):
"""The Amavis extension."""
name = "amavis"
label = "Amavis frontend"
version = "1.0"
description = ugettext_lazy("Simple amavis management frontend")
url = "quarantine"
available_for_topredirection = True
def load(self):
from .app_settings import ParametersForm, UserSettings
parameters.register(ParametersForm, "Amavis")
parameters.register(UserSettings, ugettext_lazy("Quarantine"))
def load_initial_data(self):
"""Create records for existing domains and co."""
for dom in Domain.objects.all():
policy = create_user_and_policy("@{0}".format(dom.name))
for domalias in dom.domainalias_set.all():
domalias_pattern = "@{0}".format(domalias.name)
create_user_and_use_policy(domalias_pattern, policy)
if not exts_pool.is_extension_installed(
"modoboa.extensions.postfix_relay_domains"):
return
general_callbacks.create_relay_domains_records()
exts_pool.register_extension(Amavis)
-312
View File
@@ -1,312 +0,0 @@
"""
SQL connector module.
"""
import datetime
from django.db.models import Q
from modoboa.extensions.admin.models import Domain
from modoboa.lib.db_utils import db_type
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]
if self.user.mailbox_set.count():
rcpts += self.user.mailbox_set.all()[0].alias_addresses
return 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])
return flt & Q(rid__email__regex=regexp)
return flt
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', 'S', 'H']) \
if self.navparams.get('viewrequests', '0') != '1' else Q(rs='p')
flt = 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("mail", "rid").filter(flt)
messages = self._apply_extra_select_filters(messages)
return messages
def messages_count(self):
"""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):
"""Fetch a range of messages from the internal cache.
"""
emails = []
for qm in self.messages[start - 1:stop]:
if qm["rs"] == 'D':
continue
m = {
"from": qm["mail__from_addr"],
"to": qm["rid__email"],
"subject": qm["mail__subject"],
"mailid": qm["mail__mail_id"],
"date": datetime.datetime.fromtimestamp(qm["mail__time_num"]),
"type": qm["content"],
"score": qm["bspam_level"],
"status": qm["rs"]
}
if qm["rs"] in ['', ' ']:
m["class"] = "unseen"
elif qm["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."""
content = "".join([
str(qmail.mail_text)
for qmail in Quarantine.objects.filter(mail=mailid)
])
if isinstance(content, unicode):
content = content.encode("utf-8")
return content
class PgSQLconnector(SQLconnector):
"""
The postgres version.
Make use of ``QuerySet.extra`` and postgres ``convert_from``
function to let the quarantine manager work as expected !
The T4 alias is not a random choice. The generated query was
dumped to found it. Be careful since it can changes with
future Django versions...
"""
def _apply_msgrcpt_filters(self, flt):
"""Return filters based on user's role. """
self._where = []
if self.user.group == 'SimpleUsers':
rcpts = [self.user.email]
if self.user.mailbox_set.count():
rcpts += 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)
return flt
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 '%%{0}%%'".format(
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_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)
-52
View File
@@ -1,52 +0,0 @@
# coding: utf-8
"""
An email representation based on a database record.
"""
from django.template.loader import render_to_string
from modoboa.lib.email_utils import Email
from .sql_connector import get_connector
class SQLemail(Email):
"""The SQL version of the Email class."""
def __init__(self, *args, **kwargs):
super(SQLemail, self).__init__(*args, **kwargs)
fields = ["From", "To", "Cc", "Date", "Subject"]
for f in fields:
label = f
self.headers += [
{"name": label, "value": self.get_header(self.msg, f)}
]
setattr(self, label, self.msg[f])
qreason = self.get_header(self.msg, "X-Amavis-Alert")
self.qtype = ""
self.qreason = ""
if qreason != "":
if ',' in qreason:
self.qtype, self.qreason = qreason.split(',', 1)
elif qreason.startswith("BAD HEADER SECTION "):
# Workaround for amavis <= 2.8.0 :p
self.qtype = "BAD HEADER SECTION"
self.qreason = qreason[19:]
@property
def msg(self):
"""Get message's content."""
import email
if self._msg is None:
mail_text = get_connector().get_mail_content(self.mailid)
self._msg = email.message_from_string(mail_text)
self._parse(self._msg)
return self._msg
def render_headers(self, **kwargs):
return render_to_string("amavis/mailheaders.html", {
"qtype": self.qtype, "qreason": self.qreason,
"headers": self.headers,
})
@@ -1,90 +0,0 @@
@media (max-width: 400px){
th#rstatus,
td[name='rstatus'],
th#type,
td[name='type']{
display: none;
}
}
@media (max-width: 600px){
th#subject,
td[name='subject']{
display: none;
}
}
@media (max-width: 767px){
th#to,
td[name='to']{
display: none;
}
}
@media (max-width: 990px){
#filters_bar,
th#date,
td[name='date']{
display: none;
}
}
#menubar {
position: absolute;
top: 51px;
padding-left: 15px;
padding-right: 15px;
padding-top: 20px;
padding-bottom: 10px;
left: 0;
right: 0;
margin: 0;
z-index: 10;
}
#menubar input[type="checkbox"] {
margin: 0;
}
#listing {
position: absolute;
top: 125px;
left: 0;
right: 0;
bottom: 0;
margin: 0;
overflow: auto;
padding-left: 15px;
padding-right: 15px;
}
#mailcontent {
border: 0;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
#emails tbody tr:hover {
color: #0088cc;
}
thead tr {
position: relative;
z-index: 1;
}
td[name=type] span {
cursor: pointer;
}
td.unseen {
font-weight: bold;
}
td[class*=pending] {
font-style: italic;
}
td[class*=openable] {
cursor: pointer;
}
@@ -1,21 +0,0 @@
#menubar {
padding-left: 15px;
z-index: 10;
}
#listing {
position: absolute;
padding: 0px;
right: 0;
left: 0;
bottom: 0;
overflow: hidden;
}
#mailcontent {
border: 0;
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
@@ -1,489 +0,0 @@
/**
* Return an instance of Quarantine.
*
* @constructor
* @param {Object} options - instance options
*/
var Quarantine = function(options) {
Listing.call(this, options);
};
Quarantine.prototype = {
constructor: Quarantine,
defaults: {
deflocation: "listing",
main_table_id: "emails",
scroll_container: "#listing",
navigation_params: ["sort_order", "pattern", "criteria", "msgtype"],
eor_message: gettext("No more message to show")
},
initialize: function(options) {
this.options = $.extend({}, this.defaults, options);
this.options.defcallback = $.proxy(this.listing_cb, this);
Listing.prototype.initialize.call(this, this.options);
this.register_navcallbacks();
this.listen();
this.set_msgtype();
$(document).on('click', '#selectall', this.toggle_selection);
this.initialize_searchfield();
},
/**
* Search field initialization.
*/
initialize_searchfield: function() {
$("#searchfield").searchbar({
navobj: this.navobj,
pattern_changed: function(navobj) {
navobj.setparam("reset_page", "true");
}
});
},
/**
* Activate the button corresponding to the current message type
* filter. If no message type is found, the 'All' button is
* selected.
*/
set_msgtype: function() {
var msgtype = this.navobj.getparam('msgtype');
$("input[name*=msgtype]").attr("checked", false)
.parent().removeClass('active');
if (msgtype !== undefined) {
$("input[name=msgtype_" + msgtype + "]").attr("checked", true)
.parent().addClass('active');
} else {
$("button[name=msgtype_all]").attr("checked", true)
.parent().addClass('active');
}
},
/**
* Update the main page content.
*
* @this Quarantine
* @param {Object} data - new content
*/
update_page: function(data) {
if (data.menu !== undefined) {
$("#menubar").html(data.menu);
this.initialize_searchfield();
}
if (data.listing !== undefined) {
$("#listing").html(data.listing);
}
if (this.navobj.getbaseurl() == "listing") {
this.update_listing(data);
$(this.options.scroll_container).scrollTop(10);
} else {
this.update_listing(data, false);
}
},
listen: function() {
$(document).on("click", "td[class*=openable]", $.proxy(this.viewmail_loader, this));
$(document).on("click", "a[name=selectmsgs]", $.proxy(this.selectmsgs, this));
$(document).on("click", "a[name=release-multi]",
$.proxy(this.release_selection, this));
$(document).on("click", "a[name=delete-multi]",
$.proxy(this.delete_selection, this));
$(document).on("click", "a[name=mark-as-spam-multi]",
$.proxy(this.mark_selection_as_spam, this));
$(document).on("click", "a[name=mark-as-ham-multi]",
$.proxy(this.mark_selection_as_ham, this));
$(document).on("click", "a[name=viewrequests]",
$.proxy(this.view_requests, this));
$(document).on("click", "a[name=release]", $.proxy(this.release, this));
$(document).on("click", "a[name=delete]", $.proxy(this.delete, this));
$(document).on("click", "a[name=mark-as-spam]",
$.proxy(this.mark_as_spam, this));
$(document).on("click", "a[name=mark-as-ham]",
$.proxy(this.mark_as_ham, this));
$(document).on("click", "a[name=headers]", $.proxy(this.headers, this));
$(document).on("click", "td[name=type] span", $.proxy(this.filter_by_type, this));
$(document).on("click", "#filters label", $.proxy(this.filter_by_type, this));
},
load_page: function(e) {
Listing.prototype.load_page.apply(this, arguments);
this.navobj.delparam("rcpt").update();
},
/**
* Return extra arguments used to fetch a page.
*
* @this Listing
*/
get_load_page_args: function() {
var args = Listing.prototype.get_load_page_args.call(this);
args.scroll = true;
return args;
},
/**
* Calculate the bottom position of the scroll container.
*
* @param {Object} $element - scroll container object
*/
calculate_bottom: function($element) {
var emails_height = $("#emails").height();
if (!emails_height) {
return undefined;
}
return $("#emails").height() - $element.height();
},
view_requests: function(e) {
e.preventDefault();
this.navobj.setparam("viewrequests", "1").update();
},
viewmail_loader: function(e) {
e.preventDefault();
var $tr = $(e.target).parent();
var $to = $tr.find("td[name=to]");
this.navobj.baseurl($tr.attr("id"));
if ($to.length) {
this.navobj.setparam("rcpt", $to.html().trim());
}
this.navobj.update();
},
/*
* Toggle message selection when the top checkbox's state is
* modified.
*
* If it is checked, all messages are selected. Otherwise, the
* current selection is resetted.
*/
toggle_selection: function(e) {
if (!$('#selectall').prop('checked')) {
$("#emails").htmltable("clear_selection");
return;
}
$("td[name*=selection]").each(function() {
var $input = $(this).children('input');
$input.prop('checked', true);
$('#emails').htmltable('select_row', $(this).parent());
});
},
/*
* Select all messages of a specific type.
*/
selectmsgs: function(e) {
e.preventDefault();
var type = get_target(e, 'a').attr("href");
var counter = 0;
$("td[name=type]").each(function() {
var $this = $(this);
if ($this.find('span').html().trim() == type) {
var $input = $this.parent().children('td[name=selection]').children('input');
$input.prop('checked', true);
$('#emails').htmltable('select_row', $this.parent());
counter++;
}
});
if (counter) {
$("#selectall").prop('checked', true);
}
},
/**
* Filter listing by message type (spam, virus, etc.)
*
* @param {Object} evt - event object
*/
filter_by_type: function(evt) {
var $target = $(evt.target);
var msgtype;
if ($target.is("span")) {
msgtype = $target.html().trim();
} else {
msgtype = $target.children("input").attr("name").replace("msgtype_", "");
}
if (msgtype != 'all') {
this.navobj.setparam('msgtype', msgtype);
} else {
this.navobj.delparam('msgtype');
}
this.navobj.update();
},
/**
* Return the list of currently selected messages.
*
* @return {Array} - list of message IDs
*/
get_current_selection: function() {
var selection = [];
this.htmltable.current_selection().each(function() {
var $tr = $(this);
var $to = $tr.find("td[name=to]");
if ($to.length) {
selection.push($to.html().trim() + " " + $tr.attr("id"));
} else {
selection.push($tr.attr("id"));
}
});
return selection;
},
_send_selection: function(e, name, message) {
var $link = $(e.target);
e.preventDefault();
if (!this.htmltable.current_selection().length ||
!confirm(message)) {
return;
}
var selection = this.get_current_selection();
$.ajax({
url: $link.attr("href"),
data: "action=" + name + "&selection=" + selection.join(","),
type: 'POST',
dataType: 'json'
}).done(
$.proxy(this.action_cb, this)
).fail(function(jqxhr) {
var data = $.parseJSON(jqxhr.responseText);
$("body").notify("error", data.message);
});
},
release_selection: function(e) {
this._send_selection(e, "release", gettext("Release this selection?"));
},
delete_selection: function(e) {
this._send_selection(e, "delete", gettext("Delete this selection?"));
},
/**
* Show the recipient selection form inside a modal box.
*
* @param {Object} evt - event object
* @param {string} ltype - learning type (spam or ham)
*/
show_select_rcpt_form: function(evt, ltype, selection) {
if (selection === undefined) {
selection = this.get_current_selection();
}
var url = this.options.learning_recipient_url +
"?type=" + ltype + "&selection=" +
selection.join(",");
var $this = this;
modalbox(evt, null, url, function() {
$(".submit").on("click", function(evt) {
simple_ajax_form_post(evt, {
formid: "learning_recipient_form",
reload_on_success: false,
success_cb: $.proxy($this.action_cb, $this)
});
});
});
},
/**
* Mark the current selection as spam.
*
* @param {Object} e - event object
*/
mark_selection_as_spam: function(e) {
if (this.options.check_learning_rcpt &&
this.htmltable.current_selection().length) {
this.show_select_rcpt_form(e, "spam");
} else {
this._send_selection(
e, "mark_as_spam", gettext("Mark this selection as spam?"));
}
},
/**
* Mark the current selection as ham.
*
* @param {Object} e - event object
*/
mark_selection_as_ham: function(e) {
if (this.options.check_learning_rcpt &&
this.htmltable.current_selection().length) {
this.show_select_rcpt_form(e, "ham");
} else {
this._send_selection(
e, "mark_as_ham", gettext("Mark this selection as non-spam?"));
}
},
_send_action: function(e, message) {
var $link = get_target(e, "a");
e.preventDefault();
if (!confirm(message)) {
return;
}
$.ajax({
url: $link.attr("href"),
dataType: 'json',
type: 'POST',
data: {rcpt: get_parameter_by_name($link.attr("href"), 'rcpt')}
}).done(
$.proxy(this.action_cb, this)
).fail(function(jqxhr) {
var data = $.parseJSON(jqxhr.responseText);
$("body").notify("error", data.message);
});
},
release : function(e) {
this._send_action(e, gettext("Release this message?"));
},
delete: function(e) {
this._send_action(e, gettext("Delete this message?"));
},
/**
* Mark the current message as spam.
*
* @param {Object} e - event object
*/
mark_as_spam: function(e) {
if (this.options.check_learning_rcpt) {
var $link = get_target(e, "a");
var selection = get_parameter_by_name($link.attr("href"), "rcpt") +
" " + $link.attr("data-mail-id");
this.show_select_rcpt_form(e, "spam", [selection]);
} else {
this._send_action(e, gettext("Mark as spam?"));
}
},
/**
* Mark the current message as ham.
*
* @param {Object} e - event object
*/
mark_as_ham: function(e) {
if (this.options.check_learning_rcpt) {
var $link = get_target(e, "a");
var selection = get_parameter_by_name($link.attr("href"), "rcpt") +
" " + $link.attr("data-mail-id");
this.show_select_rcpt_form(e, "ham", [selection]);
} else {
this._send_action(e, gettext("Mark as non-spam?"));
}
},
show_rawheaders: function($mailcontent) {
$mailcontent.find("#rawheaders").removeClass("hidden");
$mailcontent.find("#emailheaders").addClass("hidden");
},
headers: function(e) {
e.preventDefault();
var $link = get_target(e, "a");
var $mailcontent = $("#mailcontent").contents();
var $headers = $mailcontent.find("#emailheaders");
var $rawheaders = $mailcontent.find("#rawheaders");
if ($headers.hasClass("hidden")) {
$link.html(gettext("View full headers"));
$headers.removeClass("hidden");
$rawheaders.addClass("hidden");
return;
}
$link.html(gettext("Hide full headers"));
if (!$rawheaders.length) {
$.ajax({
url: $link.attr("href"),
global: false
}).done($.proxy(function(data) {
$mailcontent.find("#table-container").append($(data));
this.show_rawheaders($mailcontent);
}, this));
return;
}
this.show_rawheaders($mailcontent);
},
update_params: function(e) {
var $link = $(e.target);
e.preventDefault();
this.navobj.updateparams($link.attr("href")).update();
},
register_navcallbacks: function() {
this.navobj.register_callback("_listing",
$.proxy(this.listing_cb, this));
this.navobj.register_callback("viewmail",
$.proxy(this.viewmail_cb, this));
},
activate_buttons: function($tr) {
$("a[name*=-multi]").removeClass('disabled');
$("#selectall").prop('checked', true);
},
deactivate_buttons: function($tr) {
if (!this.htmltable || !this.htmltable.current_selection().length) {
$("a[name*=-multi]").addClass('disabled');
$("#selectall").prop('checked', false);
}
},
/**
* Navigation callback: listing.
*
* @this Quarantine
* @param {Object} data - ajax call response (JSON)
*/
listing_cb: function(data) {
this.update_page(data);
this.navobj.delparam("rcpt").update();
this.set_msgtype();
$("#emails").htmltable({
row_selected_event: this.activate_buttons,
row_unselected_event: $.proxy(this.deactivate_buttons, this)
});
this.htmltable = $("#emails").data("htmltable");
this.deactivate_buttons();
if (this.navobj.hasparam("reset_page")) {
this.navobj.delparam("reset_page").update(false, true);
}
$("#listing").css("overflow", "auto");
},
viewmail_cb: function(data) {
this.update_page(data);
$("#listing").css("overflow", "hidden");
},
action_cb: function(data) {
if (data.reload) {
this.navobj.update(true);
}
if (data.url) {
this.navobj.parse_string(data.url, true).update(true);
}
$("body").notify("success", data.message, 2000);
}
};
Quarantine.prototype = $.extend({}, Listing.prototype, Quarantine.prototype);
@@ -1,30 +0,0 @@
function send_action(e, message) {
var $link = $(e.target);
e.preventDefault();
if (!confirm(message)) {
return;
}
$.ajax({
url: $link.attr("href"),
dataType: 'json'
}).done(function(data) {
$("body").notify("success", data, 2000);
});
}
function release(e) {
send_action(e, gettext("Release this message?"));
}
function remove(e) {
send_action(e, gettext("Delete this message?"));
}
$(document).ready(function() {
$("#listing").css({
top: $("#menubar").outerHeight() + 10
});
$(document).on("click", "a[name=release]", release);
$(document).on("click", "a[name=delete]", remove);
});
@@ -1,18 +0,0 @@
{% load i18n form_tags %}
{% render_field form.bypass_virus_checks %}
{% render_field form.bypass_spam_checks %}
<div class="form-group">
<label class="control-label col-lg-4 col-md-4 col-sm-4">{% trans "Thresholds" %}</label>
<div class="col-lg-2 col-md-2 col-sm-2">
{{ form.spam_tag2_level }}
</div>
<span class="help-block">{% trans "or more is spam" %}</span>
</div>
<div class="form-group">
<div class="col-sm-offset-4 col-lg-2 col-md-2 col-sm-2">
{{ form.spam_kill_level }}
</div>
<span class="help-block">{% trans "or more throw spam message away" %}</span>
</div>
{% render_field form.spam_subject_tag2 activator=True activator_label=_("default") activator_size="col-sm-3" %}
{% render_field form.bypass_banned_checks %}
@@ -1,31 +0,0 @@
{% load i18n %}
<form method="POST" id="listingform">
<table id="emails" class="table table-condensed">
<thead>
<tr>
<th id="selection" width="5px"></th>
<th id="type" width="30px" class="sortable sort-header" align="center" data-sort_order="type"></th>
<th id="rstatus" width="25px"></th>
<th id="score" class="sortable sort-header" data-sort_order="score">
{% trans "Score" %}
</th>
<th id="to" class="sortable sort-header" data-sort_order="to">
{% trans "To" %}
</th>
<th id="from" class="sortable sort-header" data-sort_order="from">
{% trans "From" %}
</th>
<th id="subject" class="sortable sort-header" data-sort_order="subject">
{% trans "Subject" %}
</th>
<th id="date" class="sortable sort-header" data-sort_order="date">
{% trans "Date" %}
</th>
</tr>
</thead>
<tbody>
{% include "amavis/emails_page.html" %}
</tbody>
</table>
</form>
@@ -1,34 +0,0 @@
{% load i18n %}
{% load amavis_tags %}
{% load static %}
{% for email in email_list %}
<tr id="{{ email.mailid }}">
<td name="selection" width="5px">
<input id="selection" type="checkbox" name="select_{{ email.mailid }}" value="{{ email.mailid }}">
</td>
<td align="center" width="30px" name="type" class="{{ email.class }}">
{{ email.type|msgtype_to_html }}
</td>
<td width="25px" name="rstatus">
{% if email.status == 'R' %}
<span class="fa fa-mail-reply" title="{% trans 'Released' %}"></span>
{% elif email.status == 'S' %}
<span class="fa fa-thumbs-down" title="{% trans 'Marked as spam' %}"></span>
{% elif email.status == 'H' %}
<span class="fa fa-thumbs-up" title="{% trans 'Marked as non-spam' %}"></span>
{% endif %}
</td>
<td class="openable {{ email.class }}" name="score">{{ email.score }}</td>
<td class="openable {{ email.class }}" name="to">{{ email.to }}</td>
<td class="openable {{ email.class }}" name="from">
{{ email.from|truncatechars:30 }}
</td>
<td class="openable {{ email.class }}" name="subject">
{{ email.subject|truncatechars:30 }}
</td>
<td class="openable {{ email.class }}" name="date">
{{ email.date|date:"SHORT_DATETIME_FORMAT" }}
</td>
</tr>
{% endfor %}
@@ -1 +0,0 @@
{% if pre %}<pre>{% endif %}{{ body }}{% if pre %}</pre>{% endif %}
@@ -1,37 +0,0 @@
{% extends "fluid.html" %}{% load i18n amavis_tags %}
{% load static %}
{% block pagetitle %}{% trans "Quarantine management" %}{% endblock %}
{% block extra_css %}
<link rel="stylesheet" type="text/css" href="{% static 'amavis/css/quarantine.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'css/jquery.sortable.css' %}" />
{% endblock %}
{% block extra_js %}
<script type="text/javascript" src="{% static 'js/jquery.htmltable.js' %}"></script>
<script type="text/javascript" src="{% static 'js/jquery.infinitescroll.js' %}"></script>
<script type="text/javascript" src="{% static 'js/jquery.sortable.js' %}"></script>
<script type="text/javascript" src="{% static 'js/history.js' %}"></script>
<script type="text/javascript" src="{% static 'js/searchbar.js' %}"></script>
<script type="text/javascript" src="{% static 'js/listing.js' %}"></script>
<script type="text/javascript" src="{% static 'amavis/js/quarantine.js' %}"></script>
<script type="text/javascript">
$(document).ready(function() {
var qm = new Quarantine({
deflocation: "listing/?sort_order=-date&reset_page=true",
defcallback: "listing_cb",
load_page_url: "{% url 'amavis:mail_page' %}",
check_learning_rcpt: {{ check_learning_rcpt }},
learning_recipient_url: "{% url 'amavis:learning_recipient_set' %}"
});
});
</script>
{% endblock %}
{% block container_content %}
<div id="menubar">
{% quar_menu user %}
</div>
<div id="listing"></div>
{% endblock %}
@@ -1,5 +0,0 @@
{% extends "common/mailheaders.html" %}
{% block emailheaders %}
{% if qtype %}<div class="alert alert-danger"><strong>{{ qtype }}</strong> {{ qreason }}</div>{% endif %}
{{ block.super }}
{% endblock %}
@@ -1,79 +0,0 @@
{% load lib_tags i18n %}
<div class="row">
<div class="col-xs-7 col-md-5 col-lg-4">
<div class="btn-group" title="{% trans 'Select' %}">
<a class="btn btn-default"><input type="checkbox" id="selectall" /></a>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a name="selectmsgs" href="C">{% trans "Clean" %}</a></li>
<li><a name="selectmsgs" href="S">{% trans "Spam" %}</a></li>
<li><a name="selectmsgs" href="V">{% trans "Virus" %}</a></li>
<li><a name="selectmsgs" href="H">{% trans "Bad Header" %}</a></li>
<li><a name="selectmsgs" href="M">{% trans "Bad MIME" %}</a></li>
<li><a name="selectmsgs" href="B">{% trans "Banned" %}</a></li>
</ul>
</div>
<div class="btn-group" title="{% trans 'Release' %}">
<a name="release-multi" href="{% url 'amavis:mail_process' %}" class="btn btn-success disabled">
<span class="fa fa-check"></span> {% trans 'Release' %}
</a>
</div>
<div class="btn-group" title="{% trans 'Delete' %}">
<a name="delete-multi" href="{% url 'amavis:mail_process' %}" class="btn btn-danger disabled">
<span class="fa fa-trash"></span> {% trans 'Delete' %}
</a>
</div>
{% if manual_learning %}
<div class="btn-group" title="{% trans 'Manual learning' %}">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span class="fa fa-cog"></span> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a name="mark-as-spam-multi" href="{% url 'amavis:mail_process' %}">{% trans "Mark as spam" %}</a></li>
<li><a name="mark-as-ham-multi" href="{% url 'amavis:mail_process' %}">{% trans "Mark as non-spam" %}</a></li>
</ul>
</div>
{% endif %}
</div>
<div class="col-xs-5 col-lg-4">
{% include "common/email_searchbar.html" with extraopts=extraopts %}
</div>
<div class="hidden-md col-lg-4">
<div class="btn-group">
<div id="filters_bar" class="navbar-nav navbar-right">
<span class="label label-default">{% trans "Display" %}</span>
<div id="filters" class="btn-group" data-toggle="buttons">
<label class="btn btn-default active">
<input type="radio" name="msgtype_all" title="{% trans 'All messages' %}" checked> {% trans "All" %}
</label>
<label class="btn btn-success">
<input type="radio" name="msgtype_C" title="{% trans 'Clean only' %}"> C
</label>
<label class="btn btn-danger">
<input type="radio" name="msgtype_S" title="{% trans 'Spam only' %}"> S
</label>
<label class="btn btn-danger">
<input type="radio" name="msgtype_V" title="{% trans 'Virus only' %}"> V
</label>
<label class="btn btn-warning">
<input type="radio" name="msgtype_H" title="{% trans 'Bad Header only' %}"> H
</label>
<label class="btn btn-warning">
<input type="radio" name="msgtype_M" title="{% trans 'Bad MIME only' %}"> M
</label>
<label class="btn btn-warning">
<input type="radio" name="msgtype_B" title="{% trans 'Banned only' %}"> B
</label>
</div>
</div>
</div>
</div>
</div>
@@ -1,14 +0,0 @@
{% load i18n lib_tags %}{% load url from future %}{% blocktrans count counter=total %}
{{ counter }} release request is pending for action.
{% plural %}
{{ counter }} release requests are pending for action.
{% endblocktrans %}
{% trans "Sketch:" %}
{% for msg in requests %}
{% trans "From:" %} {{ msg.mail.sid.email }}
{% trans "To:" %} {{ msg.rid.email }}
{% trans "Date:" %} {{ msg.mail.time_num|fromunix|date:"DATETIME_FORMAT" }}
{% trans "Subject:" %} {{ msg.mail.subject }}
{% trans "Act on this message:" %} {{ baseurl }}{% url "amavis:index" %}#{{ msg.mail.mail_id }}/?rcpt={{ msg.rid.email }}
{% endfor %}
{% blocktrans %}Please visit {{ listingurl }} for a full listing.{% endblocktrans %}
@@ -1,4 +0,0 @@
<pre id="rawheaders" class="well">
{% for k, v in headers %}{{ k }}: {{ v }}
{% endfor %}
</pre>
@@ -1,28 +0,0 @@
<!DOCTYPE html>{% load i18n lib_tags amavis_tags %}{% load url from future %}
{% load static %}
<html lang="{{ LANGUAGE_CODE }}">
<head>
<meta charset="utf-8" />
<title>Modoboa</title>
<link rel="icon" type="image/png" href="{% static 'css/logo-icon.png' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'css/bootstrap.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'css/bootstrap-responsive.css' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'amavis/css/selfservice.css' %}" />
<script src="{% static 'js/jquery.js' %}" type="text/javascript"></script>
<script src="{% static 'js/bootstrap.js' %}" type="text/javascript"></script>
<script src="{% static 'js/global.js' %}" type="text/javascript"></script>
<script src="{% static 'js/notify.js' %}" type="text/javascript"></script>
<script src="{% static 'amavis/js/selfservice.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">
set_media_url("{{ STATIC_URL }}");
</script>
</head>
<body>
<div id="fixed_content">
<div id="menubar">{% viewm_menu_simple user mail_id rcpt secret_id %}</div>
<div id="listing">{{ content }}</div>
</div>
</body>
</html>
@@ -1,132 +0,0 @@
"""
Amavis frontend template tags.
"""
from django import template
from django.core.urlresolvers import reverse
from django.template.loader import render_to_string
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from ..lib import manual_learning_enabled
register = template.Library()
@register.simple_tag
def viewm_menu(user, mail_id, rcpt):
"""Menu displayed within the viewmail action."""
entries = [
{"name": "back",
"url": "javascript:history.go(-1);",
"img": "fa fa-arrow-left",
"class": "btn-primary",
"label": _("Back")},
{"name": "release",
"img": "fa fa-check",
"class": "btn-success",
"url": reverse('amavis:mail_release', args=[mail_id])
+ ("?rcpt=%s" % rcpt if rcpt else ""),
"label": _("Release")},
{"name": "delete",
"class": "btn-danger",
"img": "fa fa-trash",
"url": reverse('amavis:mail_delete', args=[mail_id])
+ ("?rcpt=%s" % rcpt if rcpt else ""),
"label": _("Delete")},
{"name": "headers",
"class": "btn-default",
"url": reverse('amavis:headers_detail', args=[mail_id]),
"label": _("View full headers")},
]
if manual_learning_enabled(user):
entries.insert(3, {
"name": "process",
"img": "fa fa-cog",
"menu": [
{"name": "mark-as-spam",
"label": _("Mark as spam"),
"url": reverse(
"amavis:mail_mark_as_spam", args=[mail_id]
) + ("?rcpt=%s" % rcpt if rcpt else ""),
"extra_attributes": {
"data-mail-id": mail_id
}},
{"name": "mark-as-ham",
"label": _("Mark as non-spam"),
"url": reverse(
"amavis:mail_mark_as_ham", args=[mail_id]
) + ("?rcpt=%s" % rcpt if rcpt else ""),
"extra_attributes": {
"data-mail-id": mail_id
}}
]
})
menu = render_to_string('common/buttons_list.html',
{"entries": entries, "extraclasses": "pull-left"})
entries = [{"name": "close",
"url": "javascript:history.go(-1);",
"img": "icon-remove"}]
menu += render_to_string(
'common/buttons_list.html',
{"entries": entries, "extraclasses": "pull-right"}
)
return menu
@register.simple_tag
def viewm_menu_simple(user, mail_id, rcpt, secret_id=""):
release_url = "{0}?rcpt={1}".format(
reverse("amavis:mail_release", args=[mail_id]), rcpt)
delete_url = "{0}?rcpt={1}".format(
reverse("amavis:mail_delete", args=[mail_id]), rcpt)
if secret_id:
release_url += "&secret_id={0}".format(secret_id)
delete_url += "&secret_id={0}".format(secret_id)
entries = [
{"name": "release",
"img": "fa fa-check",
"class": "btn-success",
"url": release_url,
"label": _("Release")},
{"name": "delete",
"img": "fa fa-trash",
"class": "btn-danger",
"url": delete_url,
"label": _("Delete")},
]
return render_to_string('common/buttons_list.html',
{"entries": entries})
@register.simple_tag
def quar_menu(user):
"""Render the quarantine listing menu.
:rtype: str
:return: resulting HTML
"""
extraopts = [{"name": "to", "label": _("To")}]
return render_to_string("amavis/main_action_bar.html", {
"extraopts": extraopts,
"manual_learning": manual_learning_enabled(user)
})
@register.filter
def msgtype_to_html(msgtype):
"""Transform a message type to a bootstrap label."""
if msgtype in ["S", "V"]:
color = 'danger'
elif msgtype == "C":
color = "success"
else:
color = "warning"
return mark_safe(
'<span class="label label-%s">%s</span>' % (color, msgtype)
)
-24
View File
@@ -1,24 +0,0 @@
from django.conf.urls import patterns, url
urlpatterns = patterns(
'modoboa.extensions.amavis.views',
url(r'^$', 'index', name="index"),
url(r'^listing/$', '_listing', name="_mail_list"),
url(r'^listing/page/$', 'listing_page', name="mail_page"),
url(r'^getmailcontent/(?P<mail_id>[\w\-\+]+)/$', 'getmailcontent',
name="mailcontent_get"),
url(r'^process/$', 'process', name="mail_process"),
url(r'^delete/(?P<mail_id>[\w\-\+]+)/$', 'delete', name="mail_delete"),
url(r'^release/(?P<mail_id>[\w\-\+]+)/$', 'release',
name="mail_release"),
url(r'^markspam/(?P<mail_id>[\w\-\+]+)/$', 'mark_as_spam',
name="mail_mark_as_spam"),
url(r'^markham/(?P<mail_id>[\w\-\+]+)/$', 'mark_as_ham',
name="mail_mark_as_ham"),
url(r'^learning_recipient/$', 'learning_recipient',
name="learning_recipient_set"),
url(r'^(?P<mail_id>[\w\-\+]+)/$', 'viewmail', name="mail_detail"),
url(r'^(?P<mail_id>[\w\-\+]+)/headers/$', 'viewheaders',
name="headers_detail"),
)
-436
View File
@@ -1,436 +0,0 @@
# coding: utf-8
"""
Amavis quarantine views.
"""
import email
import chardet
from django.shortcuts import render
from django.http import HttpResponseRedirect, Http404
from django.template import Template, Context
from django.utils.translation import ugettext as _, ungettext
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from modoboa.lib import parameters
from modoboa.lib.exceptions import BadRequest
from modoboa.lib.paginator import Paginator
from modoboa.lib.web_utils import (
getctx, render_to_json_response, _render_to_string
)
from modoboa.extensions.admin.models import Mailbox, Domain
from modoboa.extensions.amavis.templatetags.amavis_tags import (
quar_menu, viewm_menu
)
from .lib import (
selfservice, AMrelease, QuarantineNavigationParameters,
SpamassassinClient, manual_learning_enabled
)
from .forms import LearningRecipientForm
from .models import Msgrcpt
from .sql_connector import get_connector
from .sql_email import SQLemail
def empty_quarantine():
"""Shortcut to use when no content can be displayed."""
content = "<div class='alert alert-info'>%s</div>" % _("Empty quarantine")
ctx = getctx("ok", level=2, listing=content)
return render_to_json_response(ctx)
def get_listing_pages(request, connector):
"""Return listing pages."""
paginator = Paginator(
connector.messages_count(),
int(parameters.get_user(request.user, "MESSAGES_PER_PAGE"))
)
page_id = int(connector.navparams.get("page"))
page = paginator.getpage(page_id)
if not page:
return None
pages = [page]
if not page.has_next and page.has_previous and page.items < 40:
pages = [paginator.getpage(page_id - 1)] + pages
email_list = []
for page in pages:
email_list += connector.fetch(page.id_start, page.id_stop)
return {"pages": [page.number for page in pages], "rows": email_list}
@login_required
def listing_page(request):
"""Return a listing page."""
navparams = QuarantineNavigationParameters(request)
previous_page_id = int(navparams["page"]) if "page" in navparams else None
navparams.store()
connector = get_connector(user=request.user, navparams=navparams)
context = get_listing_pages(request, connector)
if context is None:
context = {"length": 0}
navparams["page"] = previous_page_id
else:
context["rows"] = _render_to_string(
request, "amavis/emails_page.html", {"email_list": context["rows"]}
)
return render_to_json_response(context)
@login_required
def _listing(request):
"""Listing initial view.
Called the first time the listing page is displayed.
"""
if not request.user.is_superuser and request.user.group != 'SimpleUsers':
if not Domain.objects.get_for_admin(request.user).count():
return empty_quarantine()
navparams = QuarantineNavigationParameters(request)
navparams.store()
connector = get_connector(user=request.user, navparams=navparams)
context = get_listing_pages(request, connector)
if context is None:
return empty_quarantine()
context["listing"] = _render_to_string(
request, "amavis/email_list.html", {"email_list": context["rows"]}
)
del context["rows"]
if request.session.get('location', 'listing') != 'listing':
context["menu"] = quar_menu(request.user)
request.session["location"] = "listingx"
return render_to_json_response(context)
@login_required
def index(request):
"""Default view."""
check_learning_rcpt = "false"
if parameters.get_admin("MANUAL_LEARNING") == "yes":
if request.user.group != "SimpleUsers":
user_level_learning = parameters.get_admin(
"USER_LEVEL_LEARNING") == "yes"
domain_level_learning = parameters.get_admin(
"DOMAIN_LEVEL_LEARNING") == "yes"
if user_level_learning or domain_level_learning:
check_learning_rcpt = "true"
return render(request, "amavis/index.html", dict(
selection="quarantine", check_learning_rcpt=check_learning_rcpt
))
def getmailcontent_selfservice(request, mail_id):
mail = SQLemail(mail_id, mformat="plain", links="0")
return render(request, "common/viewmail.html", {
"headers": mail.render_headers(),
"mailbody": mail.body
})
@selfservice(getmailcontent_selfservice)
def getmailcontent(request, mail_id):
mail = SQLemail(mail_id, mformat="plain", links="0")
return render(request, "common/viewmail.html", {
"headers": mail.render_headers(),
"mailbody": mail.body
})
def viewmail_selfservice(request, mail_id,
tplname="amavis/viewmail_selfservice.html"):
rcpt = request.GET.get("rcpt", None)
secret_id = request.GET.get("secret_id", "")
if rcpt is None:
raise Http404
content = Template("""{% load url from future %}
<iframe src="{% url 'amavis:mailcontent_get' mail_id %}" id="mailcontent"></iframe>
""").render(Context(dict(mail_id=mail_id)))
return render(request, tplname, dict(
mail_id=mail_id, rcpt=rcpt, secret_id=secret_id, content=content
))
@selfservice(viewmail_selfservice)
def viewmail(request, mail_id):
rcpt = request.GET.get("rcpt", None)
if rcpt is None:
raise BadRequest(_("Invalid request"))
if request.user.email == rcpt:
get_connector().set_msgrcpt_status(rcpt, mail_id, 'V')
elif request.user.mailbox_set.count():
mb = Mailbox.objects.get(user=request.user)
if rcpt == mb.full_address or rcpt in mb.alias_addresses:
get_connector().set_msgrcpt_status(rcpt, mail_id, 'V')
content = Template("""
<iframe src="{{ url }}" id="mailcontent"></iframe>
""").render(Context({"url": reverse("amavis:mailcontent_get", args=[mail_id])}))
menu = viewm_menu(request.user, mail_id, rcpt)
ctx = getctx("ok", menu=menu, listing=content)
request.session['location'] = 'viewmail'
return render_to_json_response(ctx)
@login_required
def viewheaders(request, mail_id):
"""Display message headers."""
content = get_connector().get_mail_content(mail_id)
msg = email.message_from_string(content)
headers = []
for name, value in msg.items():
if value:
result = chardet.detect(value)
if result["encoding"] is not None:
value = value.decode(result["encoding"])
headers += [(name, value)]
return render(request, 'amavis/viewheader.html', {
"headers": headers
})
def check_mail_id(request, mail_id):
if type(mail_id) in [str, unicode]:
if "rcpt" in request.POST:
mail_id = ["%s %s" % (request.POST["rcpt"], mail_id)]
else:
mail_id = [mail_id]
return mail_id
def get_user_valid_addresses(user):
"""Retrieve all valid addresses of a user."""
valid_addresses = []
if user.group == 'SimpleUsers':
valid_addresses.append(user.email)
try:
mb = Mailbox.objects.get(user=user)
except Mailbox.DoesNotExist:
pass
else:
valid_addresses += mb.alias_addresses
return valid_addresses
def delete_selfservice(request, mail_id):
rcpt = request.GET.get("rcpt", None)
if rcpt is None:
raise BadRequest(_("Invalid request"))
try:
get_connector().set_msgrcpt_status(rcpt, mail_id, 'D')
except Msgrcpt.DoesNotExist:
raise BadRequest(_("Invalid request"))
return render_to_json_response(_("Message deleted"))
@selfservice(delete_selfservice)
def delete(request, mail_id):
"""Delete message selection.
:param str mail_id: message unique identifier
"""
mail_id = check_mail_id(request, mail_id)
connector = get_connector()
valid_addresses = get_user_valid_addresses(request.user)
for mid in mail_id:
r, i = mid.split()
if valid_addresses and r not in valid_addresses:
continue
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)}
return render_to_json_response({
"message": message,
"url": QuarantineNavigationParameters(request).back_to_listing()
})
def release_selfservice(request, mail_id):
rcpt = request.GET.get("rcpt", None)
secret_id = request.GET.get("secret_id", None)
if rcpt is None or secret_id is None:
raise BadRequest(_("Invalid request"))
connector = get_connector()
try:
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":
connector.set_msgrcpt_status(rcpt, mail_id, 'p')
msg = _("Request sent")
else:
amr = AMrelease()
result = amr.sendreq(mail_id, secret_id, rcpt)
if result:
connector.set_msgrcpt_status(rcpt, mail_id, 'R')
msg = _("Message released")
else:
raise BadRequest(result)
return render_to_json_response(msg)
@selfservice(release_selfservice)
def release(request, mail_id):
"""Release message selection.
:param str mail_id: message unique identifier
"""
mail_id = check_mail_id(request, mail_id)
msgrcpts = []
connector = get_connector()
valid_addresses = get_user_valid_addresses(request.user)
for mid in mail_id:
r, i = mid.split()
if valid_addresses and r not in valid_addresses:
continue
msgrcpts += [connector.get_recipient_message(r, i)]
if request.user.group == "SimpleUsers" and \
parameters.get_admin("USER_CAN_RELEASE") == "no":
for msgrcpt in msgrcpts:
connector.set_msgrcpt_status(
msgrcpt.rid.email, msgrcpt.mail.mail_id, 'p'
)
message = ungettext("%(count)d request sent",
"%(count)d requests sent",
len(mail_id)) % {"count": len(mail_id)}
return render_to_json_response({
"message": message,
"url": QuarantineNavigationParameters(request).back_to_listing()
})
amr = AMrelease()
error = None
for rcpt in msgrcpts:
result = amr.sendreq(
rcpt.mail.mail_id, rcpt.mail.secret_id, rcpt.rid.email
)
if result:
connector.set_msgrcpt_status(rcpt.rid.email, rcpt.mail.mail_id, 'R')
else:
error = result
break
if not error:
message = ungettext("%(count)d message released successfully",
"%(count)d messages released successfully",
len(mail_id)) % {"count": len(mail_id)}
else:
message = error
status = 400 if error else 200
return render_to_json_response({
"message": message,
"url": QuarantineNavigationParameters(request).back_to_listing()
}, status=status)
def mark_messages(request, selection, mtype, recipient_db=None):
"""Mark a selection of messages as spam.
:param str selection: message unique identifier
:param str mtype: type of marking (spam or ham)
"""
if not manual_learning_enabled(request.user):
return render_to_json_response({"status": "ok"})
if recipient_db is None:
recipient_db = (
"user" if request.user.group == "SimpleUsers" else "global"
)
selection = check_mail_id(request, selection)
connector = get_connector()
saclient = SpamassassinClient(request.user, recipient_db)
for item in selection:
rcpt, mail_id = item.split()
content = connector.get_mail_content(mail_id)
result = saclient.learn_spam(rcpt, content) if mtype == "spam" \
else saclient.learn_ham(rcpt, content)
if not result:
break
connector.set_msgrcpt_status(rcpt, mail_id, mtype[0].upper())
if saclient.error is None:
saclient.done()
message = ungettext("%(count)d message processed successfully",
"%(count)d messages processed successfully",
len(selection)) % {"count": len(selection)}
else:
message = saclient.error
status = 400 if saclient.error else 200
return render_to_json_response({
"message": message, "reload": True
}, status=status)
@login_required
def learning_recipient(request):
"""A view to select the recipient database of a learning action."""
if request.method == "POST":
form = LearningRecipientForm(request.user, request.POST)
if form.is_valid():
return mark_messages(
request,
form.cleaned_data["selection"].split(","),
form.cleaned_data["ltype"],
form.cleaned_data["recipient"]
)
return render_to_json_response(
{"form_errors": form.errors}, status=400
)
ltype = request.GET.get("type", None)
selection = request.GET.get("selection", None)
if ltype is None or selection is None:
raise BadRequest
form = LearningRecipientForm(request.user)
form.fields["ltype"].initial = ltype
form.fields["selection"].initial = selection
return render(request, "common/generic_modal_form.html", {
"title": _("Select a database"),
"formid": "learning_recipient_form",
"action": reverse("amavis:learning_recipient_set"),
"action_classes": "submit",
"action_label": _("Validate"),
"form": form
})
@login_required
def mark_as_spam(request, mail_id):
"""Mark a single message as spam."""
return mark_messages(request, mail_id, "spam")
@login_required
def mark_as_ham(request, mail_id):
"""Mark a single message as ham."""
return mark_messages(request, mail_id, "ham")
@login_required
def process(request):
"""Process a selection of messages.
The request must specify an action to execute against the
selection.
"""
action = request.POST.get("action", None)
ids = request.POST.get("selection", "")
ids = ids.split(",")
if not ids or action is None:
return HttpResponseRedirect(reverse("amavis:index"))
if request.POST["action"] == "release":
return release(request, ids)
if request.POST["action"] == "delete":
return delete(request, ids)
if request.POST["action"] == "mark_as_spam":
return mark_messages(request, ids, "spam")
if request.POST["action"] == "mark_as_ham":
return mark_messages(request, ids, "ham")