mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02: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,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 %}
|
||||
<a href="{{ a.url }}" name="{{ a.name }}" class="{{ a.class }}" rel="{{ a.rel }}"
|
||||
{% if a.confirm %}onclick="return confirm('{{ a.confirm }}')"{% endif %}>
|
||||
<img src="{{ a.img }}" border="0" title="{{ a.title }}" />
|
||||
</a>
|
||||
{% endfor %}
|
||||
""")
|
||||
return t.render(template.Context({
|
||||
"actions" : actions
|
||||
}))
|
||||
|
||||
@register.simple_tag
|
||||
def domain_actions(user, domid):
|
||||
actions = [
|
||||
|
||||
@@ -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();
|
||||
|
||||
+17
-4
@@ -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 %}
|
||||
<a href="{{ a.url }}" name="{{ a.name }}" class="{{ a.class }}" rel="{{ a.rel }}"
|
||||
{% if a.confirm %}onclick="return confirm('{{ a.confirm }}')"{% endif %}>
|
||||
<img src="{{ a.img }}" border="0" title="{{ a.title }}" />
|
||||
</a>
|
||||
{% endfor %}
|
||||
""")
|
||||
return t.render(template.Context({
|
||||
"actions" : actions
|
||||
}))
|
||||
|
||||
def exec_as_vuser(cmd):
|
||||
code, output = exec_cmd("sudo -u %s %s" \
|
||||
|
||||
@@ -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 "<pre>%s</pre>" % 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"])
|
||||
|
||||
@@ -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 "<pre>%s</pre>" % 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
|
||||
+1
-1
@@ -73,7 +73,7 @@ td.even {
|
||||
|
||||
.title h1 {
|
||||
float: left;
|
||||
margin: 5px 10px;
|
||||
margin: 5px;
|
||||
font-size: medium;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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){
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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--;
|
||||
}
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 683 B |
Binary file not shown.
|
After Width: | Height: | Size: 652 B |
@@ -3,6 +3,7 @@
|
||||
{% load libextras %}
|
||||
{% block html_head %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/searchbar.css" />
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}js/anchor_nav.js"></script>
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}js/ProgressBar.js"></script>
|
||||
<script type="text/javascript" src="{{ MEDIA_URL }}js/email_listing.js"></script>
|
||||
{% block css %}{% endblock %}
|
||||
@@ -27,6 +28,8 @@
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
var current_anchor = new AnchorNavigation("{{ deflocation }}");
|
||||
|
||||
function viewmail(event) {
|
||||
event.stop();
|
||||
if (event.target.parentNode.id) {
|
||||
@@ -45,7 +48,7 @@ window.addEvent("domready", function() {
|
||||
{% join navparams %}
|
||||
});
|
||||
{% block js %}{% endblock %}
|
||||
ajaxListener("{{ deflocation }}", {{ defcallback }});
|
||||
current_anchor.set_default_callback({{ defcallback }});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user