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:
Antoine Nguyen
2011-06-19 19:11:02 +02:00
parent add222840c
commit 7338dd1c8f
34 changed files with 1709 additions and 1320 deletions
+1 -14
View File
@@ -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 = [
+2 -1
View File
@@ -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 %}
-63
View File
@@ -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))
+211
View File
@@ -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
+15 -6
View File
@@ -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)):
+7 -1
View File
@@ -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)
-620
View File
@@ -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
+156 -34
View File
@@ -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);
});
}
});
+201
View File
@@ -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
}))
+16 -5
View File
@@ -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'),
)
+152 -36
View File
@@ -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
View File
@@ -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):
+3 -3
View File
@@ -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
View File
@@ -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" \
-160
View File
@@ -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"])
+178
View File
@@ -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
View File
@@ -73,7 +73,7 @@ td.even {
.title h1 {
float: left;
margin: 5px 10px;
margin: 5px;
font-size: medium;
font-weight: bold;
}
+209
View File
@@ -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();
}
});
-197
View File
@@ -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){
-5
View File
@@ -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'
+20 -11
View File
@@ -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

+4 -1
View File
@@ -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 %}