mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-18 05:51:26 +02:00
First working version of the new 'message filters' plugin. Currently, only the raw edition mode is available. I also include my own MANAGESIEVE client implementation.
see #139
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# coding: utf-8
|
||||
from modoboa.lib import parameters
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
parameters.register_admin("SECRET_KEY", type="string", deflt="abcdefghijklmnop",
|
||||
help=_("Key used to encrypt/decrypt passwords"))
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# coding: utf-8
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
import base64
|
||||
from modoboa.lib import parameters
|
||||
|
||||
def encrypt(clear):
|
||||
obj = AES.new(parameters.get_admin("SECRET_KEY"),
|
||||
AES.MODE_ECB)
|
||||
if len(clear) % AES.block_size:
|
||||
clear += "\0" * (AES.block_size - len(clear) % AES.block_size)
|
||||
ciph = obj.encrypt(clear)
|
||||
ciph = base64.b64encode(ciph)
|
||||
return ciph
|
||||
|
||||
def decrypt(ciph):
|
||||
obj = AES.new(parameters.get_admin("SECRET_KEY"), AES.MODE_ECB)
|
||||
ciph = base64.b64decode(ciph)
|
||||
clear = obj.decrypt(ciph)
|
||||
return clear.rstrip('\0')
|
||||
|
||||
def get_password(request):
|
||||
return decrypt(request.session["password"])
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.utils.translation import ugettext as _
|
||||
from modoboa.lib import events
|
||||
import modoboa.admin
|
||||
from forms import LoginForm
|
||||
from modoboa.auth.lib import *
|
||||
|
||||
def dologin(request):
|
||||
error = None
|
||||
@@ -19,6 +20,9 @@ def dologin(request):
|
||||
if user.is_active:
|
||||
login(request, user)
|
||||
|
||||
if request.user.id != 1:
|
||||
request.session["password"] = encrypt(request.POST["password"])
|
||||
|
||||
events.raiseEvent("UserLogin", request=request,
|
||||
username=request.POST["username"],
|
||||
password=request.POST["password"])
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# 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))
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext as _
|
||||
from modoboa.admin.templatetags.admin_extras import gender
|
||||
|
||||
class FiltersSetForm(forms.Form):
|
||||
name = forms.CharField()
|
||||
active = forms.BooleanField(label=gender("Active", "m"), required=False,
|
||||
initial=False,
|
||||
help_text=_("Check to activate this filters set"))
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# coding: utf-8
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from managesieve import ManageSieveClient, Error
|
||||
from modoboa.lib import ConnectionsManager, ConnectionError, parameters
|
||||
|
||||
class SieveClientError(Exception):
|
||||
pass
|
||||
|
||||
class SieveClient(object):
|
||||
__metaclass__ = ConnectionsManager
|
||||
|
||||
def __init__(self, user=None, password=None):
|
||||
ret, msg = self.login(user, password)
|
||||
if not ret:
|
||||
raise ConnectionError(msg)
|
||||
|
||||
def login(self, user, password):
|
||||
self.msc = ManageSieveClient(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":
|
||||
authmech = None
|
||||
if not self.msc.connect(user, password, use_starttls, authmech):
|
||||
return False, _("Connection to MANAGESIEVE server failed, check your configuration")
|
||||
return True, None
|
||||
|
||||
def refresh(self, user, password):
|
||||
try:
|
||||
self.msc.capability()
|
||||
except Error, e:
|
||||
pass
|
||||
else:
|
||||
return
|
||||
ret, msg = self.login(user, password)
|
||||
if not ret:
|
||||
raise ConnectionError(msg)
|
||||
|
||||
def listscripts(self):
|
||||
return self.msc.listscripts()
|
||||
|
||||
def getscript(self, name):
|
||||
content = self.msc.getscript(name)
|
||||
if content is None:
|
||||
raise SieveClientError(self.msc.errmsg)
|
||||
return content
|
||||
|
||||
def pushscript(self, name, content, active=False):
|
||||
if not self.msc.havespace(name, len(content)):
|
||||
error = "%s (%s)" % (_("Not enough space on server"), self.msc.errmsg)
|
||||
raise SieveClientError(error)
|
||||
if not self.msc.putscript(name, content):
|
||||
raise SieveClientError(self.msc.errmsg)
|
||||
if active and not self.msc.setactive(name):
|
||||
raise SieveClientError(self.msc.errmsg)
|
||||
|
||||
def deletescript(self, name):
|
||||
if not self.msc.deletescript(name):
|
||||
raise SieveClientError(self.msc.errmsg)
|
||||
|
||||
def activatescript(self, name):
|
||||
if not self.msc.setactive(name):
|
||||
raise SieveClientError(self.msc.errmsg)
|
||||
@@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
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
|
||||
|
||||
def init():
|
||||
events.register("UserMenuDisplay", menu)
|
||||
|
||||
parameters.register_admin("SERVER", type="string",
|
||||
deflt="127.0.0.1",
|
||||
help=_("Address of your MANAGESIEVE server"))
|
||||
parameters.register_admin("PORT", type="int", deflt="2000",
|
||||
help=_("Listening port of your MANAGESIEVE server"))
|
||||
parameters.register_admin("STARTTLS", type="list_yesno", deflt="no",
|
||||
help=_("Use the STARTTLS extension"))
|
||||
|
||||
values = [('AUTO', 'auto')]
|
||||
for m in SUPPORTED_AUTH_MECHS:
|
||||
values += [(m, m.lower())]
|
||||
parameters.register_admin("AUTHENTICATION_MECH", type="list", deflt="auto",
|
||||
values=values,
|
||||
help=_("Prefered authentication mechanism"))
|
||||
|
||||
def destroy():
|
||||
events.unregister("UserMenuDisplay", menu)
|
||||
parameters.unregister_app("sievefilters")
|
||||
|
||||
def infos():
|
||||
return {
|
||||
"name" : "Sieve filters",
|
||||
"version" : "1.0",
|
||||
"description" : _("Plugin to easily create server-side filters")
|
||||
}
|
||||
|
||||
def urls(prefix):
|
||||
return (r'^%ssfilters/' % prefix,
|
||||
include('modoboa.extensions.sievefilters.urls'))
|
||||
|
||||
def menu(**kwargs):
|
||||
import views
|
||||
|
||||
if kwargs["target"] != "options_menu":
|
||||
return []
|
||||
return [
|
||||
{"name" : "sievefilters",
|
||||
"label" : _("Message filters"),
|
||||
"url" : reverse(views.index),
|
||||
"img" : static_url("pics/filters.png")}
|
||||
]
|
||||
@@ -0,0 +1,620 @@
|
||||
#!/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
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#sets_list {
|
||||
position: absolute;
|
||||
border: 1px solid #CCC;
|
||||
left: 10px;
|
||||
top: 0px;
|
||||
bottom: 10px;
|
||||
width: 160px;
|
||||
|
||||
}
|
||||
|
||||
#set_content {
|
||||
position: absolute;
|
||||
left: 180px;
|
||||
right: 10px;
|
||||
top: 0px;
|
||||
bottom: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#sets_list ul {
|
||||
list-style: none;
|
||||
padding: 5px 0 0 5px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#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%;
|
||||
}
|
||||
|
||||
#set_content textarea {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#id_content {
|
||||
width: 95% !important;
|
||||
height: 270px;
|
||||
}
|
||||
|
||||
#submit_container {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#error {
|
||||
white-space: pre-line;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
{% extends "nlayout.html" %}
|
||||
{% load i18n libextras sfilters_tags %}
|
||||
{% block html_head %}
|
||||
<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 (!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 %}");
|
||||
}
|
||||
|
||||
$(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);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% block middletitle %}{% trans "Message filters" %}{% endblock %}
|
||||
{% block menu %}{% sfilters_menu user %}{% 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">
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,16 @@
|
||||
{% extends "layout_simple.html" %}
|
||||
{% load i18n %}
|
||||
{% 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>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="title"><h1>{{ title }}</h1></div>
|
||||
{% if error %}<div id="error" class="error">{{ error }}</div>{% endif %}
|
||||
<form action="" method="POST" id="{{ fname }}" name="{{ fname }}">
|
||||
{% include "common/genericform.html" %}
|
||||
<div id="submit_container">
|
||||
<input type="submit" name="submit" value='{{ submit_label }}'/>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
<form action="{% url extensions.sievefilters.views.savescript %}"
|
||||
method="POST" id="feditor" name="feditor">
|
||||
<input type="hidden" name="scriptname" value="{{ name }}" />
|
||||
<textarea id="scriptcontent" name="scriptcontent">{{ scriptcontent }}</textarea>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
from django import template
|
||||
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
|
||||
|
||||
register = template.Library()
|
||||
|
||||
from modoboa.lib import static_url
|
||||
|
||||
@register.simple_tag
|
||||
def sfilters_menu(user):
|
||||
entries = [
|
||||
{"name" : "newfilterset",
|
||||
"img" : static_url("pics/add.png"),
|
||||
"label" : _("New filters set"),
|
||||
"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),}
|
||||
]
|
||||
|
||||
return render_to_string('common/menulist.html',
|
||||
{"entries" : entries, "user" : user})
|
||||
@@ -0,0 +1,43 @@
|
||||
if __name__ == "__main__":
|
||||
clt = ManageSieveClient("mail.koalabs.org", 2000)
|
||||
if clt.connect("tonio@ngyn.org", "aka/12;c", starttls=True):
|
||||
print clt.get_sasl_mechanisms()
|
||||
print clt.get_sieve_capabilities()
|
||||
print clt.havespace("test", 45)
|
||||
|
||||
if not clt.putscript("myscript", """require ["fileinto", "envelope"];
|
||||
|
||||
if envelope :contains "to" "tmartin+sent" {
|
||||
fileinto "INBOX.sent";
|
||||
}
|
||||
"""):
|
||||
print clt.errmsg
|
||||
|
||||
if not clt.putscript("test", """#comment
|
||||
InvalidSieveCommand
|
||||
"""):
|
||||
print clt.errmsg
|
||||
|
||||
clt.deletescript("thescript")
|
||||
|
||||
if not clt.setactive("myscript"):
|
||||
print clt.errmsg
|
||||
print clt.listscripts()
|
||||
|
||||
sc = clt.getscript("myscript")
|
||||
if sc is None:
|
||||
print clt.errmsg
|
||||
else:
|
||||
print sc
|
||||
|
||||
if not clt.renamescript("myscript", "thescript"):
|
||||
print clt.errmsg
|
||||
|
||||
clt.setactive("")
|
||||
|
||||
if not clt.deletescript("thescript"):
|
||||
print clt.errmsg
|
||||
else:
|
||||
print clt.errmsg
|
||||
clt.logout()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from django.conf.urls.defaults import *
|
||||
|
||||
urlpatterns = patterns('modoboa.extensions.sievefilters.views',
|
||||
(r'^$', 'index'),
|
||||
(r'^getscript/$', 'getscript'),
|
||||
(r'^savescript/$', 'savescript'),
|
||||
(r'^newfs/$', 'new_filters_set'),
|
||||
(r'^deletefs/$', 'delete_filters_set'),
|
||||
(r'^activatefs/$', 'activate_filters_set'),
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
# coding: utf-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.auth.lib import get_password
|
||||
from lib import *
|
||||
from forms import *
|
||||
|
||||
@login_required
|
||||
def index(request, tplname="sievefilters/index.html"):
|
||||
sc = SieveClient(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
active_script, scripts = sc.listscripts()
|
||||
return _render(request, tplname, {
|
||||
"active_script" : active_script,
|
||||
"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)
|
||||
|
||||
@login_required
|
||||
def savescript(request):
|
||||
if not request.POST.has_key("scriptname") or \
|
||||
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"])
|
||||
except SieveClientError, e:
|
||||
error = str(e)
|
||||
return ajax_response(request, "ko", respmsg=error)
|
||||
|
||||
return ajax_response(request, respmsg=_("Filters set saved"))
|
||||
|
||||
@login_required
|
||||
def new_filters_set(request, tplname="sievefilters/newfiltersset.html"):
|
||||
ctx = {"title" : _("Create a new filters set"),
|
||||
"fname" : "newfiltersset",
|
||||
"submit_label" : _("Create"),
|
||||
"withmenu" : False,
|
||||
"withunseen" : False}
|
||||
if request.method == "POST":
|
||||
form = FiltersSetForm(request.POST)
|
||||
error = None
|
||||
if form.is_valid():
|
||||
sc = SieveClient(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
try:
|
||||
sc.pushscript(form.cleaned_data["name"], "# Empty script",
|
||||
form.cleaned_data["active"])
|
||||
except SieveClientError, e:
|
||||
error = str(e)
|
||||
else:
|
||||
return ajax_response(request, url=reverse(index),
|
||||
respmsg=_("Filters set created"))
|
||||
ctx["form"] = form
|
||||
ctx["error"] = error
|
||||
return ajax_response(request, status="ko", template=tplname, **ctx)
|
||||
|
||||
ctx["form"] = FiltersSetForm()
|
||||
return _render(request, tplname, ctx)
|
||||
|
||||
@login_required
|
||||
def delete_filters_set(request):
|
||||
if not request.GET.has_key("name") or \
|
||||
request.GET["name"] == "":
|
||||
return
|
||||
sc = SieveClient(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
try:
|
||||
sc.deletescript(request.GET["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
|
||||
sc = SieveClient(user=request.user.username,
|
||||
password=request.session["password"])
|
||||
try:
|
||||
sc.activatescript(request.GET["name"])
|
||||
except SieveClientError, e:
|
||||
return ajax_response(request, "ko", respmsg=str(e))
|
||||
return ajax_response(request, respmsg=_("Filters set activated"))
|
||||
@@ -14,9 +14,9 @@ import email.utils
|
||||
import lxml
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from modoboa.lib import decode, tables, imap_utf7, Singleton
|
||||
from modoboa.lib import decode, u2u_decode, tables, \
|
||||
imap_utf7, static_url, ConnectionsManager, parameters
|
||||
from modoboa.lib.email_listing import MBconnector, EmailListing, Email
|
||||
from modoboa.lib import tables, imap_utf7, parameters, static_url, u2u_decode
|
||||
|
||||
class WebmailError(Exception):
|
||||
errorexpr = re.compile("\[([^\]]+)\]\s*([^\.]+)")
|
||||
@@ -113,29 +113,6 @@ class IMAPheader(object):
|
||||
def parse_subject(value, **kwargs):
|
||||
return decode(u2u_decode.u2u_decode(value))
|
||||
|
||||
class ConnectionsManager(type):
|
||||
def __init__(cls, name, bases, dict):
|
||||
super(ConnectionsManager, cls).__init__(name, bases, dict)
|
||||
cls.instances = {}
|
||||
|
||||
def __call__(cls, **kwargs):
|
||||
key = None
|
||||
if kwargs.has_key("user"):
|
||||
key = kwargs["user"]
|
||||
else:
|
||||
return None
|
||||
if not cls.instances.has_key(key):
|
||||
cls.instances[key] = None
|
||||
if kwargs.has_key("password"):
|
||||
kwargs["password"] = decrypt(kwargs["password"])
|
||||
|
||||
if cls.instances[key] is None:
|
||||
cls.instances[key] = \
|
||||
super(ConnectionsManager, cls).__call__(**kwargs)
|
||||
else:
|
||||
cls.instances[key].refresh(key, kwargs["password"])
|
||||
return cls.instances[key]
|
||||
|
||||
class IMAPconnector(object):
|
||||
__metaclass__ = ConnectionsManager
|
||||
|
||||
@@ -248,6 +225,10 @@ class IMAPconnector(object):
|
||||
for mb in data:
|
||||
flags, delimiter, name = list_response_pattern.match(mb).groups()
|
||||
name = name.strip('"').decode("imap4-utf-7")
|
||||
# Dovecot stores user scripts into a specific folder, I'm
|
||||
# wondering why this folder appears in 'list' result?
|
||||
if name in ["dovecot.sieve"]:
|
||||
continue
|
||||
if re.search("\%s" % delimiter, name):
|
||||
parts = name.split(".")
|
||||
if not descr.has_key("path"):
|
||||
@@ -642,24 +623,3 @@ def find_images_in_body(body):
|
||||
|
||||
return lxml.html.tostring(html), parts
|
||||
|
||||
|
||||
def encrypt(clear):
|
||||
from Crypto.Cipher import AES
|
||||
import base64
|
||||
|
||||
obj = AES.new(parameters.get_admin("SECRET_KEY"),
|
||||
AES.MODE_ECB)
|
||||
if len(clear) % AES.block_size:
|
||||
clear += "\0" * (AES.block_size - len(clear) % AES.block_size)
|
||||
ciph = obj.encrypt(clear)
|
||||
ciph = base64.b64encode(ciph)
|
||||
return ciph
|
||||
|
||||
def decrypt(ciph):
|
||||
from Crypto.Cipher import AES
|
||||
import base64
|
||||
|
||||
obj = AES.new(parameters.get_admin("SECRET_KEY"), AES.MODE_ECB)
|
||||
ciph = base64.b64decode(ciph)
|
||||
clear = obj.decrypt(ciph)
|
||||
return clear.rstrip('\0')
|
||||
|
||||
@@ -6,11 +6,8 @@ from modoboa.lib import events, parameters, static_url
|
||||
|
||||
def init():
|
||||
events.register("UserMenuDisplay", menu)
|
||||
events.register("UserLogin", userlogin)
|
||||
events.register("UserLogout", userlogout)
|
||||
|
||||
parameters.register_admin("SECRET_KEY", type="string", deflt="abcdefghijklmnop",
|
||||
help=_("Key used to encrypt/decrypt passwords"))
|
||||
parameters.register_admin("IMAP_SERVER", type="string",
|
||||
deflt="127.0.0.1",
|
||||
help=_("Address of your IMAP server"))
|
||||
@@ -67,7 +64,6 @@ def init():
|
||||
|
||||
def destroy():
|
||||
events.unregister("UserMenuDisplay", menu)
|
||||
events.unregister("UserLogin", userlogin)
|
||||
parameters.unregister_app("webmail")
|
||||
|
||||
def infos():
|
||||
@@ -93,13 +89,6 @@ def menu(**kwargs):
|
||||
"img" : static_url("pics/webmail.png")}
|
||||
]
|
||||
|
||||
def userlogin(**kwargs):
|
||||
from lib import IMAPconnector, encrypt
|
||||
|
||||
if kwargs["request"].user.id == 1:
|
||||
return
|
||||
kwargs["request"].session["password"] = encrypt(kwargs["password"])
|
||||
|
||||
def userlogout(**kwargs):
|
||||
from lib import IMAPconnector
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from modoboa.lib import parameters, _render, _render_error, \
|
||||
getctx, is_not_localadmin, _render_to_string, split_mailbox, \
|
||||
ajax_response
|
||||
from modoboa.lib.email_listing import parse_search_parameters, Paginator
|
||||
from modoboa.auth.lib import *
|
||||
from lib import *
|
||||
from forms import *
|
||||
from templatetags.webextras import *
|
||||
@@ -365,7 +366,7 @@ def send_mail(request, withctx=False, origmsg=None, posturl=None):
|
||||
error = str(text)
|
||||
if error is None:
|
||||
if parameters.get_admin("SMTP_AUTHENTICATION") == "yes":
|
||||
s.login(request.user.username, decrypt(request.session["password"]))
|
||||
s.login(request.user.username, get_password(request))
|
||||
s.sendmail(msg['From'], rcpts, msg.as_string())
|
||||
s.quit()
|
||||
sentfolder = parameters.get_user(request.user, "SENT_FOLDER")
|
||||
|
||||
+33
-11
@@ -12,6 +12,7 @@ from django.template.loader import render_to_string
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils import simplejson
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.auth.lib import decrypt
|
||||
|
||||
def exec_cmd(cmd, **kwargs):
|
||||
import subprocess
|
||||
@@ -94,6 +95,8 @@ def ajax_response(request, status="ok", respmsg=None,
|
||||
ctx[k] = v
|
||||
if template is not None:
|
||||
content = _render_to_string(request, template, ctx)
|
||||
elif kwargs.has_key("content"):
|
||||
content = kwargs["content"]
|
||||
else:
|
||||
content = ""
|
||||
jsonctx = {"status" : status, "content" : content}
|
||||
@@ -156,17 +159,6 @@ def db_table_exists(table, cursor=None):
|
||||
else:
|
||||
return table in table_names
|
||||
|
||||
class Singleton(type):
|
||||
def __init__(cls, name, bases, dict):
|
||||
super(Singleton, cls).__init__(name, bases, dict)
|
||||
cls.instance = None
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls.instance is None:
|
||||
cls.instance = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
return cls.instance
|
||||
|
||||
|
||||
def _check_password(password, crypted):
|
||||
scheme = parameters.get_admin("PASSWORD_SCHEME", app="admin")
|
||||
if scheme == "crypt":
|
||||
@@ -193,3 +185,33 @@ def split_mailbox(mailbox):
|
||||
domain = parts[-1]
|
||||
address = "@".join(parts[:-1])
|
||||
return (address, domain)
|
||||
|
||||
class ConnectionsManager(type):
|
||||
"""Singleton pattern implementation
|
||||
|
||||
This class is specialized in connection management.
|
||||
"""
|
||||
def __init__(cls, name, bases, dict):
|
||||
super(ConnectionsManager, cls).__init__(name, bases, dict)
|
||||
cls.instances = {}
|
||||
|
||||
def __call__(cls, **kwargs):
|
||||
key = None
|
||||
if kwargs.has_key("user"):
|
||||
key = kwargs["user"]
|
||||
else:
|
||||
return None
|
||||
if not cls.instances.has_key(key):
|
||||
cls.instances[key] = None
|
||||
if kwargs.has_key("password"):
|
||||
kwargs["password"] = decrypt(kwargs["password"])
|
||||
|
||||
if cls.instances[key] is None:
|
||||
cls.instances[key] = \
|
||||
super(ConnectionsManager, cls).__call__(**kwargs)
|
||||
else:
|
||||
cls.instances[key].refresh(key, kwargs["password"])
|
||||
return cls.instances[key]
|
||||
|
||||
class ConnectionError(Exception):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# encoding: utf-8
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import DataMigration
|
||||
from django.db import models
|
||||
|
||||
class Migration(DataMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
"Write your forwards methods here."
|
||||
p = orm.Parameter.objects.get(name="webmail.SECRET_KEY")
|
||||
p.name = "auth.SECRET_KEY"
|
||||
p.save()
|
||||
|
||||
def backwards(self, orm):
|
||||
"Write your backwards methods here."
|
||||
p = orm.Parameter.objects.get(name="auth.SECRET_KEY")
|
||||
p.name = "webmail.SECRET_KEY"
|
||||
p.save()
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'lib.parameter': {
|
||||
'Meta': {'object_name': 'Parameter'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
|
||||
},
|
||||
'lib.userparameter': {
|
||||
'Meta': {'object_name': 'UserParameter'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
|
||||
'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['lib']
|
||||
@@ -102,6 +102,7 @@ INSTALLED_APPS = (
|
||||
'modoboa.extensions.webmail',
|
||||
'modoboa.extensions.stats',
|
||||
'modoboa.extensions.amavis_quarantine',
|
||||
'modoboa.extensions.sievefilters',
|
||||
)
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
|
||||
+4
-1
@@ -6,7 +6,6 @@ body {
|
||||
}
|
||||
|
||||
div.row {
|
||||
/*position: relative;*/
|
||||
clear: both;
|
||||
padding-top: 10px;
|
||||
padding-left: 20px;
|
||||
@@ -134,3 +133,7 @@ td.even {
|
||||
background: #eee;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
clear: both;
|
||||
}
|
||||
+12
-1
@@ -13,6 +13,10 @@ var error_callbacks = {
|
||||
}
|
||||
};
|
||||
|
||||
var success_callbacks = {
|
||||
|
||||
};
|
||||
|
||||
var extra_data_callbacks = {
|
||||
|
||||
};
|
||||
@@ -29,6 +33,10 @@ function register_error_callback(name, callback) {
|
||||
return register_callback(error_callbacks, name, callback);
|
||||
}
|
||||
|
||||
function register_success_callback(name, callback) {
|
||||
return register_callback(success_callbacks, name, callback);
|
||||
}
|
||||
|
||||
function register_extra_data_callback(name, callback) {
|
||||
return register_callback(extra_data_callbacks, name, callback);
|
||||
}
|
||||
@@ -80,7 +88,10 @@ function ajax_submit(event) {
|
||||
} else if ($defined(response.url)) {
|
||||
parent.location.href = response.url;
|
||||
}
|
||||
parent.SqueezeBox.close();
|
||||
if ($defined(success_callbacks[myid])) {
|
||||
success_callbacks[myid](response);
|
||||
}
|
||||
parent.SqueezeBox.close();
|
||||
}
|
||||
},
|
||||
onFailure: function(xhr) {
|
||||
|
||||
+14
-4
@@ -22,13 +22,11 @@ var InfoBox = new Class({
|
||||
"font-size" : "1em",
|
||||
"font-weight" : "bold",
|
||||
"text-align" : "left",
|
||||
"width" : this.options.width + "px",
|
||||
"padding" : "10px",
|
||||
"position" : "absolute",
|
||||
"top" : "0px",
|
||||
"left" : boxleft + "px",
|
||||
"display" : "none"
|
||||
});
|
||||
this.calcsize();
|
||||
this.setProfile("gray");
|
||||
this.spinner = new Element("img", {
|
||||
"src" : "/static/css/spinner.gif"
|
||||
@@ -38,12 +36,24 @@ var InfoBox = new Class({
|
||||
|
||||
this.label = new Element("label").set("html", this.options.message);
|
||||
this.label.setStyles({
|
||||
"margin-left" : "10px"
|
||||
"white-space": "pre-line"
|
||||
/*"margin-left" : "10px"*/
|
||||
});
|
||||
this.label.inject(this.box);
|
||||
this.box.inject($(document.body));
|
||||
},
|
||||
|
||||
calcsize: function(width) {
|
||||
var w = ($defined(width) ? width : this.options.width);
|
||||
var boxleft = $(document.body).getSize().x / 2 - w / 2;
|
||||
|
||||
this.box.setStyles({
|
||||
width: w + "px",
|
||||
left: boxleft + "px",
|
||||
top: "0px"
|
||||
});
|
||||
},
|
||||
|
||||
setMessage: function(text) {
|
||||
this.label.set("html", text);
|
||||
},
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 682 B |
Binary file not shown.
|
After Width: | Height: | Size: 776 B |
Binary file not shown.
|
After Width: | Height: | Size: 911 B |
Reference in New Issue
Block a user