diff --git a/extensions/amavis_quarantine/cleanup.py b/extensions/amavis_quarantine/cleanup.py index 9d052c3da..235be0789 100755 --- a/extensions/amavis_quarantine/cleanup.py +++ b/extensions/amavis_quarantine/cleanup.py @@ -1,33 +1,49 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from modoboa.lib import db, parameters +import time +from modoboa.extensions.amavis_quarantine.models import * if __name__ == "__main__": from modoboa.extensions.amavis_quarantine import main + from modoboa.lib import parameters + from optparse import OptionParser + + parser = OptionParser() + parser.add_option("--debug", action="store_true", + help="Activate debug output") + options, args = parser.parse_args() + + if options.debug: + import logging + l = logging.getLogger("django.db.backends") + l.setLevel(logging.DEBUG) + l.addHandler(logging.StreamHandler()) main.init() - conn = db.getconnection("amavis_quarantine") + max_messages_age = int(parameters.get_admin("MAX_MESSAGES_AGE", app="amavis_quarantine")) + flags = ['D'] + if parameters.get_admin("RELEASED_MSGS_CLEANUP", + app="amavis_quarantine") == "yes": + flags += ['R'] + + print "Deleting marked messages..." + ids = Msgrcpt.objects.filter(rs__in=flags).values("mail_id") + for msg in Msgs.objects.filter(mail_id__in=ids): + if not msg.msgrcpt_set.exclude(rs__in=flags).count(): + msg.delete() + print "Deleting messages older than %d days..." % max_messages_age - db.execute(conn, """ -DELETE FROM msgs WHERE time_num < UNIX_TIMESTAMP()-%d*24*3600; -""" % max_messages_age) + limit = int(time.time()) - (max_messages_age * 24 * 3600) + Msgs.objects.filter(time_num__lt=limit).delete() print "Deleting unreferenced e-mail addresses..." - db.execute(conn, """ -DELETE FROM maddr - WHERE NOT EXISTS (SELECT 1 FROM msgs WHERE sid=id) - AND NOT EXISTS (SELECT 1 FROM msgrcpt WHERE rid=id); -""") - - print "Optimizing tables..." - db.execute(conn, """ -OPTIMIZE TABLE msgs, msgrcpt, quarantine, maddr; -""") - - db.close(conn) - + for maddr in Maddr.objects.all(): + if not len(maddr.msgs_set.all()) and not len(maddr.msgrcpt_set.all()): + maddr.delete() + print "Done." + diff --git a/extensions/amavis_quarantine/dbrouter.py b/extensions/amavis_quarantine/dbrouter.py new file mode 100644 index 000000000..df908458e --- /dev/null +++ b/extensions/amavis_quarantine/dbrouter.py @@ -0,0 +1,30 @@ +class AmavisRouter(object): + """A router to control all database operations on models in + the amavis_quarantine application""" + + def db_for_read(self, model, **hints): + "Point all operations on amavis_quarantine models to 'amavis'" + if model._meta.app_label == 'amavis_quarantine': + return 'amavis' + return None + + def db_for_write(self, model, **hints): + "Point all operations on amavis_quarantine models to 'amavis'" + if model._meta.app_label == 'amavis_quarantine': + return 'amavis' + return None + + def allow_relation(self, obj1, obj2, **hints): + "Allow any relation if a model in amavis_quarantine is involved" + if obj1._meta.app_label == 'amavis_quarantine' \ + or obj2._meta.app_label == 'amavis_quarantine': + return True + return None + + def allow_syncdb(self, db, model): + "Make sure the amavis_quarantine app only appears on the 'amavis' db" + if db == 'amavis': + return model._meta.app_label == 'amavis_quarantine' + elif model._meta.app_label == 'amavis_quarantine': + return False + return None diff --git a/extensions/amavis_quarantine/main.py b/extensions/amavis_quarantine/main.py index 892ecb0e1..cf330f2a8 100644 --- a/extensions/amavis_quarantine/main.py +++ b/extensions/amavis_quarantine/main.py @@ -19,6 +19,8 @@ def init(): parameters.register_admin("MAX_MESSAGES_AGE", type="int", deflt=14, help=_("Quarantine messages maximum age (in days) before deletion")) + parameters.register_admin("RELEASED_MSGS_CLEANUP", type="list_yesno", deflt="no", + help=_("Remove messages marked as released while cleaning up the database")) parameters.register_admin("AM_PDP_MODE", type="list", deflt="unix", values=[("inet", "inet"), ("unix", "unix")], diff --git a/extensions/amavis_quarantine/models.py b/extensions/amavis_quarantine/models.py new file mode 100644 index 000000000..10187e656 --- /dev/null +++ b/extensions/amavis_quarantine/models.py @@ -0,0 +1,141 @@ +# 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 + +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() + 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 + +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', primary_key=True) + 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 = ("mail", "rid") + +class Policy(models.Model): + id = models.IntegerField(primary_key=True) + policy_name = models.CharField(max_length=96, blank=True) + virus_lover = models.CharField(max_length=3, blank=True) + spam_lover = models.CharField(max_length=3, blank=True) + banned_files_lover = models.CharField(max_length=3, blank=True) + bad_header_lover = models.CharField(max_length=3, blank=True) + bypass_virus_checks = models.CharField(max_length=3, blank=True) + bypass_spam_checks = models.CharField(max_length=3, blank=True) + bypass_banned_checks = models.CharField(max_length=3, blank=True) + bypass_header_checks = models.CharField(max_length=3, blank=True) + spam_modifies_subj = models.CharField(max_length=3, blank=True) + virus_quarantine_to = models.CharField(max_length=192, blank=True) + spam_quarantine_to = models.CharField(max_length=192, blank=True) + banned_quarantine_to = models.CharField(max_length=192, blank=True) + bad_header_quarantine_to = models.CharField(max_length=192, blank=True) + clean_quarantine_to = models.CharField(max_length=192, blank=True) + other_quarantine_to = models.CharField(max_length=192, blank=True) + spam_tag_level = models.FloatField(null=True, blank=True) + spam_tag2_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) + addr_extension_spam = models.CharField(max_length=192, blank=True) + addr_extension_banned = models.CharField(max_length=192, blank=True) + addr_extension_bad_header = models.CharField(max_length=192, blank=True) + warnvirusrecip = models.CharField(max_length=3, blank=True) + warnbannedrecip = models.CharField(max_length=3, blank=True) + warnbadhrecip = models.CharField(max_length=3, blank=True) + newvirus_admin = models.CharField(max_length=192, blank=True) + virus_admin = models.CharField(max_length=192, blank=True) + banned_admin = models.CharField(max_length=192, blank=True) + bad_header_admin = models.CharField(max_length=192, blank=True) + spam_admin = models.CharField(max_length=192, blank=True) + spam_subject_tag = models.CharField(max_length=192, blank=True) + spam_subject_tag2 = models.CharField(max_length=192, blank=True) + message_size_limit = models.IntegerField(null=True, blank=True) + banned_rulenames = models.CharField(max_length=192, blank=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.TextField() + class Meta: + db_table = u'quarantine' + managed = False + ordering = ["-mail__time_num"] + unique_together = ("mail", "chunk_ind") + +class Users(models.Model): + id = models.IntegerField(primary_key=True) + priority = models.IntegerField() + policy_id = models.IntegerField() + email = models.CharField(unique=True, max_length=255) + fullname = models.CharField(max_length=765, blank=True) + local = models.CharField(max_length=3, 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 diff --git a/extensions/amavis_quarantine/sql_listing.py b/extensions/amavis_quarantine/sql_listing.py index f05cd4f80..255d9bb7f 100644 --- a/extensions/amavis_quarantine/sql_listing.py +++ b/extensions/amavis_quarantine/sql_listing.py @@ -2,9 +2,11 @@ from datetime import datetime from django.utils.translation import ugettext as _ -from modoboa.lib import tables, db, static_url +from django.db.models import Q +from modoboa.lib import tables, static_url from modoboa.lib.email_listing import MBconnector, EmailListing from modoboa.lib.emailutils import * +from models import * class Qtable(tables.Table): tableid = "emails" @@ -16,7 +18,7 @@ class Qtable(tables.Table): from_ = tables.Column("from", label=_("From"), limit=30) subject = tables.Column("subject", label=_("Subject"), limit=40) time = tables.Column("date", label=_("Date")) - to = tables.Column("to", label=_("To"), limit=30) + to = tables.Column("to", label=_("To"), sortable=False) cols_order = ['type', 'rstatus', 'to', 'from_', 'subject', 'time'] @@ -24,64 +26,51 @@ class Qtable(tables.Table): return datetime.fromtimestamp(value) class SQLconnector(MBconnector): - def __init__(self, filter=None): - self.conn = db.getconnection("amavis_quarantine") - self.filter = "" - if filter: - for a in filter: - if not a: - continue - if a[0] == "&": - self.filter += " AND " - else: - self.filter += " OR " - self.filter += a[1:] - query = self._get_query() - status, cursor = db.execute(self.conn, """ -SELECT count(quarantine.mail_id) AS total -%s -""" % query) - if not status: - print cursor - self.count = 0 - else: - self.count = int(cursor.fetchone()[0]) - - def _get_query(self): - return """ -FROM quarantine, maddr, msgrcpt, msgs -WHERE quarantine.mail_id=msgrcpt.mail_id -AND msgrcpt.rid=maddr.id -AND msgrcpt.mail_id=msgs.mail_id -AND quarantine.chunk_ind=1 -%s -ORDER BY msgs.time_num DESC -""" % self.filter + orders = { + "from" : "mail__from_addr", + "subject" : "mail__subject", + "date" : "mail__time_num" + } + + def __init__(self, mail_ids=None, filter=None): + self.count = None + self.mail_ids = mail_ids + self.filter = filter def messages_count(self, **kwargs): + if self.count is None: + filter = Q(chunk_ind=1) + if self.mail_ids is not None: + filter &= Q(mail__in=self.mail_ids) + if self.filter: + filter &= self.filter + self.messages = Quarantine.objects.filter(filter) + if kwargs.has_key("order"): + totranslate = kwargs["order"][1:] + sign = kwargs["order"][:1] + if sign == " ": + sign = "" + order = sign + self.orders[totranslate] + self.messages = self.messages.order_by(order) + self.count = self.messages.count() return self.count def fetch(self, start=None, stop=None, **kwargs): - query = self._get_query() - status, cursor = db.execute(self.conn, """ -SELECT msgs.from_addr, maddr.email, msgs.subject, msgs.content, quarantine.mail_id, - msgs.time_num, msgs.content, msgrcpt.rs -%s -LIMIT %d,%d -""" % (query, start - 1, kwargs["nbelems"])) - if not status: - print cursor - return [] + messages = self.messages[start - 1:stop] emails = [] - rows = cursor.fetchall() - for row in rows: - m = {"from" : row[0], "to" : row[1], - "subject" : row[2], "content" : row[3], - "mailid" : row[4], "date" : row[5], - "type" : row[6]} - if row[7] == "R": - m["img_rstatus"] = static_url("pics/release.png"); - emails.append(m) + for qm in messages: + for rcpt in qm.mail.msgrcpt_set.all(): + m = {"from" : qm.mail.from_addr, + "to" : rcpt.rid.email, + "subject" : qm.mail.subject, + "mailid" : qm.mail_id, + "date" : qm.mail.time_num, + "type" : qm.mail.content} + if rcpt.rs == '': + m["class"] = "unseen" + elif rcpt.rs == 'R': + m["img_rstatus"] = static_url("pics/release.png") + emails.append(m) return emails class SQLlisting(EmailListing): @@ -91,16 +80,17 @@ class SQLlisting(EmailListing): defcallback = "updatelisting" reset_wm_url = True - def __init__(self, user, filter, **kwargs): + def __init__(self, user, msgs, filter, **kwargs): if not user.is_superuser: Qtable.cols_order.remove('to') - self.mbc = SQLconnector(filter) - EmailListing.__init__(self, **kwargs) + self.mbc = SQLconnector(msgs, filter) + + super(SQLlisting, self).__init__(**kwargs) class SQLemail(Email): def __init__(self, msg, *args, **kwargs): - Email.__init__(self, msg, *args, **kwargs) - fields = ["X-Amavis-Alert", "Subject", "From", "To", "Date"] + super(SQLemail, self).__init__(msg, *args, **kwargs) + fields = ["X-Amavis-Alert", "Subject", "From", "To", "Cc", "Date"] for f in fields: label = f if not msg.has_key(f): diff --git a/extensions/amavis_quarantine/static/js/quarantine.js b/extensions/amavis_quarantine/static/js/quarantine.js index 5e3d9b960..28db0502c 100644 --- a/extensions/amavis_quarantine/static/js/quarantine.js +++ b/extensions/amavis_quarantine/static/js/quarantine.js @@ -1,6 +1,7 @@ function updatehash(evt) { evt.stop(); - current_anchor.parse_string(evt.target.get("href"), true).setparams(navparams); + var url = $defined(this.get("href")) ? this.get("href") : ""; + current_anchor.parse_string(url, true).setparams(navparams); current_anchor.update(); } @@ -20,13 +21,12 @@ function actioncallback(resp) { $("menubar").set("html", ""); $("navbar").set("html", ""); if (resp.status == "ok") { - infobox.info(resp.message); + infobox.info(resp.respmsg); + current_anchor.parse_string(resp.url, true).setparams(navparams); + current_anchor.update(1); } else { - infobox.error(resp.message); + infobox.error(resp.respmsg); } - infobox.hide(1); - current_anchor.parse_string(resp.url, true).setparams(navparams); - current_anchor.update(1); } function actionclick(evt) { @@ -37,6 +37,17 @@ function actionclick(evt) { }).get(); } +function viewmail_cs(evt) { + evt.stop(); + var to = this.getFirst("td[name=to]"); + + current_anchor.baseurl(this.get("id")); + if ($defined(to)) { + current_anchor.setparam("rcpt", to.get("html").trim()); + } + current_anchor.update(); +} + /* * Callback of the 'listing' action (the main screen) */ @@ -92,7 +103,9 @@ function listing_cb(resp) { mysubmit(event, "delete"); }); searchbox_init(); - $$("tr").addEvent("dblclick", viewmail); + init_sortable_columns(); + update_sortable_column(); + $$("tbody>tr").addEvent("dblclick", viewmail_cs); } /* diff --git a/extensions/amavis_quarantine/templates/amavis_quarantine/index.html b/extensions/amavis_quarantine/templates/amavis_quarantine/index.html index 359dcc0b9..22368f1f8 100644 --- a/extensions/amavis_quarantine/templates/amavis_quarantine/index.html +++ b/extensions/amavis_quarantine/templates/amavis_quarantine/index.html @@ -9,7 +9,13 @@ event.stop(); var selection = new Array(); $$("tr[class*=table-tr-selected]").each(function(item) { - selection.push(item.get("id")); + var content = ""; + var to = item.getFirst("td[name=to]"); + + if ($defined(to)) { + content = to.get("html").trim() + " "; + } + selection.push(content + item.get("id")); }); new Element("input", { "type" : "hidden", "name" : "selection", "value" : selection diff --git a/extensions/amavis_quarantine/templatetags/amextras.py b/extensions/amavis_quarantine/templatetags/amextras.py index b92813a72..da40bd633 100644 --- a/extensions/amavis_quarantine/templatetags/amextras.py +++ b/extensions/amavis_quarantine/templatetags/amextras.py @@ -9,7 +9,7 @@ from modoboa.extensions import amavis_quarantine register = template.Library() @register.simple_tag -def viewm_menu(selection, backurl, mail_id, perms): +def viewm_menu(selection, backurl, mail_id, rcpt, perms): options_menu = [ {"name" : "viewmode", "label" : _("View as plain text"), @@ -34,12 +34,14 @@ def viewm_menu(selection, backurl, mail_id, perms): "class" : "boxed", "rel" : "{handler:'iframe',size:{x:600,y:500}}"}, {"name" : "release", - "url" : reverse(amavis_quarantine.views.release, args=[mail_id]), + "url" : reverse(amavis_quarantine.views.release, args=[mail_id]) \ + + "?rcpt=%s" % rcpt, "img" : static_url("pics/release.png"), "label" : _("Release"), "confirm" : _("Release this message?")}, {"name" : "delete", - "url" : reverse(amavis_quarantine.views.delete, args=[mail_id]), + "url" : reverse(amavis_quarantine.views.delete, args=[mail_id]) \ + + "?rcpt=%s" % rcpt, "img" : static_url("pics/remove.png"), "label" : _("Delete"), "confirm" : _("Delete this message?")}, @@ -55,7 +57,7 @@ def viewm_menu(selection, backurl, mail_id, perms): "perms" : perms}) @register.simple_tag -def quar_menu(selection, perms): +def quar_menu(selection, user): entries = [ {"name" : "release", "url" : "", @@ -88,9 +90,13 @@ def quar_menu(selection, perms): ] menu = render_to_string('common/menu.html', - {"selection" : selection, "entries" : entries, - "perms" : perms}) + {"selection" : selection, "entries" : entries}) + if user.is_superuser or is_domain_admin(user): + extraopts = [{"name" : "to", "label" : _("To")}] + else: + extraopts = [] searchbar = render_to_string('common/email_searchbar.html', { - "MEDIA_URL" : settings.MEDIA_URL + "MEDIA_URL" : settings.MEDIA_URL, + "extraopts" : extraopts }) return menu + searchbar diff --git a/extensions/amavis_quarantine/views.py b/extensions/amavis_quarantine/views.py index ed9abef20..b9630077b 100644 --- a/extensions/amavis_quarantine/views.py +++ b/extensions/amavis_quarantine/views.py @@ -1,10 +1,6 @@ # -*- coding: utf-8 -*- -from datetime import datetime -import time import email -import re -import os from django.http import HttpResponseRedirect, HttpResponse from django.template import Template, Context from django.utils import simplejson @@ -12,51 +8,78 @@ 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 _render, _ctx_ok, _ctx_ko, decode, getctx -from modoboa.lib import db, parameters +from django.db.models import Q +from modoboa.lib import parameters, _render, getctx, ajax_response from modoboa.admin.models import Mailbox from lib import AMrelease from templatetags.amextras import * from modoboa.lib.email_listing import parse_search_parameters from sql_listing import * +from admin.lib import is_domain_admin def __get_current_url(request): - res = "?page=%s" % request.session["page"] - for p in ["criteria", "pattern"]: - if p in request.session.keys(): - res += "&%s=%s" % (p, request.session[p]) + if request.session.has_key("page"): + res = "?page=%s" % request.session["page"] + else: + res = "" + params = "&".join(map(lambda p: "%s=%s" % (p, request.session[p]), + filter(request.session.has_key, ["criteria", "pattern"]))) + if params != "": + res += "?%s" % (params) return res @login_required -def _listing(request, internal=False, filter=None): - #start = time.time() - if not request.user.is_superuser: - mb = Mailbox.objects.get(user=request.user.id) - if filter is None: - filter = ["&maddr.email='%s'" % mb.full_address] - else: - filter += ["&maddr.email='%s'" % mb.full_address] +def _listing(request): + filter = None + rcptfilter = None + msgs = None + + order = request.GET.has_key("order") and request.GET["order"] or "-date" + if not request.session.has_key("navparams"): + request.session["navparams"] = {} + request.session["navparams"]["order"] = order parse_search_parameters(request) if request.session.has_key("pattern"): - tmp = "" - for c in request.session["criteria"].split(','): - if tmp != "": - tmp += " OR " - tmp += "msgs.%s LIKE '%%%s%%'" % (c, request.session["pattern"]) - tmp = "&(%s)" % tmp - if filter is None: - filter = [tmp] - else: - filter += [tmp] + criteria = request.session["criteria"] + if criteria == "both": + criteria = "from_addr,subject,to" + for c in criteria.split(","): + if c == "from_addr": + nfilter = Q(mail__from_addr__contains=request.session["pattern"]) + elif c == "subject": + nfilter = Q(mail__subject__contains=request.session["pattern"]) + elif c == "to": + rcptfilter = request.session["pattern"] + continue + else: + raise Exception("unsupported search criteria %s" % c) + filter = nfilter if filter is None else filter | nfilter - pageid = request.GET.has_key("page") and int(request.GET["page"]) or 1 - request.session["page"] = pageid # ?? nécessaire - lst = SQLlisting(request.user, filter, baseurl="listing/", empty=internal, + q = ~Q(rs='D') + if not request.user.is_superuser: + mb = Mailbox.objects.get(user=request.user.id) + if is_domain_admin(request.user): + q &= Q(rid__email__contains=mb.domain.name) + else: + q &= Q(rid__email=mb.full_address) + if (request.user.is_superuser or is_domain_admin(request.user)) \ + and rcptfilter is not None: + q &= Q(rid__email__contains=rcptfilter) + msgs = Msgrcpt.objects.filter(q).values("mail_id") + + if request.GET.has_key("page"): + request.session["page"] = request.GET["page"] + pageid = int(request.session["page"]) + else: + if request.session.has_key("page"): + del request.session["page"] + pageid = 1 + + lst = SQLlisting(request.user, msgs, filter, baseurl="listing/", + navparams=request.session["navparams"], elems_per_page=int(parameters.get_user(request.user, "MESSAGES_PER_PAGE"))) - if internal: - return lst.render(request, pageid=int(pageid)) page = lst.paginator.getpage(pageid) if page: content = lst.fetch(request, page.id_start, page.id_stop) @@ -65,26 +88,21 @@ def _listing(request, internal=False, filter=None): content = "
%s
" % _("Empty quarantine") navbar = "" ctx = getctx("ok", listing=content, navbar=navbar, - menu=quar_menu("", request.user.get_all_permissions())) + menu=quar_menu("", request.user)) return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") @login_required -def index(request, message=None): - return _listing(request, True) +def index(request): + return SQLlisting(request.user, None, None, empty=True).render(request) @login_required def getmailcontent(request, mail_id): from sql_listing import SQLemail - conn = db.getconnection("amavis_quarantine") - status, cursor = db.execute(conn, """ -SELECT mail_text -FROM quarantine -WHERE quarantine.mail_id='%s' -""" % mail_id) + qmails = Quarantine.objects.filter(mail=mail_id) content = "" - for part in cursor.fetchall(): - content += part[0] + for qm in qmails: + content += qm.mail_text msg = email.message_from_string(content) links = request.GET.has_key("links") and request.GET["links"] or "0" mode = request.GET.has_key("mode") and request.GET["mode"] or "plain" @@ -96,102 +114,107 @@ WHERE quarantine.mail_id='%s' @login_required def viewmail(request, mail_id): - args = "" + if request.user.is_superuser or is_domain_admin(request.user): + rcpt = request.GET["rcpt"] + else: + mb = Mailbox.objects.get(user=request.user) + rcpt = mb.full_address + msgrcpt = Msgrcpt.objects.get(mail=mail_id, rid__email=rcpt) + msgrcpt.rs = 'V' + msgrcpt.save() + args = [] for kw in ["mode", "links"]: - if kw in request.GET.keys(): - args += args != "" and "&" or "?" - args += "%s=%s" % (kw, request.GET[kw]) + if request.GET.has_key(kw): + args += ["%s=%s" % (kw, request.GET[kw])] + content = Template(""" -""").render(Context({"url" : reverse(getmailcontent, args=[mail_id]) + args})) - menu = viewm_menu("", __get_current_url(request), mail_id, +""").render(Context({"url" : reverse(getmailcontent, args=[mail_id]) \ + + "?%s" % "&".join(args)})) + menu = viewm_menu("", __get_current_url(request), mail_id, rcpt, request.user.get_all_permissions()) ctx = getctx("ok", menu=menu, listing=content) return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") @login_required def viewheaders(request, mail_id): - conn = db.getconnection("amavis_quarantine") - status, cursor = db.execute(conn, """ -SELECT mail_text -FROM quarantine -WHERE quarantine.mail_id='%s' -""" % mail_id) content = "" - for part in cursor.fetchall(): - content += part[0] + for qm in Quarantine.objects.filter(mail=mail_id): + content += qm.mail_text msg = email.message_from_string(content) return _render(request, 'amavis_quarantine/viewheader.html', { "headers" : msg.items() }) -@login_required -def delete(request, mail_id, count=1): - conn = db.getconnection("amavis_quarantine") - if mail_id[0] != "'": - mail_id = "'%s'" % mail_id - status, error = db.execute(conn, - "DELETE FROM msgs WHERE mail_id IN (%s)" % mail_id) - if status: - message = ungettext("%(count)d message deleted successfully", - "%(count)d messages deleted successfully", - count) % {"count" : count} - else: - message = error - ctx = getctx("ok", url=__get_current_url(request), message=message) - return HttpResponse(simplejson.dumps(ctx), - mimetype="application/json") +def check_mail_id(request, mail_id): + if type(mail_id) in [str, unicode]: + if request.GET.has_key("rcpt"): + mail_id = ["%s %s" % (request.GET["rcpt"], mail_id)] + else: + mail_id = [mail_id] + return mail_id @login_required -def release(request, mail_id, count=1): - conn = db.getconnection("amavis_quarantine") - if mail_id[0] != "'": - mail_id = "'%s'" % mail_id - status, cursor = db.execute(conn, """ -SELECT msgs.mail_id,secret_id,quar_type,maddr.email FROM msgs, maddr, msgrcpt -WHERE msgrcpt.mail_id=msgs.mail_id AND msgrcpt.rid=maddr.id AND msgs.mail_id IN (%s) -""" % (mail_id)) - emails = {} +def delete(request, mail_id): + mail_id = check_mail_id(request, mail_id) + if not request.user.is_superuser and not is_domain_admin(request.user): + mb = Mailbox.objects.get(user=request.user) + msgrcpts = Msgrcpt.objects.filter(mail__in=mail_id, rid__email=mb.full_address) + msgrcpts.update(rs='D') + else: + for mid in mail_id: + r, i = mid.split() + msgrcpt = Msgrcpt.objects.get(mail=i, rid__email=r) + msgrcpt.rs = 'D' + msgrcpt.save() + + message = ungettext("%(count)d message deleted successfully", + "%(count)d messages deleted successfully", + len(mail_id)) % {"count" : len(mail_id)} + return ajax_response(request, respmsg=message, + url=__get_current_url(request)) + +@login_required +def release(request, mail_id): + mail_id = check_mail_id(request, mail_id) + if not request.user.is_superuser and not is_domain_admin(request.user): + mb = Mailbox.objects.get(user=request.user) + msgrcpts = Msgrcpt.objects.filter(mail__in=mail_id, rid__email=mb.full_address) + else: + msgrcpts = [] + for mid in mail_id: + r, i = mid.split() + msgrcpts += [Msgrcpt.objects.get(mail=i, rid__email=r)] + amr = AMrelease() - for row in cursor.fetchall(): - if not emails.has_key(row[0]): - emails[row[0]] = {} - emails[row[0]]["rcpts"] = [] - emails[row[0]]["secret"] = row[1] - emails[row[0]]["rcpts"] += [row[3]] - count = 0 error = None - for k, values in emails.iteritems(): - result = amr.sendreq(k, values["secret"], *values["rcpts"]) + for rcpt in msgrcpts: + result = amr.sendreq(rcpt.mail.mail_id, rcpt.mail.secret_id, rcpt.rid.email) if result: - count += 1 - db.execute(conn, "UPDATE msgrcpt SET rs='R' WHERE mail_id='%s'" % k) + rcpt.rs = 'R' + rcpt.save() else: error = result break + if not error: message = ungettext("%(count)d message released successfully", "%(count)d messages released successfully", - count) % {"count" : count} + len(mail_id)) % {"count" : len(mail_id)} else: message = error - ctx = getctx("ok", url=__get_current_url(request), message=message) - return HttpResponse(simplejson.dumps(ctx), - mimetype="application/json") + return ajax_response(request, "ko" if error else "ok", respmsg=message, + url=__get_current_url(request)) @login_required def process(request): - ids = "" - count = len(request.POST["selection"].split(",")) - for id in request.POST["selection"].split(","): - if ids != "": - ids += "," - ids += "'%s'" % id - if ids == "": + ids = request.POST.get("selection", "") + ids = ids.split(",") + if not len(ids): return HttpResponseRedirect(reverse(index)) if request.POST["action"] == "release": - return release(request, ids, count) + return release(request, ids) if request.POST["action"] == "delete": - return delete(request, ids, count) + return delete(request, ids) diff --git a/extensions/webmail/static/css/webmail.css b/extensions/webmail/static/css/webmail.css index c8ff5f942..deb211ebc 100644 --- a/extensions/webmail/static/css/webmail.css +++ b/extensions/webmail/static/css/webmail.css @@ -45,16 +45,6 @@ html > body #listing { -khtml-user-select: none; /* Webkit (Safari, Chrome) */ } -th[class*=sortable] { - cursor: pointer; -/*-moz-box-shadow: 0px -15px 10px -10px rgba(0,0,0,0.6) inset; - -webkit-box-shadow: 0px -15px 10px -10px rgba(0,0,0,0.6) inset;*/ -} - -th.selected { - background: #EEEEEE; -} - .draggable { cursor: move; /*position: relative;*/ diff --git a/extensions/webmail/templates/webmail/index.html b/extensions/webmail/templates/webmail/index.html index 07993d5aa..aadd73756 100644 --- a/extensions/webmail/templates/webmail/index.html +++ b/extensions/webmail/templates/webmail/index.html @@ -75,13 +75,7 @@ function wm_updatelisting(resp) { wm_updatelisting(resp); rtimer = refreshFolder.periodical({{ refreshrate }}); searchbox_init(); - if (navparams.has("order")) { - src = navparams.get("order").charAt(0) == "-" ? "down_arrow.png" : "up_arrow.png"; - th = $$("th[id=" + navparams.get("order").substring(1) + "]"); - th.getElement("img").set("src", static_url("pics/" + src)); - th.getElement("img").setStyle("display", "block"); - th.addClass("selected"); - } + update_sortable_column(); var emails_table = new HtmlTable($("emails"), { selectable: true, @@ -211,24 +205,8 @@ function wm_updatelisting(resp) { } }); } - + init_sortable_columns(); $$("tbody>tr").addEvent("dblclick", viewmail); - $$("th[class*=sortable]").addEvent("click", function(evt) { - var tmp = "?"; - var order = (navparams.has("order")) ? navparams.get("order") : ""; - - if (order == "") { - order = current_anchor.getparam("order"); - } - if (order.substring(1) == evt.target.id) { - var sign = (order.charAt(0) == '+') ? '-' : '+'; - order = sign + evt.target.id; - } else { - order = "-" + evt.target.id; - } - navparams.set("order", order); - current_anchor.setparams(navparams).update(); - }); }); {% endblock %} {% block middletitle %}{% trans "Webmail" %}{% endblock %} diff --git a/lib/db.py b/lib/db.py deleted file mode 100644 index a926148d0..000000000 --- a/lib/db.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- - -""" - -""" -import re -import MySQLdb - -connections = {} - -def mysql_newconnection(host, dbname, login, password): - import MySQLdb - return MySQLdb.connect(host=host, db=dbname, user=login, passwd=password) - -def getconnection(name): -# if not name in connections.keys(): - if True: - from django.conf import settings - - cfg = settings.DB_CONNECTIONS[name] - if cfg["driver"] == "mysql": - conn = mysql_newconnection(cfg["host"], cfg["dbname"], cfg["login"], - cfg["password"]) - - if connections.has_key(name): - close(connections[name]) - - connections[name] = conn - return connections[name] - -def execute(conn, query): - try: - cursor = conn.cursor() - cursor.execute(query) - if re.search("INSERT|DELETE|UPDATE", query): - conn.commit() - return (True, None) - return (True, cursor) - except MySQLdb.Error, e: - return (False, "Error %d: %s" % (e.args[0], e.args[1])) - -def close(conn): - conn.close() - diff --git a/lib/email_listing.py b/lib/email_listing.py index 3dc4cd532..0bfbdab89 100644 --- a/lib/email_listing.py +++ b/lib/email_listing.py @@ -83,16 +83,21 @@ class Paginator(object): p.baseurl = self.baseurl return p -class EmailListing: +class EmailListing(object): def __init__(self, baseurl=None, folder=None, elems_per_page=40, navparams={}, **kwargs): self.folder = folder self.elems_per_page = elems_per_page self.navparams = navparams self.extravars = {} - order = "order" in kwargs.keys() and kwargs["order"] or None self.empty = "empty" in kwargs.keys() and kwargs["empty"] or False if not self.empty: + if kwargs.has_key("order"): + order = kwargs["order"] + elif self.navparams.has_key("order"): + order = self.navparams["order"] + else: + order = None self.paginator = Paginator(self.mbc.messages_count(folder=self.folder, order=order), elems_per_page) diff --git a/lib/tables.py b/lib/tables.py index 832b0d8d3..cd5c06426 100644 --- a/lib/tables.py +++ b/lib/tables.py @@ -17,10 +17,9 @@ from django.template.loader import render_to_string class Column: """Simple column representation """ - sortable = True - def __init__(self, name, **kwargs): self.name = name + self.sortable = True for k, v in kwargs.iteritems(): setattr(self, k, v) @@ -50,9 +49,8 @@ class ImgColumn(Column): This kind of column only contains images tags (). """ - sortable = False - def __str__(self): + self.sortable = False try: return getattr(self, "header") except AttributeError: diff --git a/settings_sample.py b/settings_sample.py index c29cbb043..ae5beee11 100644 --- a/settings_sample.py +++ b/settings_sample.py @@ -27,9 +27,18 @@ DATABASES = { # "NAME" : "", # "USER" : "", # "PASSWORD" : "" + # }, + # "amavis": { + # "ENGINE" : "", + # "HOST" : "", + # "NAME" : "", + # "USER" : "", + # "PASSWORD" : "" # } } +DATABASE_ROUTERS = ["extensions.amavis_quarantine.lib.AmavisRouter"] + # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. @@ -120,16 +129,5 @@ AUTHENTICATION_BACKENDS = ( 'modoboa.lib.authbackends.SimpleBackend' ) -# Amavis quarantine -# DB_CONNECTIONS = ( -# {"amavis_quarantine" : {"driver" : "", -# "host" : "", -# "dbname" : "", -# "login" : "", -# "password" : ""} -# } -# ) - - #SESSION_COOKIE_AGE = 300 #CACHE_BACKEND = 'file://' diff --git a/static/css/default.css b/static/css/default.css index 68f7e4719..bbb634177 100644 --- a/static/css/default.css +++ b/static/css/default.css @@ -318,6 +318,16 @@ th.center, td.center { text-align: center; } +th[class*=sortable] { + cursor: pointer; + /*-moz-box-shadow: 0px -15px 10px -10px rgba(0,0,0,0.6) inset; + -webkit-box-shadow: 0px -15px 10px -10px rgba(0,0,0,0.6) inset;*/ +} + +th.selected { + background: #EEEEEE; +} + td { border-bottom: 1px solid #C8D6E8; } diff --git a/static/js/email_listing.js b/static/js/email_listing.js index 12ab9572c..97f561ecf 100644 --- a/static/js/email_listing.js +++ b/static/js/email_listing.js @@ -10,16 +10,20 @@ function navbarcallback(event) { } function updatelisting(resp) { - if ($defined(resp.menu)) { - $("menubar").set("html", resp.menu); - parse_menubar("menubar"); - } - if ($defined(resp.listing)) { - $("listing").set("html", resp.listing); - } - if ($defined(resp.navbar)) { - $("navbar").set("html", resp.navbar); - $("navbar").getChildren("a").addEvent("click", navbarcallback); - } + if ($defined(resp.url)) { + current_anchor.baseurl(resp.url).update(); + return; + } + if ($defined(resp.menu)) { + $("menubar").set("html", resp.menu); + parse_menubar("menubar"); + } + if ($defined(resp.listing)) { + $("listing").set("html", resp.listing); + } + if ($defined(resp.navbar)) { + $("navbar").set("html", resp.navbar); + $("navbar").getChildren("a").addEvent("click", navbarcallback); + } } diff --git a/templates/common/email_listing.html b/templates/common/email_listing.html index 970a55550..54fce2f75 100644 --- a/templates/common/email_listing.html +++ b/templates/common/email_listing.html @@ -23,6 +23,37 @@ function viewmail(event) { } } +function init_sortable_columns() { + $$("th[class*=sortable]").addEvent("click", function(evt) { + var tmp = "?"; + var order = (navparams.has("order")) ? navparams.get("order") : ""; + + if (order == "") { + order = current_anchor.getparam("order"); + } + if (order.substring(1) == evt.target.id) { + var sign = (order.charAt(0) == '+') ? '-' : '+'; + order = sign + evt.target.id; + } else { + order = "-" + evt.target.id; + } + navparams.set("order", order); + current_anchor.setparams(navparams).update(); + }); +} + +function update_sortable_column() { + if (!navparams.has("order")) { + return; + } + var src = navparams.get("order").charAt(0) == "-" ? "down_arrow.png" : "up_arrow.png"; + var th = $$("th[id=" + navparams.get("order").substring(1) + "]"); + + th.getElement("img").set("src", static_url("pics/" + src)); + th.getElement("img").setStyle("display", "block"); + th.addClass("selected"); +} + window.addEvent("domready", function() { navparams = new Hash({ {% join navparams %} diff --git a/templates/common/email_searchbar.html b/templates/common/email_searchbar.html index ab48d7e13..d214763fe 100644 --- a/templates/common/email_searchbar.html +++ b/templates/common/email_searchbar.html @@ -5,6 +5,9 @@
{% trans "From address" %}
{% trans "Subject" %}
+ {% for opt in extraopts %} + {{ opt.label }}
+ {% endfor %} {% trans "Both" %}