mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-11-13 09:04:04 +01:00
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
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
</script>
|
||||
{% 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 %}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <draft-martin-managesieve-12>.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{% extends "layout_simple.html" %}
|
||||
{% load i18n sfilters_tags %}
|
||||
{% block html_head %}
|
||||
{{ block.super }}
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}js/form.js"></script>
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}js/dynamic_textinput.js"></script>
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}sievefilters/js/dynamic_fields.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}sievefilters/css/style.css" />
|
||||
|
||||
<script type="text/javascript">
|
||||
var cond_templates = null;
|
||||
var action_templates = null;
|
||||
var da = null;
|
||||
var dc = null;
|
||||
|
||||
function initform() {
|
||||
var clist = $$("select[name*=cond_target_]");
|
||||
var alist = $$("select[name*=action_name_]");
|
||||
|
||||
dc = new DynamicCondition(clist, {
|
||||
templates: cond_templates
|
||||
});
|
||||
da = new DynamicAction(alist, {
|
||||
templates: action_templates,
|
||||
ufolders_url: "{% url extensions.sievefilters.views.get_user_folders %}"
|
||||
});
|
||||
|
||||
clist.addEvent("change", dc.onTargetChange.bind(dc));
|
||||
alist.addEvent("change", da.onNameChange.bind(da));
|
||||
}
|
||||
|
||||
$(document).addEvent("domready", function() {
|
||||
new Request.JSON({
|
||||
url: "{% url extensions.sievefilters.views.get_templates 'condition' %}",
|
||||
onSuccess: function(response) {
|
||||
cond_templates = response;
|
||||
}
|
||||
}).get();
|
||||
new Request.JSON({
|
||||
url: "{% url extensions.sievefilters.views.get_templates 'action' %}",
|
||||
onSuccess: function(response) {
|
||||
action_templates = response;
|
||||
initform();
|
||||
}
|
||||
}).get();
|
||||
register_error_callback("filterform", initform);
|
||||
register_extra_data_callback("filterform", function() {
|
||||
return "conds=" + dc.fields_cnt + "&actions=" + da.fields_cnt;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="title"><h1>{{ title }}</h1></div>
|
||||
<form action="{{ actionurl }}" method="POST" id="filterform" name="filterform">
|
||||
{% if oldname %}<input type="hidden" name="oldname" value="{{ oldname }}" />{% endif %}
|
||||
{{ form.name.label_tag }}:
|
||||
{{ form.name }}{% display_errors form.name.errors %}
|
||||
<h2>{% trans "Conditions" %}</h2>
|
||||
<div id="condlist">
|
||||
<div id="matchtype">
|
||||
{{ form.match_type }}{% display_errors form.match_type.errors %}
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
{% for ec in conds_nb %}
|
||||
{% display_condition form ec %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<h2>{% trans "Actions" %}</h2>
|
||||
<div id="actionlist">
|
||||
{% for ea in actions_nb %}
|
||||
{% display_action form ea %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<input type="submit" id="submit" name="submit" value='{% trans "Save" %}' />
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% load i18n sfilters_tags %}
|
||||
<h1>{% trans "Filters" %}</h1>
|
||||
<p>
|
||||
{% if fs.filters %}
|
||||
<table>
|
||||
<tr>
|
||||
<th width="10%">{% trans "Actions" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th width="5%">{% trans "Active" %}</th>
|
||||
</tr>
|
||||
{% for filter in fs.filters %}
|
||||
<tr class="{% cycle 'odd' 'even' %}">
|
||||
<td>{% filter_actions fs.name filter forloop.counter forloop.last %}</td>
|
||||
<td>{{ filter.name }}</td>
|
||||
<td>
|
||||
<a name="togglestate"
|
||||
href="{% url extensions.sievefilters.views.toggle_filter_state fs.name filter.name %}">
|
||||
{{ filter.enabled|yesno }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="info">{% trans "No filter defined yet" %}</div>
|
||||
{% endif %}
|
||||
</p>
|
||||
@@ -0,0 +1,8 @@
|
||||
{% load sfilters_tags i18n %}
|
||||
<div id="fset_menu" class="leftbox">
|
||||
<h1>{% trans "Actions" %}</h1>
|
||||
{% fset_menu "gui" fs.name %}
|
||||
</div>
|
||||
<div id="filters_list">
|
||||
{% include "sievefilters/filters.html" %}
|
||||
</div>
|
||||
@@ -1,130 +1,41 @@
|
||||
{% extends "nlayout.html" %}
|
||||
{% load i18n libextras sfilters_tags %}
|
||||
{% block html_head %}
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}js/anchor_nav.js"></script>
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}sievefilters/js/lib.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}sievefilters/css/style.css" />
|
||||
<script type="text/javascript">
|
||||
var curscript = null;
|
||||
|
||||
function loadscript(evt) {
|
||||
var params = "name=" + this.get("href");
|
||||
|
||||
curscript = this.get("href");
|
||||
evt.stop();
|
||||
$$("#sets_list li").removeClass("selected");
|
||||
this.getParent().addClass("selected");
|
||||
new Request.JSON({
|
||||
url: "{% url extensions.sievefilters.views.getscript %}",
|
||||
onSuccess: function(response) {
|
||||
if (response.status == "ok") {
|
||||
$("set_content").set("html", response.content);
|
||||
}
|
||||
}
|
||||
}).get(params);
|
||||
}
|
||||
|
||||
function check_prereqs(evt, msg) {
|
||||
evt.stop();
|
||||
if (!curscript) {
|
||||
return false;
|
||||
}
|
||||
if ($defined(msg) && !confirm(msg)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function send_command(cmd) {
|
||||
new Request.JSON({
|
||||
url: cmd,
|
||||
data: "name=" + curscript,
|
||||
onSuccess: function(response) {
|
||||
if (response.status == "ko") {
|
||||
infobox.calcsize(300);
|
||||
infobox.error(response.respmsg);
|
||||
} else {
|
||||
if (!response.norefresh) {
|
||||
location.reload();
|
||||
}
|
||||
infobox.calcsize();
|
||||
infobox.info(response.respmsg);
|
||||
infobox.hide(2);
|
||||
}
|
||||
}
|
||||
}).get();
|
||||
}
|
||||
|
||||
function savescript(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();
|
||||
}
|
||||
|
||||
function deletescript(evt) {
|
||||
if (!check_prereqs(evt, gettext("Delete filters set?"))) {
|
||||
return;
|
||||
}
|
||||
send_command("{% url extensions.sievefilters.views.delete_filters_set %}");
|
||||
}
|
||||
|
||||
function activatescript(evt) {
|
||||
if (!check_prereqs(evt, gettext("Activate this filters set?"))) {
|
||||
return;
|
||||
}
|
||||
send_command("{% url extensions.sievefilters.views.activate_filters_set %}");
|
||||
}
|
||||
|
||||
function downloadscript(evt) {
|
||||
if (!curscript) {
|
||||
evt.stop();
|
||||
return;
|
||||
}
|
||||
this.set("href", this.get("href") + "?name=" + curscript);
|
||||
}
|
||||
var current_anchor = new AnchorNavigation("{{ active_script }}", {
|
||||
defcallback: loadfs
|
||||
});
|
||||
|
||||
$(document).addEvent("domready", function() {
|
||||
$$("a[name=loadscript]").addEvent("click", loadscript);
|
||||
$$("a[name=savescript]").addEvent("click", savescript);
|
||||
$$("a[name=deletescript]").addEvent("click", deletescript);
|
||||
$$("a[name=activatescript]").addEvent("click", activatescript);
|
||||
$$("a[name=downloadscript]").addEvent("click", downloadscript);
|
||||
$("curfset").addEvent("change", changefs);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block middletitle %}{% trans "Message filters" %}{% endblock %}
|
||||
{% block menu %}{% sfilters_menu user %}{% endblock %}
|
||||
{% block menu %}
|
||||
<div id="fsets_list">
|
||||
<label>{% trans "Filters sets" %}</label>
|
||||
<select id="curfset">
|
||||
{% if active_script %}
|
||||
<option value="{{ active_script }}">{{ active_script }} (active)</option>
|
||||
{% endif %}
|
||||
{% for script in scripts %}
|
||||
<option value="{{ script }}">{{ script }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<ul>{% sfilters_menu user %}</ul>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% if messages %}
|
||||
{% display_messages messages %}
|
||||
{% endif %}
|
||||
<div id="sets_list">
|
||||
<ul>
|
||||
{% if active_script %}
|
||||
<li>
|
||||
<a href="{{ active_script }}" name="loadscript">{{ active_script }}</a>
|
||||
<img src="{{ MEDIA_URL }}pics/active.png" />
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for script in scripts %}
|
||||
<li><a href="{{ script }}" name="loadscript">{{ script }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div id="set_content">
|
||||
{% if not active_script and not scripts %}
|
||||
<div class="info">{% trans "No filters set defined yet. You must create one in order to define filters." %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
{% block html_head %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}sievefilters/css/style.css" />
|
||||
<script src="{{ MEDIA_URL }}js/form.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).addEvent("domready", function() {
|
||||
register_success_callback("newfiltersset", function(response) {
|
||||
var name = response.url;
|
||||
var option = new Element("option", {value: name, html: name});
|
||||
var curfset = $(parent.document).getElementById("curfset");
|
||||
|
||||
option.inject(curfset);
|
||||
curfset.set("value", name);
|
||||
if ($("id_active").checked) {
|
||||
curfset.getElements("option").each(function(item) {
|
||||
if (item.get("value") == name) {
|
||||
item.set("html", name + " (" + gettext("active") + ")");
|
||||
} else {
|
||||
item.set("html", item.get("value"));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="title"><h1>{{ title }}</h1></div>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<form action="{% url extensions.sievefilters.views.savescript %}"
|
||||
{% load i18n sfilters_tags %}
|
||||
<div id="fset_menu" class="leftbox">
|
||||
<h1>{% trans "Actions" %}</h1>
|
||||
{% fset_menu "raw" name %}
|
||||
</div>
|
||||
<form action="{% url extensions.sievefilters.views.savefs name %}"
|
||||
method="POST" id="feditor" name="feditor">
|
||||
<input type="hidden" name="scriptname" value="{{ name }}" />
|
||||
<textarea id="scriptcontent" name="scriptcontent">{{ scriptcontent }}</textarea>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -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("""
|
||||
<ul class="errors">
|
||||
{% for error in errors %}
|
||||
<li>{{ error|safe }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
""")
|
||||
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("""
|
||||
<div class="item">
|
||||
{{ tfield }}{{ofield}}{{ vfield }}{{ verrors }}
|
||||
</div>
|
||||
""")
|
||||
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("""
|
||||
<div class="item">
|
||||
{{ afield }}{% for v in values %}{{ v }}{% endfor %}
|
||||
</div>
|
||||
""")
|
||||
return t.render(template.Context({
|
||||
"afield" : action, "values" : values
|
||||
}))
|
||||
|
||||
@@ -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<name>.+)/$', 'savefs'),
|
||||
(r'^newfs/$', 'new_filters_set'),
|
||||
(r'^deletefs/$', 'delete_filters_set'),
|
||||
(r'^activatefs/$', 'activate_filters_set'),
|
||||
(r'^downloadfs/$', 'download_filters_set'),
|
||||
(r'^removefs/(?P<name>.+)/$', 'remove_filters_set'),
|
||||
(r'^activatefs/(?P<name>.+)/$', 'activate_filters_set'),
|
||||
(r'^downloadfs/(?P<name>.+)/$', 'download_filters_set'),
|
||||
(r'^templates/(?P<ftype>\w+)/$', 'get_templates'),
|
||||
(r'^userfolders/$', 'get_user_folders'),
|
||||
(r'^(?P<setname>.+)/newfilter/$', 'newfilter'),
|
||||
(r'^(?P<setname>.+)/editfilter/(?P<fname>.+)/$', 'editfilter'),
|
||||
(r'^(?P<setname>.+)/removefilter/(?P<fname>.+)/$', 'removefilter'),
|
||||
(r'^(?P<setname>.+)/togglestate/(?P<fname>.+)/$',
|
||||
'toggle_filter_state'),
|
||||
(r'^(?P<setname>.+)/moveup/(?P<fname>.+)/$',
|
||||
'move_filter_up'),
|
||||
(r'^(?P<setname>.+)/movedown/(?P<fname>.+)/$',
|
||||
'move_filter_down'),
|
||||
(r'^(?P<name>.+)/$', 'getfs'),
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
+11
-19
@@ -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):
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user