From 7338dd1c8fe8ab76016df9a098a801c6077648ed Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sun, 19 Jun 2011 19:11:02 +0200 Subject: [PATCH] First version of the sieve plugin is over \o/ It needs more testing but all functionalities are implemented. Maybe some extra sieve extensions would be nice but I'll see that based on user feedback. I also include some general refactoring in this commit. see #139 --- admin/templatetags/admin_extras.py | 15 +- extensions/amavis_quarantine/sql_listing.py | 3 +- .../templates/amavis_quarantine/index.html | 4 +- extensions/sievefilters/digest_md5.py | 63 -- extensions/sievefilters/forms.py | 211 ++++++ extensions/sievefilters/lib.py | 21 +- extensions/sievefilters/main.py | 8 +- extensions/sievefilters/managesieve.py | 620 ------------------ extensions/sievefilters/static/css/style.css | 190 +++++- .../sievefilters/static/js/dynamic_fields.js | 208 ++++++ extensions/sievefilters/static/js/lib.js | 201 ++++++ .../templates/sievefilters/filter.html | 79 +++ .../templates/sievefilters/filters.html | 27 + .../templates/sievefilters/guieditor.html | 8 + .../templates/sievefilters/index.html | 135 +--- .../templates/sievefilters/newfiltersset.html | 22 + .../templates/sievefilters/rawfilter.html | 8 +- .../templatetags/sfilters_tags.py | 141 +++- extensions/sievefilters/urls.py | 21 +- extensions/sievefilters/views.py | 188 +++++- extensions/webmail/lib.py | 30 +- extensions/webmail/static/js/webmail.js | 6 +- .../webmail/templates/webmail/index.html | 12 +- lib/__init__.py | 21 +- lib/email_listing.py | 160 ----- lib/emailutils.py | 178 +++++ static/css/box.css | 2 +- static/js/anchor_nav.js | 209 ++++++ static/js/common.js | 197 ------ static/js/domains.js | 5 - static/js/dynamic_textinput.js | 31 +- static/pics/movedown.png | Bin 0 -> 683 bytes static/pics/moveup.png | Bin 0 -> 652 bytes templates/common/email_listing.html | 5 +- 34 files changed, 1709 insertions(+), 1320 deletions(-) delete mode 100644 extensions/sievefilters/digest_md5.py delete mode 100644 extensions/sievefilters/managesieve.py create mode 100644 extensions/sievefilters/static/js/dynamic_fields.js create mode 100644 extensions/sievefilters/static/js/lib.js create mode 100644 extensions/sievefilters/templates/sievefilters/filter.html create mode 100644 extensions/sievefilters/templates/sievefilters/filters.html create mode 100644 extensions/sievefilters/templates/sievefilters/guieditor.html create mode 100644 lib/emailutils.py create mode 100644 static/js/anchor_nav.js create mode 100644 static/pics/movedown.png create mode 100644 static/pics/moveup.png diff --git a/admin/templatetags/admin_extras.py b/admin/templatetags/admin_extras.py index 3df3a625a..49dd4055e 100644 --- a/admin/templatetags/admin_extras.py +++ b/admin/templatetags/admin_extras.py @@ -3,7 +3,7 @@ from django.template.loader import render_to_string from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse from modoboa import admin -from modoboa.lib import events, static_url +from modoboa.lib import events, static_url, render_actions register = template.Library() @@ -141,19 +141,6 @@ def permissions_menu(user): return render_to_string('common/menu.html', {"entries" : entries, "user" : user}) -def render_actions(actions): - t = template.Template(""" -{% for a in actions %} - - - -{% endfor %} -""") - return t.render(template.Context({ - "actions" : actions - })) - @register.simple_tag def domain_actions(user, domid): actions = [ diff --git a/extensions/amavis_quarantine/sql_listing.py b/extensions/amavis_quarantine/sql_listing.py index 1f75123f5..d10c83e80 100644 --- a/extensions/amavis_quarantine/sql_listing.py +++ b/extensions/amavis_quarantine/sql_listing.py @@ -3,7 +3,8 @@ from datetime import datetime from django.utils.translation import ugettext as _ from modoboa.lib import tables, db -from modoboa.lib.email_listing import MBconnector, EmailListing, Email +from modoboa.lib.email_listing import MBconnector, EmailListing +from modoboa.lib.emailutils import * class Qtable(tables.Table): tableid = "emails" diff --git a/extensions/amavis_quarantine/templates/amavis_quarantine/index.html b/extensions/amavis_quarantine/templates/amavis_quarantine/index.html index 8ef7e1ec7..359dcc0b9 100644 --- a/extensions/amavis_quarantine/templates/amavis_quarantine/index.html +++ b/extensions/amavis_quarantine/templates/amavis_quarantine/index.html @@ -30,7 +30,7 @@ {% endblock %} {% block js %} - register_callback("_listing", listing_cb); - register_callback("viewmail", viewmail_cb); + current_anchor.register_callback("_listing", listing_cb); + current_anchor.register_callback("viewmail", viewmail_cb); {% endblock %} {% block middletitle %}{% trans "Quarantine" %}{% endblock %} diff --git a/extensions/sievefilters/digest_md5.py b/extensions/sievefilters/digest_md5.py deleted file mode 100644 index 75c7aa0ed..000000000 --- a/extensions/sievefilters/digest_md5.py +++ /dev/null @@ -1,63 +0,0 @@ -# coding: utf-8 - -import base64 -import hashlib -import binascii -import re -import random - -class DigestMD5(object): - def __init__(self, challenge, digesturi): - self.__digesturi = digesturi - self.__challenge = challenge - - self.__params = {} - pexpr = re.compile('(\w+)="(.+)"') - for elt in base64.b64decode(challenge).split(","): - m = pexpr.match(elt) - if m is None: - continue - self.__params[m.group(1)] = m.group(2) - - def __make_cnonce(self): - ret = "" - for i in xrange(12): - ret += chr(random.randint(0, 0xff)) - return base64.b64encode(ret) - - def __digest(self, value): - return hashlib.md5(value).digest() - - def __hexdigest(self, value): - return binascii.hexlify(hashlib.md5(value).digest()) - - def __make_response(self, username, password, check=False): - a1 = "%s:%s:%s" % (self.__digest("%s:%s:%s" % (username, self.realm, password)), - self.__params["nonce"], self.cnonce) - if check: - a2 = ":%s" % self.__digesturi - else: - a2 = "AUTHENTICATE:%s" % self.__digesturi - resp = "%s:%s:00000001:%s:auth:%s" \ - % (self.__hexdigest(a1), self.__params["nonce"], - self.cnonce, self.__hexdigest(a2)) - - return self.__hexdigest(resp) - - def response(self, username, password): - self.realm = self.__params["realm"] if self.__params.has_key("realm") else "" - self.cnonce = self.__make_cnonce() - respvalue = self.__make_response(username, password) - - dgres = 'username="%s",%snonce="%s",cnonce="%s",nc=00000001,qop=auth,' \ - 'digest-uri="%s",response=%s' \ - % (username, ('realm="%s",' % self.realm) if len(self.realm) else "", - self.__params["nonce"], self.cnonce, self.__digesturi, respvalue) - - return base64.b64encode(dgres) - - def check_last_challenge(self, username, password, value): - challenge = base64.b64decode(value.strip('"')) - return challenge == \ - ("rspauth=%s" % self.__make_response(username, password, True)) - diff --git a/extensions/sievefilters/forms.py b/extensions/sievefilters/forms.py index 87167010a..eae16948d 100644 --- a/extensions/sievefilters/forms.py +++ b/extensions/sievefilters/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.http import QueryDict from django.utils.translation import ugettext as _ from modoboa.admin.templatetags.admin_extras import gender @@ -8,3 +9,213 @@ class FiltersSetForm(forms.Form): initial=False, help_text=_("Check to activate this filters set")) +header_operators = [("contains", _("contains"), "string"), + ("notcontains", _("does not contain"), "string"), + ("is", _("is"), "string"), + ("isnot", _("is not"), "string")] + +class FilterForm(forms.Form): + name = forms.CharField(label=_("Name")) + match_type = forms.ChoiceField(choices=[("allof", _("Match all of the following")), + ("anyof", _("Match any of the following")), + ("all", _("Match all messages"))], + initial="anyof", + widget=forms.RadioSelect) + + cond_templates = [ + {"name" : "Subject", "label" : _("Subject"), "operators" : header_operators}, + {"name" : "From", "label" : _("Sender"), "operators" : header_operators}, + {"name" : "To", "label" : _("Recipient"), "operators" : header_operators}, + {"name" : "Cc", "label" : _("Cc"), "operators" : header_operators}, + {"name" : "size", "label" : _("Size"), + "operators" : [("over", _("is greater than"), "number"), + ("under", _("is less than"), "number")]}, + ] + + action_templates = [ + {"name" : "fileinto", "label" : _("Move message to"), + "args" : [{"type" : "list", "vloader" : "userfolders"}]}, + {"name" : "redirect", "label" : _("Redirect message to"), + "args" : [{"type" : "string"}]}, + ] + + def __init__(self, conditions, actions, request, *args, **kwargs): + super(FilterForm, self).__init__(*args, **kwargs) + + self.conds_cnt = 0 + for c in conditions: + getattr(self, "_build_%s_field" % c[0])(c[1], c[2]) + self.actions_cnt = 0 + for a in actions: + getattr(self, "_build_%s_field" % a[0])(request, a[1]) + + def _build_header_field(self, name, op, value): + targets = [] + ops = [] + vfield = None + for tpl in FilterForm.cond_templates: + targets += [(tpl["name"], tpl["label"]),] + if tpl["name"] != name: + continue + for opdef in tpl["operators"]: + ops += [opdef[:2]] + if op != opdef[0]: + continue + if opdef[2] in ["string", "number"]: + vfield = forms.CharField(max_length=255, initial=value) + + self.fields["cond_target_%d" % self.conds_cnt] = \ + forms.ChoiceField(initial=name, choices=targets) + self.fields["cond_operator_%d" % self.conds_cnt] = \ + forms.ChoiceField(initial=op, choices=ops) + self.fields["cond_value_%d" % self.conds_cnt] = vfield + self.conds_cnt += 1 + + def _build_Subject_field(self, op, value): + self._build_header_field("Subject", op, value) + + def _build_To_field(self, op, value): + self._build_header_field("To", op, value) + + def _build_From_field(self, op, value): + self._build_header_field("From", op, value) + + def _build_Cc_field(self, op, value): + self._build_header_field("Cc", op, value) + + def _build_size_field(self, op, value): + self._build_header_field("size", op, value) + + def _build_action_field(self, request, name, value): + actions = [] + args = None + for tpl in FilterForm.action_templates: + actions += [(tpl["name"], tpl["label"]),] + if name == tpl["name"]: + args = tpl["args"] + self.fields["action_name_%d" % self.actions_cnt] = \ + forms.ChoiceField(initial=name, choices=actions) + for cnt in xrange(0, len(args)): + arg = args[cnt] + aname = "action_arg_%d_%d" % (self.actions_cnt, cnt) + if arg["type"] == "string": + self.fields[aname] = forms.CharField(max_length=255, initial=value) + elif arg["type"] == "list": + choices = getattr(self, arg["vloader"])(request) + self.fields[aname] = forms.ChoiceField(initial=value, choices=choices) + self.actions_cnt += 1 + + def _build_redirect_field(self, request, value): + self._build_action_field(request, "redirect", value) + + def _build_fileinto_field(self, request, value): + self._build_action_field(request, "fileinto", value) + + def __build_folders_list(self, folders): + ret = [] + for fd in folders: + value = fd["path"] if fd.has_key("path") else fd["name"] + ret += [(value, fd["name"])] + if fd.has_key("sub") and len(fd["sub"]): + ret += self.__build_folders_list(fd["sub"]) + return ret + + def userfolders(self, request): + from modoboa.extensions.webmail.lib import IMAPconnector + + mbc = IMAPconnector(user=request.user.username, + password=request.session["password"]) + ret = mbc.getfolders(request.user, unseen_messages=False) + + folders = self.__build_folders_list(ret) + return folders + + def tofilter(self): + conditions = [] + actions = [] + for cpt in xrange(0, self.conds_cnt): + conditions += [(self.cleaned_data["cond_target_%d" % cpt], + ":" + self.cleaned_data["cond_operator_%d" % cpt], + self.cleaned_data["cond_value_%d" % cpt])] + for cpt in xrange(0, self.actions_cnt): + naction = (self.cleaned_data["action_name_%d" % cpt],) + argcpt = 0 + while True: + try: + naction += (self.cleaned_data["action_arg_%d_%d" % (cpt, argcpt)],) + except KeyError: + break + argcpt += 1 + actions += [naction] + + return (conditions, actions) + +def build_filter_form_from_qdict(request): + conditions = [] + actions = [] + qdict = QueryDict("", mutable=True) + qdict["name"] = request.POST["name"] + qdict["match_type"] = request.POST["match_type"] + cpt = 0 + i = 0 + while True: + if cpt == int(request.POST["conds"]): + break + if request.POST.has_key("cond_target_%d" % i): + qdict["cond_target_%d" % cpt] = request.POST["cond_target_%d" % i] + qdict["cond_operator_%d" % cpt] = request.POST["cond_operator_%d" % i] + qdict["cond_value_%d" % cpt] = request.POST["cond_value_%d" % i] + condtarget = request.POST["cond_target_%d" % i] + condop = request.POST["cond_operator_%d" % i] + condvalue = request.POST["cond_value_%d" % i] + conditions += [(condtarget, condop, condvalue)] + cpt += 1 + i += 1 + cpt = 0 + i = 0 + while True: + if cpt == int(request.POST["actions"]): + break + if request.POST.has_key("action_name_%d" % i): + qdict["action_name_%d" % cpt] = request.POST["action_name_%d" % i] + action = request.POST["action_name_%d" % i] + argcpt = 0 + args = [] + while True: + try: + qdict["action_arg_%d_%d" % (cpt, argcpt)] = \ + request.POST["action_arg_%d_%d" % (i, argcpt)] + args += [request.POST["action_arg_%d_%d" % (i, argcpt)]] + except KeyError: + break + argcpt += 1 + args = [action] + args + actions += [args] + cpt += 1 + i += 1 + + return FilterForm(conditions, actions, request, qdict) + +def build_filter_form_from_filter(request, name, fobj): + from sievelib.commands import SizeCommand + + match_type = fobj["test"].name + conditions = [] + for t in fobj["test"]["tests"]: + if isinstance(t, SizeCommand): + conditions += [("size", t["comparator"][1:], t["limit"])] + else: + conditions += [(t["header-names"].strip('"'), + t["match-type"][1:], + t["key-list"].strip('"'))] + actions = [] + for c in fobj.children: + action = (c.name,) + for arg in c.args_definition: + action += (c[arg["name"]].strip('"'),) + actions += [action] + + form = FilterForm(conditions, actions, request) + form.fields["name"].initial = name + form.fields["match_type"].initial = match_type + return form diff --git a/extensions/sievefilters/lib.py b/extensions/sievefilters/lib.py index 05928185f..7c39448af 100644 --- a/extensions/sievefilters/lib.py +++ b/extensions/sievefilters/lib.py @@ -1,7 +1,9 @@ # coding: utf-8 from django.utils.translation import ugettext as _ -from managesieve import ManageSieveClient, Error +from sievelib.managesieve import Client, Error +from sievelib.parser import Parser +from sievelib.factory import FiltersSet from modoboa.lib import parameters from modoboa.lib.connections import ConnectionsManager, ConnectionError @@ -17,9 +19,9 @@ class SieveClient(object): raise ConnectionError(msg) def login(self, user, password): - self.msc = ManageSieveClient(parameters.get_admin("SERVER"), - int(parameters.get_admin("PORT")), - debug=False) + self.msc = Client(parameters.get_admin("SERVER"), + int(parameters.get_admin("PORT")), + debug=False) use_starttls = True if parameters.get_admin("STARTTLS") == "yes" else False authmech = parameters.get_admin("AUTHENTICATION_MECH") if authmech == "AUTO": @@ -42,11 +44,18 @@ class SieveClient(object): def listscripts(self): return self.msc.listscripts() - def getscript(self, name): + def getscript(self, name, format="raw"): content = self.msc.getscript(name) if content is None: raise SieveClientError(self.msc.errmsg) - return content + if format == "raw": + return content + p = Parser() + if not p.parse(content): + return None + fs = FiltersSet(name) + fs.from_parser_result(p) + return fs def pushscript(self, name, content, active=False): if not self.msc.havespace(name, len(content)): diff --git a/extensions/sievefilters/main.py b/extensions/sievefilters/main.py index 094ad30c6..996bde87d 100644 --- a/extensions/sievefilters/main.py +++ b/extensions/sievefilters/main.py @@ -3,7 +3,7 @@ from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse from django.conf.urls.defaults import include from modoboa.lib import events, parameters, static_url -from managesieve import SUPPORTED_AUTH_MECHS +from sievelib.managesieve import SUPPORTED_AUTH_MECHS def init(): events.register("UserMenuDisplay", menu) @@ -22,6 +22,12 @@ def init(): parameters.register_admin("AUTHENTICATION_MECH", type="list", deflt="auto", values=values, help=_("Prefered authentication mechanism")) + + # User parameters + parameters.register_user("EDITOR_MODE", type="list", deflt="raw", + label=_("Editor mode"), + values=[("raw", "raw"), ("gui", "simplified")], + help=_("Select the mode you want the editor to work in")) def destroy(): events.unregister("UserMenuDisplay", menu) diff --git a/extensions/sievefilters/managesieve.py b/extensions/sievefilters/managesieve.py deleted file mode 100644 index 2b6e22b31..000000000 --- a/extensions/sievefilters/managesieve.py +++ /dev/null @@ -1,620 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -"""MANAGESIEVE client. - -A protocol for securely managing Sieve scripts on a remote server. -This protocol allows a user to have multiple scripts, and also alerts -a user to syntactically flawed scripts. - -Implementation based on . -""" - -import sys, os -import socket -import re -import base64 -import ssl - -from digest_md5 import DigestMD5 - -CRLF = '\r\n' - -KNOWN_CAPABILITIES = ["IMPLEMENTATION", "SASL", "SIEVE", - "STARTTLS", "NOTIFY", "LANGUAGE", - "RENAME", "NOOP", "UNAUTHENTICATE"] - -SUPPORTED_AUTH_MECHS = ["DIGEST-MD5", "PLAIN", "LOGIN"] - -class Error(Exception): - pass - -class Response(Exception): - def __init__(self, code, data): - self.code = code - self.data = data - - def __str__(self): - return "%s %s" % (self.code, self.data) - -def authentication_required(meth): - """Simple class method decorator. - - Checks if the client is currently connected. - - :param meth: the original called method - """ - def check(cls, *args, **kwargs): - if cls.authenticated: - return meth(cls, *args, **kwargs) - raise Error("Authentication required") - return check - -class ManageSieveClient: - read_size = 4096 - read_timeout = 5 - - def __init__(self, srvaddr, srvport=2000, debug=False): - self.srvaddr = srvaddr - self.srvport = srvport - self.__debug = debug - self.sock = None - self.__read_buffer = "" - self.authenticated = False - self.errcode = None - - self.__capabilities = {} - self.__respcode_expr = re.compile("(OK|NO|BYE)\s*(.+)?") - self.__error_expr = re.compile('(\(\w+\))?\s*(".+")') - self.__size_expr = re.compile("\{(\d+)\+?\}") - self.__active_expr = re.compile("ACTIVE", re.IGNORECASE) - - def __del__(self): - if self.sock is not None: - self.sock.close() - self.sock = None - - def __dprint(self, message): - if not self.__debug: - return - print "DEBUG: %s" % message - - def __read_block(self, size): - """Read a block of 'size' bytes from the server. - - An internal buffer is used to read data from the server. If - enough data is available from it, we return that data. - - Eventually, we try to grab the missing part from the server - for ManageSieveClient.read_timeout seconds. If no data can be - retrieved, it is considered as a fatal error and an 'Error' - exception is raised. - - :param size: number of bytes to read - :rtype: string - :returns: the read block (can be empty) - """ - buf = "" - if len(self.__read_buffer): - limit = size if size <= len(self.__read_buffer) else len(self.__read_buffer) - buf = self.__read_buffer[:limit] - self.__read_buffer = self.__read_buffer[limit:] - size -= limit - if not size: - return buf - try: - buf += self.sock.recv(size) - except (socket.timeout, ssl.SSLError), e: - raise Error("Failed to read %d bytes from the server" % size) - return buf - - def __read_line(self): - """Read one line from the server. - - An internal buffer is used to read data from the server - (blocks of ManageSieveClient.read_size bytes). If the buffer - is not empty, we try to find an entire line to return. - - If we failed, we try to read new content from the server for - ManageSieveClient.read_timeout seconds. If no data can be - retrieved, it is considered as a fatal error and an 'Error' - exception is raised. - - :rtype: string - :return: the read line - """ - ret = "" - while True: - try: - pos = self.__read_buffer.index(CRLF) - ret = self.__read_buffer[0:pos] - self.__read_buffer = self.__read_buffer[pos+len(CRLF):] - break - except ValueError: - pass - try: - nval = self.sock.recv(self.read_size) - if not len(nval): - break - self.__read_buffer += nval - except (socket.timeout, ssl.SSLError): - raise Error("Failed to read data from the server") - if len(ret): - m = self.__respcode_expr.match(ret) - if m: - if m.group(1) == "BYE": - raise Error("Connection closed by server") - if m.group(1) == "NO": - self.__parse_error(m.group(2)) - raise Response(m.group(1), m.group(2)) - return ret - - def __read_response(self, nblines=-1): - """Read a response from the server. - - In the usual case, we read lines until we find one that looks - like a response (OK|NO|BYE\s*(.+)?). - - If *nblines* > 0, we read excactly nblines before returning. - - :param nblines: number of lines to read (default : -1) - :rtype: tuple - :return: a tuple of the form (code, data, response). If - nblines is provided, code and data can be equal to None. - """ - resp, code, data = ("", None, None) - cpt = 0 - while True: - try: - line = self.__read_line() - except Response, inst: - code = inst.code - data = inst.data - break - if not len(line): - break - resp += line + CRLF - cpt += 1 - if nblines != -1 and cpt == nblines: - break - - return (code, data, resp) - - def __prepare_args(self, args): - """Format command arguments before sending them. - - Command arguments of type string must be quoted, the only - exception concerns size indication (of the form {\d\+?}). - - :param args: list of arguments - :return: a list for transformed arguments - """ - ret = [] - for a in args: - if type(a) in [str, unicode] and self.__size_expr.match(a) is None: - ret += ['"%s"' % a] - continue - ret += ["%s" % str(a)] - return ret - - def __send_command(self, name, args=[], withcontent=False, extralines=[], nblines=-1): - """Send a command to the server. - - If args is not empty, we concatenate the given command with - the content of this list. If extralines is not empty, they are - sent one by one to the server. (CLRF are automatically appended to them) - - We wait for a response just after the command has been sent. - - :param name: the command to sent - :param args: a list of arguments for this command - :param withcontent: tells the function to return the server's response or not - :param extralines: a list of extra lines to sent after the command - :param nblines: the number of response lines to read (all by default) - - :returns: a tuple of the form (code, data[, response]) - """ - tosend = name - if len(args): - tosend += " " + " ".join(self.__prepare_args(args)) - self.__dprint("Command: %s" % tosend) - self.sock.send("%s%s" % (tosend, CRLF)) - for l in extralines: - self.sock.send("%s%s" % (l, CRLF)) - code, data, content = self.__read_response(nblines) - - if withcontent: - return (code, data, content) - return (code, data) - - def __get_capabilities(self): - code, data, capabilities = self.__read_response() - if code == "NO": - return False - - for l in capabilities.splitlines()[0:-2]: - parts = l.split(None, 1) - cname = parts[0].strip('"') - if not cname in KNOWN_CAPABILITIES: - continue - self.__capabilities[cname] = \ - parts[1].strip('"') if len(parts) > 1 else None - return True - - def __parse_error(self, text): - """Parse an error received from the server. - - if text corresponds to a size indication, we grab the - remaining content from the server. - - Otherwise, we try to match an error of the form \(\w+\)?\s*".+" - - On succes, the two public members errcode and errmsg are - filled with the parsing results. - - :param text: the response to parse - """ - m = self.__size_expr.match(text) - if m is not None: - self.errcode = "" - self.errmsg = self.__read_block(int(m.group(1)) + 2) - return - - m = self.__error_expr.match(text) - if m is None: - raise Error("Bad error message") - if m.group(1) is not None: - self.errcode = m.group(1).strip("()") - else: - self.errcode = "" - self.errmsg = m.group(2).strip('"') - - def _plain_authentication(self, login, password): - """SASL PLAIN authentication - - :param login: username - :param password: clear password - :return: True on success, False otherwise. - """ - params = base64.b64encode('\0' + '\0'.join([login, password])) - code, data = self.__send_command("AUTHENTICATE", ["PLAIN", params]) - if code == "OK": - return True - return False - - def _login_authentication(self, login, password): - """SASL LOGIN authentication - - :param login: username - :param password: clear password - :return: True on success, False otherwise. - """ - extralines = ['"%s"' % base64.b64encode(login), - '"%s"' % base64.b64encode(password)] - code, data = self.__send_command("AUTHENTICATE", ["LOGIN"], - extralines=extralines) - if code == "OK": - return True - return False - - def _digest_md5_authentication(self, login, password): - """SASL DIGEST-MD5 authentication - - :param login: username - :param password: clear password - :return: True on success, False otherwise. - """ - code, data, challenge = \ - self.__send_command("AUTHENTICATE", ["DIGEST-MD5"], - withcontent=True, nblines=1) - dmd5 = DigestMD5(challenge, "sieve/%s" % self.srvaddr) - - code, data, challenge = \ - self.__send_command('"%s"' % dmd5.response(login, password), - withcontent=True, nblines=1) - if not len(challenge): - return False - if not dmd5.check_last_challenge(login, password, challenge): - self.errmsg = "Bad challenge received from server" - return False - code, data = self.__send_command('""') - if code == "OK": - return True - return False - - def __authenticate(self, login, password, authmech=None): - """AUTHENTICATE command - - Actually, it is just a wrapper to the real commands (one by - mechanism). We try all supported mechanisms (from the - strongest to the weakest) until we find one supported by the - server. - - Then we try to authenticate (only once). - - :param login: username - :param password: clear password - :param authmech: prefered authentication mechanism - :return: True on success, False otherwise - """ - if not self.__capabilities.has_key("SASL"): - raise Error("SASL not supported by the server") - srv_mechanisms = self.get_sasl_mechanisms() - - if authmech is None or authmech not in SUPPORTED_AUTH_MECHS: - mech_list = SUPPORTED_AUTH_MECHS - else: - mech_list = [authmech] - - for mech in mech_list: - if not mech in srv_mechanisms: - continue - mech = mech.lower() - mech = re.sub("-", "_", mech) - if getattr(self, "_%s_authentication" % mech.lower())(login, password): - self.authenticated = True - return True - return False - - self.errmsg = "No suitable mechanism found" - return False - - def __starttls(self, keyfile=None, certfile=None): - """STARTTLS command - - See MANAGESIEVE specifications, section 2.2. - - :param keyfile: an eventual private key to use - :param certfile: an eventual certificate to use - :rtype: boolean - """ - if not self.has_tls_support(): - raise Error("STARTTLS not supported by the server") - code, data = self.__send_command("STARTTLS") - if code != "OK": - return False - try: - nsock = ssl.wrap_socket(self.sock, keyfile, certfile) - except ssl.SSLError, e: - raise Error("SSL error: %s" % str(e)) - self.sock = nsock - self.__capabilities = {} - self.__get_capabilities() - return True - - def get_implementation(self): - """Returns the IMPLEMENTATION value. - - It is read from server capabilities. (see the CAPABILITY - command) - - :rtype: string - """ - return self.__capabilities["IMPLEMENTATION"] - - def get_sasl_mechanisms(self): - """Returns the supported authentication mechanisms. - - They're read from server capabilities. (see the CAPABILITY - command) - - :rtype: list of string - """ - return self.__capabilities["SASL"].split() - - def has_tls_support(self): - """Tells if the server has STARTTLS support or not. - - It is read from server capabilities. (see the CAPABILITY - command) - - :rtype: boolean - """ - return self.__capabilities.has_key("STARTTLS") - - def get_sieve_capabilities(self): - """Returns the SIEVE extensions supported by the server. - - They're read from server capabilities. (see the CAPABILITY - command) - - :rtype: string - """ - if type(self.__capabilities["SIEVE"]) == str: - self.__capabilities["SIEVE"] = self.__capabilities["SIEVE"].split() - return self.__capabilities["SIEVE"] - - def connect(self, login, password, starttls=False, authmech=None): - """Establish a connection with the server. - - This function must be used. It read the server capabilities - and wraps calls to STARTTLS and AUTHENTICATE commands. - - :param login: username - :param password: clear password - :param starttls: use a TLS connection or not - :param authmech: prefered authenticate mechanism - :rtype: boolean - """ - try: - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.connect((self.srvaddr, self.srvport)) - self.sock.settimeout(ManageSieveClient.read_timeout) - except socket.error, msg: - raise Error("Connection to server failed: %s" % str(msg)) - - if not self.__get_capabilities(): - raise Error("Failed to read capabilities from server") - if starttls and not self.__starttls(): - return False - if self.__authenticate(login, password, authmech): - return True - return False - - def logout(self): - """Disconnect from the server - - See MANAGESIEVE specifications, section 2.3 - """ - self.__send_command("LOGOUT") - - def capability(self): - """Ask server capabilities. - - See MANAGESIEVE specifications, section 2.4 This command does - not affect capabilities recorded by this client. - - :rtype: string - """ - code, data, capabilities = \ - self.__send_command("CAPABILITY", withcontent=True) - if code == "OK": - return capabilities - return None - - @authentication_required - def havespace(self, scriptname, scriptsize): - """Ask for available space. - - See MANAGESIEVE specifications, section 2.5 - - :param scriptname: script's name - :param scriptsize: script's size - :rtype: boolean - """ - code, data = self.__send_command("HAVESPACE", [scriptname, scriptsize]) - if code == "OK": - return True - return False - - @authentication_required - def listscripts(self): - """List available scripts. - - See MANAGESIEVE specifications, section 2.7 - - :returns: a 2-uple (active script, [script1, ...]) - """ - code, data, listing = self.__send_command("LISTSCRIPTS", withcontent=True) - if code == "NO": - return None - ret = [] - active_script = None - for l in listing.splitlines(): - if self.__size_expr.match(l): - continue - m = re.match('"([^"]+)"\s*(.+)', l) - if m is None: - ret += [l.strip('"')] - else: - if self.__active_expr.match(m.group(2)): - active_script = m.group(1) - else: - ret += [m.group(1)] - self.__dprint(ret) - return (active_script, ret) - - @authentication_required - def getscript(self, name): - """Download a script from the server - - See MANAGESIEVE specifications, section 2.9 - - :param name: script's name - :rtype: string - :returns: the script's content on succes, None otherwise - """ - code, data, content = self.__send_command("GETSCRIPT", [name], withcontent=True) - if code == "OK": - lines = content.splitlines() - if self.__size_expr.match(lines[0]) is not None: - lines = lines[1:] - return "\n".join(lines) - return None - - @authentication_required - def putscript(self, name, content): - """Upload a script to the server - - See MANAGESIEVE specifications, section 2.6 - - :param name: script's name - :param content: script's content - :rtype: boolean - """ - content = "{%d+}%s%s" % (len(content), CRLF, content) - code, data = \ - self.__send_command("PUTSCRIPT", [name, content]) - if code == "OK": - return True - return False - - @authentication_required - def deletescript(self, name): - """Delete a script from the server - - See MANAGESIEVE specifications, section 2.10 - - :param name: script's name - :rtype: boolean - """ - code, data = self.__send_command("DELETESCRIPT", [name]) - if code == "OK": - return True - return False - - @authentication_required - def renamescript(self, oldname, newname): - """Rename a script on the server - - See MANAGESIEVE specifications, section 2.11.1 - - As this command is optional, we emulate it if the server does - not support it. - - :param oldname: current script's name - :param newname: new script's name - :rtype: boolean - """ - if self.__capabilities.has_key("RENAMESCRIPT"): - code, data = self.__send_command("RENAMESCRIPT", [oldname, newname]) - if code == "OK": - return True - return False - - (active_script, scripts) = self.listscripts() - if scripts == None or not oldname in scripts: - self.errmsg = "Old script does not exist" - return False - if newname in scripts: - self.errmsg = "New script already exists" - return False - oldscript = self.getscript(oldname) - if oldscript is None: - return False - if not self.putscript(newname, oldscript): - return False - if active_script == oldname: - if not self.setactive(newname): - return False - if not self.deletescript(oldname): - return False - return True - - @authentication_required - def setactive(self, scriptname): - """Define the active script - - See MANAGESIEVE specifications, section 2.8 - - If scriptname is empty, the current active script is disabled, - ie. there will be no active script anymore. - - :param scriptname: script's name - :rtype: boolean - """ - code, data = self.__send_command("SETACTIVE", [scriptname]) - if code == "OK": - return True - return False - diff --git a/extensions/sievefilters/static/css/style.css b/extensions/sievefilters/static/css/style.css index 6d516fb32..298dcf4f3 100644 --- a/extensions/sievefilters/static/css/style.css +++ b/extensions/sievefilters/static/css/style.css @@ -1,58 +1,65 @@ -#sets_list { +#fsets_list { + margin-left: 5px; + float: left; + display: inline; +} + +#fset_menu { position: absolute; - border: 1px solid #CCC; - left: 10px; top: 0px; - bottom: 10px; - width: 160px; - + left: 10px; } #set_content { position: absolute; - left: 180px; + left: 10px; right: 10px; top: 0px; bottom: 10px; padding: 0; } -#sets_list ul { - list-style: none; - padding: 5px 0 0 5px; +#set_content h1 { + padding: 2px 5px; margin: 0; + font-size: 1.2em; + background: #CEDBEC; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; } -#sets_list li { - margin: 2px 0; -} - -#sets_list a, #sets_list a:visited { - color: black; - text-decoration: none; -} - -#sets_list a:hover { - color: #6AB0C5; - text-decoration: none; -} - -#sets_list li.selected { - background-color: #FFFF77; -} - -#sets_list img { - float: right; -} - -#set_content form { - height: 100%; +#fsets_bar { + margin-left: 10px; + width: 250px; } #set_content textarea { margin: 0; width: 100%; height: 100%; + padding: 2px; +} + +#set_content label { + margin: 5px; +} + +#filters_list { + border-left: 1px solid #CCC; + position: absolute; + padding-left: 5px; + left: 190px; + right: 10px; + top: 0px; + bottom: 0px; +} + +#feditor { + position: absolute; + left: 190px; + right: 10px; + top: 0px; + bottom: 0px; } #id_content { @@ -60,6 +67,23 @@ height: 270px; } +/* Filter form */ +#filterform { + margin: 7px; +} + +#filterform h2 { + margin: 0; + padding: 0; + font-size: 1em; + margin-top: 20px; +} + +#filterform #id_name { + margin-top: 10px; + width: 200px; +} + #submit_container { width: 100%; position: absolute; @@ -69,4 +93,102 @@ #error { white-space: pre-line; +} + +#condlist { + border: 1px solid #CCC; + padding: 5px; +} + +#actionlist { + border: 1px solid #CCC; + padding-top: 10px; +} + +#condlist ul { + margin: 0; + padding: 0; + list-style: none; + display: inline; +} + +#condlist li { + margin: 0; + padding: 0; + display: inline; +} + +#matchtype { + margin-bottom: 10px; +} + +#submit { + margin-top: 20px; +} + +div.item { + margin-bottom: 10px; + margin-left: 5px; +} + +div.item input, div.item select { + margin: 0 2px; +} + +#condlist label { + margin: 0 !important; + padding: 0; +} + +ul.errors { + list-style: none; + display: inline; + margin: 0; + padding: 0; +} + +.errors li { + color: red; + font-style: bold; + display: inline; +} + +.leftbox { + width: 155px; +} + +.leftbox h1 { + float: none !important; + padding: 0; + margin: 0; + font-size: 1.1em; +} + +.leftbox ul { + list-style: none; + padding: 5px 0 0 0; + margin: 0; +} + +.leftbox li { + margin: 5px 0; +} + +.leftbox a, .leftbox a:visited { + color: black; + text-decoration: none; +} + +.leftbox a:hover { + color: #6AB0C5; + text-decoration: none; +} + +.leftbox li.selected { + background-color: #FFFF77; +} + +.leftbox img { + float: left; + margin-right: 5px; } \ No newline at end of file diff --git a/extensions/sievefilters/static/js/dynamic_fields.js b/extensions/sievefilters/static/js/dynamic_fields.js new file mode 100644 index 000000000..0dcafb5f4 --- /dev/null +++ b/extensions/sievefilters/static/js/dynamic_fields.js @@ -0,0 +1,208 @@ + +var DynamicCondition = new Class({ + + Extends: DynamicTextInput, + + options: { + defaultcond: "Subject", + templates: [] + }, + + loadoperators: function(container, tpl) { + container.empty(); + for (var ocpt = 0; ocpt < tpl["operators"].length; ocpt++) { + var opt = new Element("option", { + value: tpl["operators"][ocpt][0], + html: tpl["operators"][ocpt][1] + }); + opt.inject(container); + } + }, + + onTargetChange: function(event) { + var parts = event.target.get("name").split("_"); + var container = $("id_cond_operator_" + parts[2]); + + for (var cpt = 0; cpt < this.options.templates.length; cpt++) { + var tpl = this.options.templates[cpt]; + if (tpl["name"] != event.target.get("value")) { + continue; + } + this.loadoperators(container, tpl); + break; + } + }, + + createfield: function() { + var div = new Element("div", {"class": "item"}); + var condtarget = new Element("select", { + id: "id_cond_target_" + this.nextid, + name: "cond_target_" + this.nextid + }); + var condoperator = new Element("select", { + id: "id_cond_operator_" + this.nextid, + name: "cond_operator_" + this.nextid + }); + var condvalue = new Element("input", { + id: "id_cond_value_" + this.nextid, + name: "cond_value_" + this.nextid, + type: "text" + }); + + for (var cpt = 0; cpt < this.options.templates.length; cpt++) { + var tpl = this.options.templates[cpt]; + var opt = new Element("option", { + value: tpl["name"], + html: tpl["label"] + }); + opt.inject(condtarget); + + if (tpl["name"] != this.options.defaultcond) { + continue; + } + this.loadoperators(condoperator, tpl); + } + condtarget.inject(div); + condtarget.addEvent("change", this.onTargetChange.bind(this)); + condoperator.inject(div); + condvalue.inject(div); + return [div, condvalue]; + }, + + removeelement: function(event) { + var id = (event.target.get("id").split("_"))[1]; + + event.stop(); + $("id_cond_target_" + id).destroy(); + $("id_cond_operator_" + id).destroy(); + $("id_cond_value_" + id).destroy(); + event.target.destroy(); + this.fields_cnt--; + } +}); + +var DynamicAction = new Class({ + Extends: DynamicTextInput, + + options: { + defaultaction: "fileinto", + templates: [], + ufolders_url: null + }, + + initialize: function(targets, options) { + this.parent(targets, options); + $("id_action_arg_0_0").getElements("option").each(function(item) { + var cpt = 1; + var start = 0; + var value = item.get("value"); + + while (true) { + var id = value.indexOf(".", start); + if (id == -1) { + break; + } + item.setStyle("margin-left", (cpt * 20) + "px"); + cpt++; + start = id + 1; + } + }); + this.fdoptions = $("id_action_arg_0_0").getElements("option"); + }, + + loadargs: function(id, tpl) { + var res = new Elements(); + + for (var acpt = 0; acpt < tpl["args"].length; acpt++) { + var argname = "action_arg_" + id + "_" + acpt; + var child = null; + + if (tpl["args"][acpt]["type"] == "list") { + child = new Element("select", {id: "id_" + argname, name: argname}); + if ($defined(tpl["args"][acpt]["vloader"])) { + this[tpl["args"][acpt]["vloader"]](child); + } + } else if (tpl["args"][acpt]["type"] == "string") { + child = new Element("input", {type: "text", + id: "id_" + argname, name: argname}); + } + res.push(child); + } + return res; + }, + + deletePrevArgs: function(id) { + for (var cpt = 0; true; cpt++) { + var arg = $("id_action_arg_" + id + "_" + cpt); + if (!$defined(arg)) { + break; + } + arg.destroy(); + } + }, + + onNameChange: function(event) { + var parts = event.target.get("name").split("_"); + var container = event.target.getParent(); + + event.stop(); + this.deletePrevArgs(parts[2]); + for (var cpt = 0; cpt < this.options.templates.length; cpt++) { + var tpl = this.options.templates[cpt]; + if (tpl["name"] != event.target.get("value")) { + continue; + } + var args = this.loadargs(parts[2], tpl); + args.inject(container); + } + }, + + createfield: function() { + var div = new Element("div", {"class": "item"}); + var actionname = new Element("select", { + id: "id_action_name_" + this.nextid, + name: "action_name_" + this.nextid + }); + var args = null; + + for (var cpt = 0; cpt < this.options.templates.length; cpt++) { + var tpl = this.options.templates[cpt]; + var opt = new Element("option", { + value: tpl["name"], + html: tpl["label"] + }); + opt.inject(actionname); + + if (tpl["name"] != this.options.defaultaction) { + continue; + } + args = this.loadargs(this.nextid, tpl); + } + actionname.inject(div); + actionname.addEvent("change", this.onNameChange.bind(this)); + args.inject(div); + return [div, actionname]; + }, + + removeelement: function(event) { + var id = (event.target.get("id").split("_"))[1]; + + event.stop(); + $("id_action_name_" + id).destroy(); + for (var cpt = 0; true; cpt++) { + var arg = $("id_action_arg_" + id + "_" + cpt); + if (!$defined(arg)) { + break; + } + arg.destroy(); + } + event.target.destroy(); + this.fields_cnt--; + }, + + userfolders: function(container) { + this.fdoptions.each(function(item) { + item.clone().inject(container); + }); + } +}); \ No newline at end of file diff --git a/extensions/sievefilters/static/js/lib.js b/extensions/sievefilters/static/js/lib.js new file mode 100644 index 000000000..b498ac05c --- /dev/null +++ b/extensions/sievefilters/static/js/lib.js @@ -0,0 +1,201 @@ +/* + * Plugin : "Sieve filters" + * + * Javascript utilities + */ +var curscript = null; + +/* + * Remove a filter from a filters set. + * + */ +function removefilter(evt) { + evt.stop(); + if (!confirm(gettext("Remove this filter?"))) { + return; + } + new Request.JSON({ + url: this.get("href"), + onSuccess: function(response) { + if (response.status == "ok") { + current_anchor.update(true); + infobox.info(response.respmsg); + infobox.hide(1); + } else { + infobox.error(response.respmsg); + } + } + }).get(); +} + +/* + * Handler listening for the "change" event on the filters sets list. + */ +function changefs(evt) { + evt.stop(); + current_anchor.baseurl(this.get("value")).update(); +} + +/* + * Initializes buttons located inside the filters list + */ +function init_filters_list() { + SqueezeBox.assign($("filters_list").getElements('a[class=boxed]'), { + parse: 'rel' + }); + $$("a[name=togglestate]").addEvent("click", toggle_filter_state); + $$("a[name*=movefilter_]").addEvent("click", move_filter); +} + +/* + * Function called just after a new filters set has been loaded. + * + * It updates the middle DIV and initializes the user interface. + */ +function loadfs(response) { + curscript = current_anchor.getbaseurl(); + $("curfset").set("value", curscript); + $("set_content").set("html", response.content); + parse_menubar("fset_menu"); + if ($defined($("filters_list"))) { + init_filters_list(); + } + $$("a[name=removefilter]").addEvent("click", removefilter); + $$("a[name=activatefs]").addEvent("click", activatefs); + $$("a[name=savefs]").addEvent("click", savefs); + $$("a[name=removefs]").addEvent("click", removefs); +} + +/* + * Common function to send a command that manipulates a filters set. + * + * (remove, activate) + */ +function send_command(cmd, extracb) { + new Request.JSON({ + url: cmd, + onSuccess: function(response) { + if (response.status == "ko") { + infobox.calcsize(300); + infobox.error(response.respmsg); + } else { + if ($defined(extracb)) { + extracb(); + } + if (!response.norefresh) { + current_anchor.update(true); + } + infobox.calcsize(); + infobox.info(response.respmsg); + infobox.hide(2); + } + } + }).get(); +} + +/* + * Common utility used by filters set manipulation functions to + * validate any new asked action. (activate, save, remove) + */ +function check_prereqs(evt, msg) { + evt.stop(); + if (!curscript) { + return false; + } + if ($defined(msg) && !confirm(msg)) { + return false; + } + return true; +} + +/* + * Save the current filters set by applying the modifications to the + * server. + */ +function savefs(evt) { + if (!check_prereqs(evt, gettext("Save changes?"))) { + return; + } + new Request.JSON({ + url: $("feditor").get("action"), + method: $("feditor").get("method"), + data: $("feditor").toQueryString(), + onSuccess: function(response) { + if (response.status == "ko") { + infobox.calcsize(300); + infobox.error(response.respmsg); + } else { + infobox.calcsize(); + infobox.info(response.respmsg); + infobox.hide(2); + } + } + }).send(); +} + +/* + * Remove the current filters set. + * + * On success, the user will be automatically redirected to the active + * filters set. (FIXME: what happens when there is no more script?) + */ +function removefs(evt) { + if (!check_prereqs(evt, gettext("Remove filters set?"))) { + return; + } + send_command(this.get("href"), function() { + $("curfset").getElements("option[value=" + curscript + "]").destroy(); + if (!$("curfset").getElements("option[value=" + curscript + "]").length) { + current_anchor.reset().update(); + location.reload(); + } else { + current_anchor.reset(); + } + }); +} + +/* + * Activate the current filters set. + */ +function activatefs(evt) { + if (!check_prereqs(evt, gettext("Activate this filters set?"))) { + return; + } + send_command( + this.get("href"), + function() { + $("curfset").getElements("option").each(function(item) { + if (item.get("value") == curscript) { + item.set("html", curscript + " (" + gettext("active") + ")"); + } else { + item.set("html", item.get("value")); + } + }); + } + ); +} + +function toggle_filter_state(event) { + event.stop(); + new Request.JSON({ + url: this.get("href"), + onSuccess: function(response) { + if (response.status == "ok") { + this.set("html", response.respmsg); + } else { + infobox.error(respmsg.respmsg); + } + }.bind(this) + }).get(); +} + +function move_filter(event) { + event.stop(); + new Request.JSON({ + url: this.get("href"), + onSuccess: function(response) { + $("filters_list").set("html", response.content); + init_filters_list(); + } + }).get(); +} \ No newline at end of file diff --git a/extensions/sievefilters/templates/sievefilters/filter.html b/extensions/sievefilters/templates/sievefilters/filter.html new file mode 100644 index 000000000..bff9438a8 --- /dev/null +++ b/extensions/sievefilters/templates/sievefilters/filter.html @@ -0,0 +1,79 @@ +{% extends "layout_simple.html" %} +{% load i18n sfilters_tags %} +{% block html_head %} +{{ block.super }} + + + + + + + +{% endblock %} +{% block content %} +

{{ title }}

+
+ {% if oldname %}{% endif %} + {{ form.name.label_tag }}: + {{ form.name }}{% display_errors form.name.errors %} +

{% trans "Conditions" %}

+
+
+ {{ form.match_type }}{% display_errors form.match_type.errors %} +
+
+
+ {% for ec in conds_nb %} + {% display_condition form ec %} + {% endfor %} +
+

{% trans "Actions" %}

+
+ {% for ea in actions_nb %} + {% display_action form ea %} + {% endfor %} +
+ +
+{% endblock %} diff --git a/extensions/sievefilters/templates/sievefilters/filters.html b/extensions/sievefilters/templates/sievefilters/filters.html new file mode 100644 index 000000000..85f42bb7a --- /dev/null +++ b/extensions/sievefilters/templates/sievefilters/filters.html @@ -0,0 +1,27 @@ +{% load i18n sfilters_tags %} +

{% trans "Filters" %}

+

+ {% if fs.filters %} + + + + + + + {% for filter in fs.filters %} + + + + + + {% endfor %} +
{% trans "Actions" %}{% trans "Name" %}{% trans "Active" %}
{% filter_actions fs.name filter forloop.counter forloop.last %}{{ filter.name }} + + {{ filter.enabled|yesno }} + +
+ {% else %} +

{% trans "No filter defined yet" %}
+ {% endif %} +

diff --git a/extensions/sievefilters/templates/sievefilters/guieditor.html b/extensions/sievefilters/templates/sievefilters/guieditor.html new file mode 100644 index 000000000..8923141c0 --- /dev/null +++ b/extensions/sievefilters/templates/sievefilters/guieditor.html @@ -0,0 +1,8 @@ +{% load sfilters_tags i18n %} +
+

{% trans "Actions" %}

+ {% fset_menu "gui" fs.name %} +
+
+ {% include "sievefilters/filters.html" %} +
diff --git a/extensions/sievefilters/templates/sievefilters/index.html b/extensions/sievefilters/templates/sievefilters/index.html index 494e7f148..e9e112b87 100644 --- a/extensions/sievefilters/templates/sievefilters/index.html +++ b/extensions/sievefilters/templates/sievefilters/index.html @@ -1,130 +1,41 @@ {% extends "nlayout.html" %} {% load i18n libextras sfilters_tags %} {% block html_head %} + + {% endblock %} {% block middletitle %}{% trans "Message filters" %}{% endblock %} -{% block menu %}{% sfilters_menu user %}{% endblock %} +{% block menu %} +
+ + +
+ +{% endblock %} {% block content %} {% if messages %} {% display_messages messages %} {% endif %} -
- -
+ {% if not active_script and not scripts %} +
{% trans "No filters set defined yet. You must create one in order to define filters." %}
+ {% endif %}
-
{% endblock %} diff --git a/extensions/sievefilters/templates/sievefilters/newfiltersset.html b/extensions/sievefilters/templates/sievefilters/newfiltersset.html index b7f2acef5..f9de4181e 100644 --- a/extensions/sievefilters/templates/sievefilters/newfiltersset.html +++ b/extensions/sievefilters/templates/sievefilters/newfiltersset.html @@ -3,6 +3,28 @@ {% block html_head %} + {% endblock %} {% block content %}

{{ title }}

diff --git a/extensions/sievefilters/templates/sievefilters/rawfilter.html b/extensions/sievefilters/templates/sievefilters/rawfilter.html index 96e1c42a8..1149afac2 100644 --- a/extensions/sievefilters/templates/sievefilters/rawfilter.html +++ b/extensions/sievefilters/templates/sievefilters/rawfilter.html @@ -1,6 +1,10 @@ -
+

{% trans "Actions" %}

+ {% fset_menu "raw" name %} + + -
diff --git a/extensions/sievefilters/templatetags/sfilters_tags.py b/extensions/sievefilters/templatetags/sfilters_tags.py index cdc9bcbeb..1b4413256 100644 --- a/extensions/sievefilters/templatetags/sfilters_tags.py +++ b/extensions/sievefilters/templatetags/sfilters_tags.py @@ -3,6 +3,7 @@ from django.template.loader import render_to_string from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse from modoboa.extensions.sievefilters import views +from modoboa.lib import render_actions register = template.Library() @@ -17,24 +18,130 @@ def sfilters_menu(user): "url" : reverse(views.new_filters_set), "class" : "boxed", "rel" : "{handler:'iframe',size:{x:300,y:170}}" - }, - {"name" : "savescript", - "img" : static_url("pics/save.png"), - "label" : _("Save filters set"), - "url" : ""}, - {"name" : "activatescript", - "img" : static_url("pics/active.png"), - "label" : _("Activate filters set"), - "url" : reverse(views.activate_filters_set)}, - {"name" : "deletescript", - "img" : static_url("pics/remove.png"), - "label" : _("Remove filters set"), - "url" : reverse(views.delete_filters_set),}, - {"name" : "downloadscript", - "img" : static_url("pics/download.png"), - "label" : _("Download"), - "url" : reverse(views.download_filters_set)} + } ] return render_to_string('common/menulist.html', {"entries" : entries, "user" : user}) + +@register.simple_tag +def fset_menu(mode, setname): + entries = [] + if mode == "gui": + entries += [ + {"name" : "newfilter", + "img" : static_url("pics/add.png"), + "label" : _("New filter"), + "url" : reverse(views.newfilter, args=[setname]), + "class" : "boxed", + "rel" : "{handler:'iframe',size:{x:650,y:400}}"}, + + ] + if mode == "raw": + entries += [ + {"name" : "savefs", + "img" : static_url("pics/save.png"), + "label" : _("Save filters set"), + "url" : reverse(views.savefs, args=[setname])}, + ] + + entries += [ + {"name" : "activatefs", + "img" : static_url("pics/active.png"), + "label" : _("Activate filters set"), + "url" : reverse(views.activate_filters_set, args=[setname])}, + {"name" : "removefs", + "img" : static_url("pics/remove.png"), + "label" : _("Remove filters set"), + "url" : reverse(views.remove_filters_set, args=[setname])}, + {"name" : "downloadfs", + "img" : static_url("pics/download.png"), + "label" : _("Download"), + "url" : reverse(views.download_filters_set, args=[setname])} + ] + + return render_to_string('common/menu.html', + {"entries" : entries}) + +@register.simple_tag +def filter_actions(setname, f, position, islast): + actions = [ + {"name" : "editfilter", + "url" : reverse(views.editfilter, args=[setname, f["name"]]), + "img" : static_url("pics/edit.png"), + "title" : _("Edit filter"), + "class" : "boxed", + "rel" : "{handler:'iframe',size:{x:650,y:400}}"}, + {"name" : "removefilter", + "url" : reverse(views.removefilter, args=[setname, f["name"]]), + "img" : static_url("pics/remove.png"), + "title" : _("Remove this filter")} + ] + if position != 1: + actions += [ + {"name" : "movefilter_up", + "url" : reverse(views.move_filter_up, args=[setname, f["name"]]), + "img" : static_url("pics/moveup.png"), + "title" : _("Move this filter up")} + ] + if not islast: + actions += [ + {"name" : "movefilter_down", + "url" : reverse(views.move_filter_down, args=[setname, f["name"]]), + "img" : static_url("pics/movedown.png"), + "title" : _("Move this filter down")}, + ] + return render_actions(actions) + +@register.simple_tag +def display_errors(errors): + if not len(errors): + return "" + t = template.Template(""" + +""") + return t.render(template.Context({ + "errors" : errors + })) + +@register.simple_tag +def display_condition(form, cnt): + target = form["cond_target_%d" % cnt] + operator = form["cond_operator_%d" % cnt] + value = form["cond_value_%d" % cnt] + t = template.Template(""" +
+ {{ tfield }}{{ofield}}{{ vfield }}{{ verrors }} +
+""") + return t.render(template.Context({ + "tfield" : target, + "ofield" : operator, + "vfield" : value, + "verrors" : display_errors(value.errors) + })) + +@register.simple_tag +def display_action(form, cnt): + action = form["action_name_%d" % cnt] + values = [] + acnt = 0 + while True: + try: + values += [form["action_arg_%d_%d" % (cnt, acnt)]] + print display_errors(form["action_arg_%d_%d" % (cnt, acnt)].errors) + acnt += 1 + except KeyError: + break + t = template.Template(""" +
+ {{ afield }}{% for v in values %}{{ v }}{% endfor %} +
+""") + return t.render(template.Context({ + "afield" : action, "values" : values + })) diff --git a/extensions/sievefilters/urls.py b/extensions/sievefilters/urls.py index 273d9d871..d728be9c0 100644 --- a/extensions/sievefilters/urls.py +++ b/extensions/sievefilters/urls.py @@ -2,10 +2,21 @@ from django.conf.urls.defaults import * urlpatterns = patterns('modoboa.extensions.sievefilters.views', (r'^$', 'index'), - (r'^getscript/$', 'getscript'), - (r'^savescript/$', 'savescript'), + (r'^savefs/(?P.+)/$', 'savefs'), (r'^newfs/$', 'new_filters_set'), - (r'^deletefs/$', 'delete_filters_set'), - (r'^activatefs/$', 'activate_filters_set'), - (r'^downloadfs/$', 'download_filters_set'), + (r'^removefs/(?P.+)/$', 'remove_filters_set'), + (r'^activatefs/(?P.+)/$', 'activate_filters_set'), + (r'^downloadfs/(?P.+)/$', 'download_filters_set'), + (r'^templates/(?P\w+)/$', 'get_templates'), + (r'^userfolders/$', 'get_user_folders'), + (r'^(?P.+)/newfilter/$', 'newfilter'), + (r'^(?P.+)/editfilter/(?P.+)/$', 'editfilter'), + (r'^(?P.+)/removefilter/(?P.+)/$', 'removefilter'), + (r'^(?P.+)/togglestate/(?P.+)/$', + 'toggle_filter_state'), + (r'^(?P.+)/moveup/(?P.+)/$', + 'move_filter_up'), + (r'^(?P.+)/movedown/(?P.+)/$', + 'move_filter_down'), + (r'^(?P.+)/$', 'getfs'), ) diff --git a/extensions/sievefilters/views.py b/extensions/sievefilters/views.py index 15ea26a5b..6a1a70f48 100644 --- a/extensions/sievefilters/views.py +++ b/extensions/sievefilters/views.py @@ -4,7 +4,8 @@ from django.http import HttpResponse from django.contrib.auth.decorators import login_required from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse -from modoboa.lib import _render, ajax_response +from modoboa.lib import _render, _render_to_string, \ + ajax_response, ajax_simple_response, parameters from modoboa.auth.lib import get_password from lib import * from forms import * @@ -14,38 +15,121 @@ def index(request, tplname="sievefilters/index.html"): sc = SieveClient(user=request.user.username, password=request.session["password"]) active_script, scripts = sc.listscripts() + if active_script is None: + active_script = "" return _render(request, tplname, { "active_script" : active_script, - "scripts" : sorted(scripts) + "scripts" : sorted(scripts), }) @login_required -def getscript(request): - if not request.GET.has_key("name") or request.GET["name"] == "": - return - sc = SieveClient(user=request.user.username, - password=request.session["password"]) - try: - content = sc.getscript(request.GET["name"]) - except SieveClientError, e: - pass - return ajax_response(request, template="sievefilters/rawfilter.html", - name=request.GET["name"], scriptcontent=content) +def get_templates(request, ftype): + if ftype == "condition": + return ajax_simple_response(FilterForm.cond_templates) + return ajax_simple_response(FilterForm.action_templates) @login_required -def savescript(request): - if not request.POST.has_key("scriptname") or \ - not request.POST.has_key("scriptcontent"): +def get_user_folders(request): + from modoboa.extensions.webmail.lib import IMAPconnector + + mbc = IMAPconnector(user=request.user.username, + password=request.session["password"]) + ret = mbc.getfolders(request.user, unseen_messages=False) + return ajax_simple_response(ret) + +@login_required +def getfs(request, name): + sc = SieveClient(user=request.user.username, + password=request.session["password"]) + editormode = parameters.get_user(request.user, "EDITOR_MODE") + try: + content = sc.getscript(name, format=editormode) + except SieveClientError, e: + return ajax_response(request, "ko", respmsg=str(e)) + + if editormode == "raw": + return ajax_response(request, template="sievefilters/rawfilter.html", + name=name, scriptcontent=content) + return ajax_response(request, template="sievefilters/guieditor.html", + fs=content) + +def build_filter_ctx(ctx, form): + ctx["form"] = form + ctx["conds_nb"] = range(form.conds_cnt) + ctx["actions_nb"] = range(form.actions_cnt) + return ctx + +def submitfilter(request, setname, okmsg, tplname, tplctx, update=False, sc=None): + form = build_filter_form_from_qdict(request) + if form.is_valid(): + if sc is None: + sc = SieveClient(user=request.user.username, + password=request.session["password"]) + fset = sc.getscript(setname, format="fset") + conditions, actions = form.tofilter() + if not update: + fset.addfilter(form.cleaned_data["name"], conditions, actions, + form.cleaned_data["match_type"]) + else: + fset.updatefilter(request.POST["oldname"], + form.cleaned_data["name"], conditions, actions, + form.cleaned_data["match_type"]) + sc.pushscript(fset.name, str(fset)) + return ajax_response(request, respmsg=okmsg, ajaxnav=True) + tplctx = build_filter_ctx(tplctx, form) + return ajax_response(request, status="ko", template=tplname, **tplctx) + +@login_required +def newfilter(request, setname, tplname="sievefilters/filter.html"): + ctx = {"title" : _("New filter"), + "actionurl" : reverse(newfilter, args=[setname])} + if request.method == "POST": + return submitfilter(request, setname, _("Filter created"), tplname, ctx) + + conds = [("Subject", "contains", "")] + actions = [("fileinto", "")] + form = FilterForm(conds, actions, request) + ctx = build_filter_ctx(ctx, form) + return _render(request, tplname, ctx) + +@login_required +def editfilter(request, setname, fname, tplname="sievefilters/filter.html"): + ctx = {"title" : _("Edit filter"), + "actionurl" : reverse(editfilter, args=[setname, fname])} + sc = SieveClient(user=request.user.username, + password=request.session["password"]) + if request.method == "POST": + return submitfilter(request, setname, _("Filter modified"), tplname, ctx, + update=True, sc=sc) + + fset = sc.getscript(setname, format="fset") + f = fset.getfilter(fname) + form = build_filter_form_from_filter(request, fname, f) + ctx = build_filter_ctx(ctx, form) + ctx["oldname"] = fname + return _render(request, tplname, ctx) + +@login_required +def removefilter(request, setname, fname): + sc = SieveClient(user=request.user.username, + password=request.session["password"]) + fset = sc.getscript(setname, format="fset") + if fset.removefilter(fname): + sc.pushscript(fset.name, str(fset)) + return ajax_response(request, respmsg=_("Filter removed")) + return ajax_response(request, "ko", respmsg=_("Failed to remove filter")) + +@login_required +def savefs(request, name): + if not request.POST.has_key("scriptcontent"): return sc = SieveClient(user=request.user.username, password=request.session["password"]) try: - sc.pushscript(request.POST["scriptname"], - request.POST["scriptcontent"]) + sc.pushscript(name, request.POST["scriptcontent"]) except SieveClientError, e: error = str(e) return ajax_response(request, "ko", respmsg=error) - return ajax_response(request, respmsg=_("Filters set saved")) @login_required @@ -67,7 +151,9 @@ def new_filters_set(request, tplname="sievefilters/newfiltersset.html"): except SieveClientError, e: error = str(e) else: - return ajax_response(request, url=reverse(index), + return ajax_response(request, + ajaxnav=True, + url=form.cleaned_data["name"], respmsg=_("Filters set created")) ctx["form"] = form ctx["error"] = error @@ -77,45 +163,75 @@ def new_filters_set(request, tplname="sievefilters/newfiltersset.html"): return _render(request, tplname, ctx) @login_required -def delete_filters_set(request): - if not request.GET.has_key("name") or \ - request.GET["name"] == "": - return +def remove_filters_set(request, name): sc = SieveClient(user=request.user.username, password=request.session["password"]) try: - sc.deletescript(request.GET["name"]) + sc.deletescript(name) except SieveClientError, e: return ajax_response(request, "ko", respmsg=str(e)) return ajax_response(request, respmsg=_("Filters set deleted")) @login_required -def activate_filters_set(request): - if not request.GET.has_key("name") or \ - request.GET["name"] == "": - return +def activate_filters_set(request, name): sc = SieveClient(user=request.user.username, password=request.session["password"]) try: - sc.activatescript(request.GET["name"]) + sc.activatescript(name) except SieveClientError, e: return ajax_response(request, "ko", respmsg=str(e)) return ajax_response(request, respmsg=_("Filters set activated")) @login_required -def download_filters_set(request): - if not request.GET.has_key("name") or \ - request.GET["name"] == "": - return +def download_filters_set(request, name): sc = SieveClient(user=request.user.username, password=request.session["password"]) try: - script = sc.getscript(request.GET["name"]) + script = sc.getscript(name) except SieveClientError, e: return ajax_response(request, "ko", respmsg=str(e)) resp = HttpResponse(script) resp["Content-Type"] = "text/plain" resp["Content-Length"] = len(script) - resp["Content-Disposition"] = "attachment; filename=%s.txt" % request.GET["name"] + resp["Content-Disposition"] = 'attachment; filename="%s.txt"' % name return resp + +@login_required +def toggle_filter_state(request, setname, fname): + sc = SieveClient(user=request.user.username, + password=request.session["password"]) + try: + fset = sc.getscript(setname, format="fset") + if fset.is_filter_disabled(fname): + ret = fset.enablefilter(fname) + newstate = _("yes") + else: + ret = fset.disablefilter(fname) + newstate = _("no") + if not ret: + pass + sc.pushscript(setname, str(fset)) + except SieveClientError, e: + return ajax_response(request, "ko", respmsg=str(e)) + + return ajax_response(request, respmsg=newstate) + +def move_filter(request, setname, fname, direction): + sc = SieveClient(user=request.user.username, + password=request.session["password"]) + try: + fset = sc.getscript(setname, format="fset") + fset.movefilter(fname, direction) + sc.pushscript(setname, str(fset)) + except (SieveClientError), e: + return ajax_response(request, "ko", respmsg=str(e)) + return ajax_response(request, template="sievefilters/filters.html", fs=fset) + +@login_required +def move_filter_up(request, setname, fname): + return move_filter(request, setname, fname, "up") + +@login_required +def move_filter_down(request, setname, fname): + return move_filter(request, setname, fname, "down") diff --git a/extensions/webmail/lib.py b/extensions/webmail/lib.py index 28a5eab52..be671607f 100644 --- a/extensions/webmail/lib.py +++ b/extensions/webmail/lib.py @@ -10,14 +10,15 @@ import socket import imaplib import ssl from datetime import datetime, timedelta -import email.utils +import email import lxml from django.utils.translation import ugettext as _ from django.conf import settings from modoboa.lib import decode, u2u_decode, tables, \ imap_utf7, static_url, parameters from modoboa.lib.connections import ConnectionsManager -from modoboa.lib.email_listing import MBconnector, EmailListing, Email +from modoboa.lib.email_listing import MBconnector, EmailListing +from modoboa.lib.emailutils import * class WebmailError(Exception): errorexpr = re.compile("\[([^\]]+)\]\s*([^\.]+)") @@ -51,16 +52,6 @@ class WMtable(tables.Table): except AttributeError: return value -class EmailAddress(object): - def __init__(self, address): - self.fulladdress = u2u_decode.u2u_decode(address).strip("\r\t\n") - (self.name, self.address) = email.utils.parseaddr(self.fulladdress) - if self.name == "": - self.fulladdress = self.address - - def __str__(self): - return self.fulladdress - class IMAPheader(object): @staticmethod def parse_address(value, **kwargs): @@ -250,7 +241,7 @@ class IMAPconnector(object): from operator import itemgetter return sorted(result, key=itemgetter("name")) - def getfolders(self, user): + def getfolders(self, user, unseen_messages=True): md_folders = [{"name" : "INBOX", "class" : "inbox"}, {"name" : parameters.get_user(user, "DRAFTS_FOLDER"), "class" : "drafts"}, @@ -259,12 +250,13 @@ class IMAPconnector(object): {"name" : parameters.get_user(user, "TRASH_FOLDER"), "class" : "trash"}] md_folders += self._listfolders(md_folders=md_folders) - for fd in md_folders: - key = fd.has_key("path") and "path" or "name" - count = self.unseen_messages(fd[key]) - if count == 0: - continue - fd["unseen"] = count + if unseen_messages: + for fd in md_folders: + key = fd.has_key("path") and "path" or "name" + count = self.unseen_messages(fd[key]) + if count == 0: + continue + fd["unseen"] = count return md_folders def _add_flag(self, folder, mail_id, flag): diff --git a/extensions/webmail/static/js/webmail.js b/extensions/webmail/static/js/webmail.js index 56cc70a75..65b588bc3 100644 --- a/extensions/webmail/static/js/webmail.js +++ b/extensions/webmail/static/js/webmail.js @@ -119,7 +119,7 @@ function compose_callback(resp) { onSuccess: function(resp) { resp = JSON.decode(resp); if (resp.status == "ko") { - get_callback("compose")(resp); + current_anchor.get_callback("compose")(resp); if ($defined(resp.error)) { infobox.error(resp.error); @@ -199,7 +199,7 @@ function refreshFolder() { new Request.JSON({url: query, noCache : true, onSuccess: function(resp) { var callback = ($defined(resp.callback)) ? resp.callback : "default"; - callbacks[callback](resp); + current_anchor.get_callback(callback)(resp); infobox.info(gettext("Done")); infobox.hide(1); }}).get(); @@ -222,7 +222,7 @@ function simple_request(url, params) { onSuccess : function(response) { if (response.status == "ok") { if (!$defined(response.next)) { - get_callback("folder")(response); + current_anchor.get_callback("folder")(response); } infobox.info(gettext("Done")); if ($defined(response.next)) { diff --git a/extensions/webmail/templates/webmail/index.html b/extensions/webmail/templates/webmail/index.html index 22a286fdc..07993d5aa 100644 --- a/extensions/webmail/templates/webmail/index.html +++ b/extensions/webmail/templates/webmail/index.html @@ -58,11 +58,11 @@ function wm_updatelisting(resp) { displayID: "quotatext" }); - register_callback("viewmail", viewmail_cb); - register_callback("compose", compose_callback); - register_callback("reply", compose_callback); - register_callback("replyall", compose_callback); - register_callback("forward", compose_callback); + current_anchor.register_callback("viewmail", viewmail_cb); + current_anchor.register_callback("compose", compose_callback); + current_anchor.register_callback("reply", compose_callback); + current_anchor.register_callback("replyall", compose_callback); + current_anchor.register_callback("forward", compose_callback); window.addEvent("pageRefresh", function() { if (rtimer) { @@ -71,7 +71,7 @@ function wm_updatelisting(resp) { } }); - register_callback("folder", function(resp) { + current_anchor.register_callback("folder", function(resp) { wm_updatelisting(resp); rtimer = refreshFolder.periodical({{ refreshrate }}); searchbox_init(); diff --git a/lib/__init__.py b/lib/__init__.py index 3b1b42e81..690b7e982 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -6,7 +6,7 @@ import hashlib, crypt, string from random import Random from django.http import HttpResponse from django.conf import settings -from django.template import RequestContext +from django import template from django.shortcuts import render_to_response from django.template.loader import render_to_string from django.utils.translation import ugettext as _ @@ -27,7 +27,7 @@ def _render(request, tpl, user_context): (useful to use settings variables like MEDIA_URL inside templates) """ return render_to_response(tpl, user_context, - context_instance=RequestContext(request)) + context_instance=template.RequestContext(request)) def _render_to_string(request, tpl, user_context): """Custom rendering function @@ -35,11 +35,24 @@ def _render_to_string(request, tpl, user_context): Same as _render. """ return render_to_string(tpl, user_context, - context_instance=RequestContext(request)) + context_instance=template.RequestContext(request)) def _render_error(request, errortpl="error", user_context={}): return render_to_response("common/%s.html" % errortpl, user_context, - context_instance=RequestContext(request)) + context_instance=template.RequestContext(request)) + +def render_actions(actions): + t = template.Template(""" +{% for a in actions %} + + + +{% endfor %} +""") + return t.render(template.Context({ + "actions" : actions + })) def exec_as_vuser(cmd): code, output = exec_cmd("sudo -u %s %s" \ diff --git a/lib/email_listing.py b/lib/email_listing.py index dc223b775..05fab0610 100644 --- a/lib/email_listing.py +++ b/lib/email_listing.py @@ -140,166 +140,6 @@ class EmailListing: context[k] = v return _render(request, self.tpl, context) -class Email(object): - def __init__(self, msg, mformat="plain", dformat="plain", links="0"): - self.attached_map = {} - self.contents = {"html" : "", "plain" : ""} - self.headers = [] - self.attachments = {} - self.mformat = mformat - self.dformat = dformat - self.links = links - - self.__parse(msg) - - if not self.contents.has_key(mformat) or self.contents[mformat] == "": - # Fallback - self.mformat = mformat == "html" and "plain" or "html" - - self.body = \ - getattr(self, "viewmail_%s" % self.mformat) \ - (self.contents[self.mformat], links=links) - - def __parse_default(self, msg, level): - """Default parser - - All parts handled by this parser will be consireded as - attachments. - """ - fname = msg.get_filename() - if fname is not None: - decoded = decode_header(fname) - value = decoded[0][1] is None and decoded[0][0] \ - or unicode(decoded[0][0], decoded[0][1]) - else: - value = "part_%s" % level - self.attachments[level] = value - - def _parse_text(self, msg, level): - """Displayable content parser - - text, html, calendar, etc. All those contents can be displayed - inside a navigator. - """ - if msg.get_content_subtype() not in ["plain", "html"]: - self.__parse_default(msg, level) - target = "plain" - else: - target = msg.get_content_subtype() - self.contents[target] += decode(msg.get_payload(decode=True), - charset=msg.get_content_charset()) - - def _parse_image(self, msg, level): - """image/* parser - - The only reason to make a specific parser for images is that, - sometimes, messages embark inline images, which means they - must be displayed and not attached. - """ - if self.dformat == "html" and self.links != "0" \ - and msg.has_key("Content-Disposition"): - if msg["Content-Disposition"].startswith("inline"): - cid = None - if msg.has_key("Content-ID"): - m = re.match("<(.+)>", msg["Content-ID"]) - cid = m is not None and m.group(1) or msg["Content-ID"] - fname = msg.get_filename() - if fname is None: - if msg.has_key("Content-Location"): - fname = msg["Content-Location"] - elif cid is not None: - fname = cid - else: - # I give up for now :p - return - self.attached_map[cid] = re.match("^http:", fname) and fname \ - or self.__save_image(fname, msg) - return - self.__parse_default(msg, level) - - def __parse(self, msg, level=None): - """Recursive email parser - - A message structure can be complex. To correctly handle - unknown MIME types, a simple rule is applied : if I don't know - how to display a specific part, it becomes an attachment! If - no name is specified for an attachment, the part number - described in the RFC 3501 (which retrieves BODY sections) is - used to build a file name (like part_1.1). - - :param msg: message (or part) to parse - :param level: current part number - """ - if msg.is_multipart() and msg.get_content_maintype() != "message": - cpt = 1 - for part in msg.get_payload(): - nlevel = level is None and ("%d" % cpt) \ - or "%s.%d" % (level, cpt) - self.__parse(part, nlevel) - cpt += 1 - return - - if level is None: - level = "1" - try: - getattr(self, "_parse_%s" % msg.get_content_maintype())(msg, level) - except AttributeError: - self.__parse_default(msg, level) - - def __save_image(self, fname, part): - """Save an inline image on the filesystem. - - Some HTML messages are using inline images (attached images - with links on them inside the body). In order to display them, - images are saved on the filesystem and links contained in the - message are modified. - - :param fname: the image associated filename - :param part: the email part that contains the image payload - """ - if re.search("\.\.", fname): - return None - path = "/static/tmp/" + fname - fp = open(settings.MODOBOA_DIR + path, "wb") - fp.write(part.get_payload(decode=True)) - fp.close() - return path - - def map_cid(self, url): - import re - - m = re.match(".*cid:(.+)", url) - if m: - if self.attached_map.has_key(m.group(1)): - return self.attached_map[m.group(1)] - return url - - def render_headers(self, **kwargs): - return render_to_string("common/mailheaders.html", { - "headers" : self.headers, - }) - - def viewmail_plain(self, content, **kwargs): - return "
%s
" % content - - def viewmail_html(self, content, **kwargs): - if content is None or content == "": - return (False, "") - links = kwargs.has_key("links") and kwargs["links"] or "0" - html = lxml.html.fromstring(content) - if links == "0": - html.rewrite_links(lambda x: None) - else: - html.rewrite_links(self.map_cid) - body = html.find("body") - if body is None: - body = lxml.html.tostring(html) - else: - body = lxml.html.tostring(body) - body = re.sub("<(/?)body", lambda m: "<%sdiv" % m.group(1), body) - body = Template(body).render(Context({})) - return body - def parse_search_parameters(request): if request.GET.has_key("pattern"): request.session["pattern"] = re.escape(request.GET["pattern"]) diff --git a/lib/emailutils.py b/lib/emailutils.py new file mode 100644 index 000000000..33552c025 --- /dev/null +++ b/lib/emailutils.py @@ -0,0 +1,178 @@ +# coding: utf-8 + +import email +import lxml +from django.template import Template, Context +from django.template.loader import render_to_string +import u2u_decode +from lib import decode + +class EmailAddress(object): + def __init__(self, address): + self.fulladdress = u2u_decode.u2u_decode(address).strip("\r\t\n") + (self.name, self.address) = email.utils.parseaddr(self.fulladdress) + if self.name == "": + self.fulladdress = self.address + + def __str__(self): + return self.fulladdress + +class Email(object): + def __init__(self, msg, mformat="plain", dformat="plain", links="0"): + self.attached_map = {} + self.contents = {"html" : "", "plain" : ""} + self.headers = [] + self.attachments = {} + self.mformat = mformat + self.dformat = dformat + self.links = links + + self.__parse(msg) + + if not self.contents.has_key(mformat) or self.contents[mformat] == "": + # Fallback + self.mformat = mformat == "html" and "plain" or "html" + + self.body = \ + getattr(self, "viewmail_%s" % self.mformat) \ + (self.contents[self.mformat], links=links) + + def __parse_default(self, msg, level): + """Default parser + + All parts handled by this parser will be consireded as + attachments. + """ + fname = msg.get_filename() + if fname is not None: + decoded = decode_header(fname) + value = decoded[0][1] is None and decoded[0][0] \ + or unicode(decoded[0][0], decoded[0][1]) + else: + value = "part_%s" % level + self.attachments[level] = value + + def _parse_text(self, msg, level): + """Displayable content parser + + text, html, calendar, etc. All those contents can be displayed + inside a navigator. + """ + if msg.get_content_subtype() not in ["plain", "html"]: + self.__parse_default(msg, level) + target = "plain" + else: + target = msg.get_content_subtype() + self.contents[target] += decode(msg.get_payload(decode=True), + charset=msg.get_content_charset()) + + def _parse_image(self, msg, level): + """image/* parser + + The only reason to make a specific parser for images is that, + sometimes, messages embark inline images, which means they + must be displayed and not attached. + """ + if self.dformat == "html" and self.links != "0" \ + and msg.has_key("Content-Disposition"): + if msg["Content-Disposition"].startswith("inline"): + cid = None + if msg.has_key("Content-ID"): + m = re.match("<(.+)>", msg["Content-ID"]) + cid = m is not None and m.group(1) or msg["Content-ID"] + fname = msg.get_filename() + if fname is None: + if msg.has_key("Content-Location"): + fname = msg["Content-Location"] + elif cid is not None: + fname = cid + else: + # I give up for now :p + return + self.attached_map[cid] = re.match("^http:", fname) and fname \ + or self.__save_image(fname, msg) + return + self.__parse_default(msg, level) + + def __parse(self, msg, level=None): + """Recursive email parser + + A message structure can be complex. To correctly handle + unknown MIME types, a simple rule is applied : if I don't know + how to display a specific part, it becomes an attachment! If + no name is specified for an attachment, the part number + described in the RFC 3501 (which retrieves BODY sections) is + used to build a file name (like part_1.1). + + :param msg: message (or part) to parse + :param level: current part number + """ + if msg.is_multipart() and msg.get_content_maintype() != "message": + cpt = 1 + for part in msg.get_payload(): + nlevel = level is None and ("%d" % cpt) \ + or "%s.%d" % (level, cpt) + self.__parse(part, nlevel) + cpt += 1 + return + + if level is None: + level = "1" + try: + getattr(self, "_parse_%s" % msg.get_content_maintype())(msg, level) + except AttributeError: + self.__parse_default(msg, level) + + def __save_image(self, fname, part): + """Save an inline image on the filesystem. + + Some HTML messages are using inline images (attached images + with links on them inside the body). In order to display them, + images are saved on the filesystem and links contained in the + message are modified. + + :param fname: the image associated filename + :param part: the email part that contains the image payload + """ + if re.search("\.\.", fname): + return None + path = "/static/tmp/" + fname + fp = open(settings.MODOBOA_DIR + path, "wb") + fp.write(part.get_payload(decode=True)) + fp.close() + return path + + def map_cid(self, url): + import re + + m = re.match(".*cid:(.+)", url) + if m: + if self.attached_map.has_key(m.group(1)): + return self.attached_map[m.group(1)] + return url + + def render_headers(self, **kwargs): + return render_to_string("common/mailheaders.html", { + "headers" : self.headers, + }) + + def viewmail_plain(self, content, **kwargs): + return "
%s
" % content + + def viewmail_html(self, content, **kwargs): + if content is None or content == "": + return (False, "") + links = kwargs.has_key("links") and kwargs["links"] or "0" + html = lxml.html.fromstring(content) + if links == "0": + html.rewrite_links(lambda x: None) + else: + html.rewrite_links(self.map_cid) + body = html.find("body") + if body is None: + body = lxml.html.tostring(html) + else: + body = lxml.html.tostring(body) + body = re.sub("<(/?)body", lambda m: "<%sdiv" % m.group(1), body) + body = Template(body).render(Context({})) + return body diff --git a/static/css/box.css b/static/css/box.css index 11eb64575..2cd66ece8 100644 --- a/static/css/box.css +++ b/static/css/box.css @@ -73,7 +73,7 @@ td.even { .title h1 { float: left; - margin: 5px 10px; + margin: 5px; font-size: medium; font-weight: bold; } diff --git a/static/js/anchor_nav.js b/static/js/anchor_nav.js new file mode 100644 index 000000000..c98624711 --- /dev/null +++ b/static/js/anchor_nav.js @@ -0,0 +1,209 @@ +/* + * Class that implements a simple anchor based navigation system for + * AJAX applications. + */ +var AnchorNavigation = new Class({ + Implements: [Options], + + options: { + checkinterval: 300, + defloadingtext: gettext("Loading..."), + defloadingcolor: "gray", + defcallback: null + }, + + initialize: function(deflocation, options) { + this.setOptions(options); + + this.deflocation = deflocation; + this.callbacks = {}; + this.params = $H(); + this.base = ''; + this.serialized = null; + this.reset_loading_infos(); + if (this.options.defcallback) { + this.register_callback("default", this.options.defcallback); + this.ca_id = + this.check_anchor.bind(this).periodical(this.options.checkinterval); + } + }, + + reset_loading_infos: function() { + this.loading_message = this.options.defloadingtext; + this.loading_color = this.options.defloadingcolor; + }, + + reset: function() { + this.base = null; + this.params.empty(); + return this; + }, + + parse_string: function(value, reset) { + var splits = (value.charAt(0) == '#') + ? value.substring(1).split('?') : value.split('?'); + + if (splits.length == 0) { + return this; + } + if (!$defined(reset)) { + reset = false; + } + if (reset) { + this.reset(); + } + this.base = (splits[0] == "") ? this.deflocation : splits[0]; + + var re = new RegExp("/$"); + if (!this.base.match(re)) { + this.base += '/'; + } + if (splits.length > 1) { + var params = splits[1].split('&'); + for (var i = 0; i < params.length; i++) { + var tmp = params[i].split('='); + this.setparam(tmp[0], tmp[1]); + } + } + return this; + }, + + from_string: function(value, reset) { + this.parse_string(value); + this.serialized = value; + return this; + }, + + serialize: function() { + var res = this.base; + + if (this.params.getLength() != 0) { + res += "?" + this.params.toQueryString(); + } + return res; + }, + + update: function(force) { + window.fireEvent("pageRefresh"); + location.hash = this.serialize(); + if ($defined(force)) { + this.force = force; + } + }, + + deleteParam: function(str) { + this.params.erase(str); + }, + + updateparams: function(str) { + if (str.charAt(0) == '?') { + str = str.substring(1); + } + var elems = str.split('&'); + for (var i = 0; i < elems.length; i++) { + this.setparamfromstring(elems[i]); + } + }, + + setparamfromstring: function(str) { + var def = str.split('='); + this.params.set(def[0], def[1]); + }, + + setparam: function(name, value) { + this.params.set(name, value); + return this; + }, + + setparams: function(params) { + this.params.extend(params); + return this; + }, + + getparam: function(name) { + if (!this.params.has(name)) { + return ""; + } + return this.params.get(name); + }, + + baseurl: function(value, noreset) { + if (!$defined(noreset) || noreset == 0) { + this.reset(); + } + this.base = value; + var re = new RegExp("/$"); + if (!this.base.match(re)) { + this.base += '/'; + } + return this; + }, + + addbaseurl: function(value) { + var newbase = this.base + value; + return this.baseurl(newbase); + }, + + getbaseurl: function() { + return this.base.substr(0, this.base.length - 1); + }, + + delparam: function(name) { + if (!this.params.has(name)) { + return false; + } + this.params.erase(name); + return this; + }, + + register_callback: function(name, callback) { + this.callbacks[name] = callback; + }, + + get_callback: function(name) { + return this.callbacks[name]; + }, + + set_default_callback: function(callback) { + this.register_callback("default", callback); + if (!$defined(this.ca_id)) { + this.ca_id = + this.check_anchor.bind(this).periodical(this.options.checkinterval); + } + }, + + check_anchor: function() { + if (this.serialized == location.hash + && !$defined(this.force)) { + return; + } + delete(this.force); + this.from_string(location.hash); + if (!this.serialized) { + location.hash = this.deflocation; + return; + } + + var query = this.serialized.substring(1); + + infobox.show(this.loading_message, { + profile: this.loading_color, + spinner: true + }); + new Request.JSON({ + url: query, + noCache : true, + onSuccess: function(resp) { + var callback = ($defined(resp.callback)) ? resp.callback : "default"; + this.callbacks[callback](resp); + infobox.info(gettext("Done")); + infobox.hide(1); + this.reset_loading_infos(); + }.bind(this), + onFailure: function(xhr) { + $(document.body).set("html", xhr.responseText); + $(document.body).setStyle("overflow", "auto"); + } + }).get(); + } +}); diff --git a/static/js/common.js b/static/js/common.js index b697a0b5d..b5c8214b3 100644 --- a/static/js/common.js +++ b/static/js/common.js @@ -9,7 +9,6 @@ window.addEvent('domready', function(){ }); var media_url = ""; -callbacks = {}; /* * A simple function to initialize the value of the global variable @@ -27,202 +26,6 @@ function static_url(value) { return media_url + value; } -function register_callback(name, callback) { - callbacks[name] = callback; -}; - -get_callback = function(name) { - return callbacks[name]; -}; - -/* - * AJAX anchor based navigation - */ -var current_anchor = null; - -function check_anchor() { - if (current_anchor.serialized == location.hash - && !$defined(current_anchor.force)) { - return; - } - delete(current_anchor.force); - current_anchor.from_string(location.hash); - if (!current_anchor.serialized) { - location.hash = current_anchor.deflocation; - return; - } - - var query = current_anchor.serialized.substring(1); - - infobox.show(current_anchor.loading_message, { - profile: current_anchor.loading_color, - spinner: true - }); - new Request.JSON({ - url: query, - noCache : true, - onSuccess: function(resp) { - var callback = ($defined(resp.callback)) ? resp.callback : "default"; - callbacks[callback](resp); - infobox.info(gettext("Done")); - infobox.hide(1); - current_anchor.reset_loading_infos(); - }, - onFailure: function(xhr) { - $(document.body).set("html", xhr.responseText); - $(document.body).setStyle("overflow", "auto"); - } - }).get(); -}; - -/* - * Initialize the AJAX anchor based navigation - */ -function ajaxListener(deflocation, defcallback) { - current_anchor = new HashWrapper(deflocation); - register_callback("default", defcallback); - check_anchor.periodical(300); -}; - -/* - * - */ -function HashWrapper(deflocation) { - this.deflocation = deflocation; - this.params = $H(); - this.base = ''; - this.serialized = null; - - this.reset_loading_infos = function() { - this.loading_message = gettext("Loading..."); - this.loading_color = "gray"; - }; - this.reset_loading_infos(); - - this.reset = function() { - this.base = null; - this.params.empty(); - return this; - }; - - this.parse_string = function(value, reset) { - var splits = (value.charAt(0) == '#') - ? value.substring(1).split('?') : value.split('?'); - - if (splits.length == 0) { - return this; - } - if (!$defined(reset)) { - reset = false; - } - if (reset) { - this.reset(); - } - this.base = (splits[0] == "") ? this.deflocation : splits[0]; - - var re = new RegExp("/$"); - if (!this.base.match(re)) { - this.base += '/'; - } - if (splits.length > 1) { - var params = splits[1].split('&'); - for (var i = 0; i < params.length; i++) { - var tmp = params[i].split('='); - this.setparam(tmp[0], tmp[1]); - } - } - return this; - }; - - this.from_string = function(value, reset) { - this.parse_string(value); - this.serialized = value; - return this; - }; - - this.serialize = function() { - var res = this.base; - - if (this.params.getLength() != 0) { - res += "?" + this.params.toQueryString(); - } - return res; - }; - - this.update = function(force) { - window.fireEvent("pageRefresh"); - location.hash = this.serialize(); - if ($defined(force)) { - this.force = force; - } - }; - - this.deleteParam = function(str) { - this.params.erase(str); - }; - - this.updateparams = function(str) { - if (str.charAt(0) == '?') { - str = str.substring(1); - } - var elems = str.split('&'); - for (var i = 0; i < elems.length; i++) { - this.setparamfromstring(elems[i]); - } - }; - - this.setparamfromstring = function(str) { - var def = str.split('='); - this.params.set(def[0], def[1]); - }; - - this.setparam = function(name, value) { - this.params.set(name, value); - return this; - }; - - this.setparams = function(params) { - this.params.extend(params); - return this; - }; - - this.getparam = function(name) { - if (!this.params.has(name)) { - return ""; - } - return this.params.get(name); - }; - - this.baseurl = function(value, noreset) { - if (!$defined(noreset) || noreset == 0) { - this.reset(); - } - this.base = value; - var re = new RegExp("/$"); - if (!this.base.match(re)) { - this.base += '/'; - } - return this; - }; - - this.addbaseurl = function(value) { - var newbase = this.base + value; - return this.baseurl(newbase); - }; - - this.getbaseurl = function() { - return this.base.substr(0, this.base.length - 1); - }; - - this.delparam = function(name) { - if (!this.params.has(name)) { - return false; - } - this.params.erase(name); - return this; - }; -}; - function parse_menubar(id) { if ($(id)) { $(id).getElements('li.dropdown').each(function(elem){ diff --git a/static/js/domains.js b/static/js/domains.js index e4758ac29..991d4be06 100644 --- a/static/js/domains.js +++ b/static/js/domains.js @@ -1,11 +1,6 @@ window.addEvent('domready', function() { var handlers = new Array(); - if ($defined($("domcontent"))) { - SqueezeBox.assign($("domcontent").getElements('a[class=boxed]'), { - parse: 'rel' - }); - } if ($defined($("subcontent"))) { SqueezeBox.assign($("subcontent").getElements('a[class=boxed]'), { parse: 'rel' diff --git a/static/js/dynamic_textinput.js b/static/js/dynamic_textinput.js index d8891d067..5c1682529 100644 --- a/static/js/dynamic_textinput.js +++ b/static/js/dynamic_textinput.js @@ -10,6 +10,7 @@ var DynamicTextInput = new Class({ if (!$defined(targets) || !targets.length) { return; } + this.fields_cnt = 0; for (var cpt = 0; cpt < targets.length; cpt++) { var target = targets[cpt]; var btn; @@ -18,31 +19,28 @@ var DynamicTextInput = new Class({ btn = new Element("img", { src: parent.static_url("pics/add.png") }); - btn.addEvent("click", this.newinput.bind(this)); + btn.addEvent("click", this.newelement.bind(this)); } else { var btnid = "btn_" + cpt; btn = new Element("img", { src: parent.static_url("pics/remove.png"), id: btnid }); - btn.addEvent("click", this.removeinput.bind(this)); + btn.addEvent("click", this.removeelement.bind(this)); } btn.setStyles({ "float": "right", "margin-top": "2px" }); btn.inject(target, "after"); + this.fields_cnt++; } this.nextid = targets.length; this.target = targets[0]; this.setOptions(options); }, - newinput: function(event) { - if (this.target.get("value") == "") { - return; - } - var coords = this.target.getCoordinates(); + createfield: function() { var div = new Element("div").addClass("row"); var label = new Element("label").inject(div); var ninput = new Element("input", { @@ -51,6 +49,15 @@ var DynamicTextInput = new Class({ name: this.target.get("name"), value: this.target.get("value") }).inject(div); + + return [div, ninput]; + }, + + newelement: function(event) { + if (this.target.get("value") == "") { + return; + } + var field = this.createfield(); var delbtn = new Element("img", { id: "delbtn_" + this.nextid, src: parent.static_url("pics/remove.png") @@ -61,17 +68,19 @@ var DynamicTextInput = new Class({ "float" : "right", "margin-top": "2px" }); - div.inject(this.target.getParent(), "after"); - delbtn.inject(ninput, "after"); - delbtn.addEvent("click", this.removeinput.bind(this)); + field[0].inject(this.target.getParent(), "after"); + delbtn.inject(field[1], "after"); + delbtn.addEvent("click", this.removeelement.bind(this)); this.target.set("value", ""); + this.fields_cnt++; }, - removeinput: function(event) { + removeelement: function(event) { var id = (event.target.get("id").split("_"))[1]; var input = $("id_" + this.target.get("name") + "_" + id); input.destroy(); event.target.destroy(); + this.fields_cnt--; } }); \ No newline at end of file diff --git a/static/pics/movedown.png b/static/pics/movedown.png new file mode 100644 index 0000000000000000000000000000000000000000..3dd7fccdf06321880f69d65631a363e4b813ba04 GIT binary patch literal 683 zcmV;c0#yBpP)i}4~9FCHr5K@{qtRq-ka(iJ?ZP=uTm zA)-=KTXM11RS2~$6nm&uv{cQSuKf$?CbP4jht?XK7~kVJ-#qia^UW|KLbc-EY953z z>WRV7dqi_}NvUZfgl}C)!*&F0M_|b+V97E80CykVr~%gk05Haon|Y41T|%KagO77# zXg_$ht|?xxSRKlv`0H+L2mld?91sx{?rsQB5|>q-9K_b`yI?say^?H5vawt?QN0%L zQr8VKjyDQ9NJT;|(TgXq`xKW7BF8I9f|vxjL{S!?dO5hlaQ@UaF9;B#f^;@jnqQnt zF(N{uTTQn`k0*~rlb-kaSCCvlqKp-L5!3TI5NItHN89$(7@Zg?Pfm^Zz3vh1d@XXv z%dw2{#h9WC`nbQFn~GCVFR$PcMeY21IKx0j@A8ZjM9Y6Ue=LTlryr z>(@2W+wdKbgN~t*f$yfVK)ah_*yWGG{JKoJQ9bX-)!YpMx+aPwk<4VDSzECWL8lc@ z`=3~j{FA#{Y~yeIt$3GuF4Da7HUP}#KVRBt{l5SJIDAFD4*5 zlTT<9K@`TnH@h?0jfHw>O#jh>1nt2m6SAzrkW<%*%=Q}tRboP9pC%r$9&(LA+0sq)ybQD9srhR zz3Fxu)^6aqJl)?FN%nOeOgb)4?+M_zJQ@)DvRBSb2FFH|!GH*69hXP{3*flC1BAti zbJNzAm&ca3iTKDR3xq|-Y2`eKx?tij|4I5$vH1yqf%5H^Fb8J54jK)5$VM-C5%i8b<|k&Kxh=#FG>Ox&>r z4?sb}hlkg>1-vahgJcyDBP0eg&{{(&pkA-x@zeQAv9vj35<}`!ZpEItce&w-qk8xH z6RRw9@QrP7!N5#{!3lE@ZdYYZTfgiFi6Lb!&3aDLCbZTHrTP~zgJ5t59%w*hO + {% block css %}{% endblock %} @@ -27,6 +28,8 @@ {% endblock %}