From ace8448efea2d1c1453cd2ad3ede88aa9ff199e6 Mon Sep 17 00:00:00 2001 From: tonio Date: Thu, 10 Jun 2010 16:07:00 +0200 Subject: [PATCH] Ticket #57 and #59. Go back to passwords stored in session. This time, we use pycrypto to encrypt passwords using the AES algorithm. http://www.dlitz.net/software/pycrypto/ IMAP connections are now refreshed. --- .../webmail/{imap_listing.py => lib.py} | 58 ++++++++-- extensions/webmail/main.py | 22 ++-- extensions/webmail/templates/compose.html | 3 +- extensions/webmail/templates/headers.html | 2 +- extensions/webmail/templates/index.html | 3 + extensions/webmail/templatetags/webextras.py | 6 +- extensions/webmail/views.py | 104 +++++++++++------- 7 files changed, 134 insertions(+), 64 deletions(-) rename extensions/webmail/{imap_listing.py => lib.py} (90%) diff --git a/extensions/webmail/imap_listing.py b/extensions/webmail/lib.py similarity index 90% rename from extensions/webmail/imap_listing.py rename to extensions/webmail/lib.py index 1f2469935..afb0c2b37 100644 --- a/extensions/webmail/imap_listing.py +++ b/extensions/webmail/lib.py @@ -122,7 +122,7 @@ class ConnectionsManager(type): super(ConnectionsManager, cls).__init__(name, bases, dict) cls.instances = {} - def __call__(cls, *args, **kwargs): + def __call__(cls, **kwargs): key = None if kwargs.has_key("user"): key = kwargs["user"] @@ -130,10 +130,14 @@ class ConnectionsManager(type): return None if not cls.instances.has_key(key): cls.instances[key] = None - if cls.instances[key] is None or cls.instances[key].invalid: - # I hope python's garbage collector will detect this correclty + if kwargs.has_key("password"): + kwargs["password"] = decrypt(kwargs["password"]) + + if cls.instances[key] is None: cls.instances[key] = \ - super(ConnectionsManager, cls).__call__(*args, **kwargs) + super(ConnectionsManager, cls).__call__(**kwargs) + else: + cls.instances[key].refresh(key, kwargs["password"]) return cls.instances[key] class IMAPconnector(object): @@ -143,11 +147,24 @@ class IMAPconnector(object): self.criterions = [] self.address = parameters.get_admin("webmail", "IMAP_SERVER") self.port = int(parameters.get_admin("webmail", "IMAP_PORT")) - self.invalid = False status, msg = self.login(user, password) if not status: raise Exception(msg) + def refresh(self, user, password): + """Check if current connection needs a refresh + + Is it really secure? + """ + if self.m is not None: + try: + self.m.select() + return + except imaplib.IMAP4.error, error: + print error + print self.login(user, password) + + def login(self, user, passwd): import imaplib try: @@ -167,11 +184,12 @@ class IMAPconnector(object): def logout(self): try: - self.m.close() + self.m.select() except imaplib.IMAP4.error: pass self.m.logout() - self.invalid = True + del self.m + self.m = None def _encodefolder(self, folder): if not folder: @@ -318,9 +336,9 @@ class ImapListing(EmailListing): defcallback = "wm_updatelisting" reset_wm_url = False - def __init__(self, user, **kwargs): + def __init__(self, user, password, **kwargs): self.user = user - self.mbc = IMAPconnector(user=user.username) + self.mbc = IMAPconnector(user=user.username, password=password) if kwargs.has_key("pattern"): self.parse_search_parameters(kwargs["criteria"], kwargs["pattern"]) @@ -414,3 +432,25 @@ class ImapEmail(Email): "folder" : kwargs["folder"], "mail_id" : kwargs["mail_id"], "attachments" : attachments != [] and attachments or None }) + +def encrypt(clear): + from Crypto.Cipher import AES + import base64 + + obj = AES.new(parameters.get_admin("webmail", "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("webmail", "SECRET_KEY"), + AES.MODE_ECB) + ciph = base64.b64decode(ciph) + clear = obj.decrypt(ciph) + return clear.rstrip('\0') diff --git a/extensions/webmail/main.py b/extensions/webmail/main.py index c00d48525..d6b3a0dc6 100644 --- a/extensions/webmail/main.py +++ b/extensions/webmail/main.py @@ -8,6 +8,10 @@ def init(): events.register("UserMenuDisplay", menu) events.register("UserLogin", userlogin) events.register("UserLogout", userlogout) + + parameters.register_admin("webmail", "SECRET_KEY", type="string", + deflt="abcdefghijklmnop", + help=_("Key used to encrypt/decrypt passwords")) parameters.register_admin("webmail", "IMAP_SERVER", type="string", deflt="127.0.0.1", help=_("Address of your IMAP server")) @@ -62,17 +66,17 @@ def menu(**kwargs): ] def userlogin(**kwargs): - from imap_listing import IMAPconnector + 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 if kwargs["request"].user.id == 1: return m = IMAPconnector(user=kwargs["request"].user.username, - password=kwargs["password"]) - -def userlogout(**kwargs): - from imap_listing import IMAPconnector - - if kwargs["request"].user.id == 1: - return - m = IMAPconnector(user=kwargs["request"].user.username) + password=kwargs["request"].session["password"]) m.logout() diff --git a/extensions/webmail/templates/compose.html b/extensions/webmail/templates/compose.html index 99c3d3a7b..c6f0f4ea1 100644 --- a/extensions/webmail/templates/compose.html +++ b/extensions/webmail/templates/compose.html @@ -43,8 +43,7 @@ {% include "common/genericform.html" %} {% if body %} - {% else %} diff --git a/extensions/webmail/templates/headers.html b/extensions/webmail/templates/headers.html index 11d702132..1dc5c534f 100644 --- a/extensions/webmail/templates/headers.html +++ b/extensions/webmail/templates/headers.html @@ -6,7 +6,7 @@ {% trans "Attachments" %} - {% url extensions.webmail.main.getattachment folder mail_id as test %} + {% url extensions.webmail.views.getattachment folder mail_id as test %} {% for attachment in attachments %} {{ attachment }} {% endfor %} diff --git a/extensions/webmail/templates/index.html b/extensions/webmail/templates/index.html index 1021ca568..3e26387d6 100644 --- a/extensions/webmail/templates/index.html +++ b/extensions/webmail/templates/index.html @@ -185,6 +185,9 @@ ul.visible { if (resp.status == "ko") { wm_updatelisting(resp); window.fireEvent("resize"); + if ($defined(resp.error)) { + infobox.error(resp.error); + } return; } current_anchor.parse_string(resp.url, true).setparams(navparams); diff --git a/extensions/webmail/templatetags/webextras.py b/extensions/webmail/templatetags/webextras.py index 6dcbce399..d23c56157 100644 --- a/extensions/webmail/templatetags/webextras.py +++ b/extensions/webmail/templatetags/webextras.py @@ -3,7 +3,8 @@ from django.template.loader import render_to_string from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse from mailng.extensions import webmail -from mailng.extensions.webmail.imap_listing import IMAPheader +from mailng.extensions.webmail.lib import IMAPheader +from mailng.lib import parameters register = template.Library() @@ -85,7 +86,8 @@ def listing_menu(selection, folder, user): {"name" : "fdaction", "label" : _("Empty folder"), "img" : "/static/pics/clear.png", - "url" : folder == "Trash" \ + "url" : folder == parameters.get_user(user, "webmail", + "TRASH_FOLDER") \ and reverse(webmail.views.empty, args=[folder]) or ""} ] } diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index d19f4cee0..62a973924 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -10,10 +10,10 @@ from django.core.urlresolvers import reverse from django.contrib.auth.decorators import login_required from mailng.admin.models import Mailbox from mailng.lib import parameters, _render, _render_error, getctx, is_not_localadmin -from imap_listing import * +from mailng.lib.email_listing import parse_search_parameters +from lib import * from forms import * from templatetags.webextras import * -from lib.email_listing import parse_search_parameters def __get_current_url(request): res = "%s?page=%s" % (request.session["folder"], request.session["page"]) @@ -47,7 +47,7 @@ def folder(request, name, updatenav=True): optparams["criteria"] = request.session["criteria"] else: optparams["reset"] = True - lst = ImapListing(request.user, + lst = ImapListing(request.user, request.session["password"], baseurl=name, folder=name, order=order, **optparams) page = lst.paginator.getpage(request.session["page"]) @@ -55,31 +55,33 @@ def folder(request, name, updatenav=True): content = lst.fetch(request, page.id_start, page.id_stop) navbar = lst.render_navbar(page) else: - content = "Empty folder" + content = _("Empty folder") navbar = "" folders = render_to_string("webmail/folders.html", { "folders" : lst.getfolders() }) ctx = getctx("ok", folders=folders, listing=content, navbar=navbar, - menu=listing_menu("", name, request.user.get_all_permissions())) + menu=listing_menu("", name, request.user)) return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") @login_required @is_not_localadmin() def index(request): - try: - navp = request.session.has_key("navparams") \ - and request.session["navparams"] or {} - lst = ImapListing(request.user, - baseurl="INBOX", navparams=navp, folder="INBOX", - empty=True) - except Exception, exp: - return _render_error(request, user_context={"error" : exp}) + #try: + navp = request.session.has_key("navparams") \ + and request.session["navparams"] or {} + lst = ImapListing(request.user, request.session["password"], + baseurl="INBOX", navparams=navp, folder="INBOX", + empty=True) + #except Exception, exp: + # return _render_error(request, user_context={"error" : exp}) return lst.render(request) def fetchmail(request, folder, mail_id, all=False): - res = IMAPconnector(user=request.user.username).fetch(start=mail_id, - folder=folder, all=all) + res = IMAPconnector(user=request.user.username, + password=request.session["password"]).fetch(start=mail_id, + folder=folder, + all=all) if len(res): return res[0] return None @@ -102,7 +104,8 @@ def viewmail(request, folder, mail_id=None): def getmailcontent(request, folder, mail_id): msg = fetchmail(request, folder, mail_id, True) if "class" in msg.keys() and msg["class"] == "unseen": - IMAPconnector(user=request.user.username).msg_read(folder, mail_id) + IMAPconnector(user=request.user.username, + password=request.session["password"]).msg_read(folder, mail_id) email = ImapEmail(msg, mode="html", links="1") try: pageid = request.session["page"] @@ -141,14 +144,16 @@ def move(request): for arg in ["msgset", "to"]: if not request.GET.has_key(arg): return - mbc = IMAPconnector(user=request.user.username) + mbc = IMAPconnector(user=request.user.username, + password=request.session["password"]) mbc.move(request.GET["msgset"], request.session["folder"], request.GET["to"]) return folder(request, request.session["folder"], False) @login_required @is_not_localadmin() def delete(request, fdname, mail_id): - mbc = IMAPconnector(user=request.user.username) + mbc = IMAPconnector(user=request.user.username, + password=request.session["password"]) mbc.move(mail_id, fdname, parameters.get_user(request.user, "webmail", "TRASH_FOLDER")) return folder(request, fdname, False) @@ -158,7 +163,8 @@ def delete(request, fdname, mail_id): def mark(request, name): if not request.GET.has_key("status") or not request.GET.has_key("ids"): return - mbc = IMAPconnector(user=request.user.username) + mbc = IMAPconnector(user=request.user.username, + password=request.session["password"]) try: getattr(mbc, "msg_%s" % request.GET["status"])(name, request.GET["ids"]) except AttributeError: @@ -168,15 +174,17 @@ def mark(request, name): @login_required @is_not_localadmin() def empty(request, name): - if name == "Trash": - mbc = IMAPconnector(user=request.user.username) + if name == parameters.get_user(request.user, "webmail", "TRASH_FOLDER"): + mbc = IMAPconnector(user=request.user.username, + password=request.session["password"]) mbc.empty(name) return folder(request, name, False) @login_required @is_not_localadmin() def compact(request, name): - mbc = IMAPconnector(user=request.user.username) + mbc = IMAPconnector(user=request.user.username, + password=request.session["password"]) mbc.compact(name) return folder(request, name, False) @@ -190,8 +198,10 @@ def render_compose(request, form, posturl, bodyheader=None, body=None): ctx = getctx("ok", level=2, menu=menu, listing=content) return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") -def send_mail(request, withctx=False, origmsg=None): +def send_mail(request, withctx=False, origmsg=None, posturl=None): form = ComposeMailForm(request.POST) + error = None + ctx = None if form.is_valid(): from email.mime.text import MIMEText from email.utils import make_msgid, formatdate @@ -211,24 +221,30 @@ def send_mail(request, withctx=False, origmsg=None): if "cc" in request.POST.keys(): msg["Cc"] = request.POST["cc"] rcpts += msg["Cc"].split(",") + error = None try: s = smtplib.SMTP(parameters.get_admin("webmail", "SMTP_SERVER")) if parameters.get_admin("webmail", "SMTP_SECURED") == "yes": s.starttls() - except (smtplib.SMTPException, ssl.SSLError), error: - print error - # Prévoir la remontée de cette erreur au niveau du client!! + except (smtplib.SMTPException, ssl.SSLError), text: + error = text + if error is None: + if parameters.get_admin("webmail", "SMTP_AUTHENTICATION") == "yes": + s.login(request.user.username, decrypt(request.session["password"])) + s.sendmail(msg['From'], rcpts, msg.as_string()) + s.quit() + sentfolder = parameters.get_user(request.user, "webmail", "SENT_FOLDER") + IMAPconnector(user=request.user.username, + password=request.session["password"]).push_mail(sentfolder, msg) + ctx = getctx("ok", url=__get_current_url(request)) + + if ctx is None: + listing = render_to_string("webmail/compose.html", + {"form" : form, + "body" : request.POST["id_body"].strip(), + "posturl" : posturl}) + ctx = getctx("ko", level=2, error=error, listing=listing) - if parameters.get_admin("webmail", "SMTP_AUTHENTICATION") == "yes": - s.login(request.user.username, request.session["password"]) - s.sendmail(msg['From'], rcpts, msg.as_string()) - s.quit() - sentfolder = parameters.get_user(request.user, "webmail", "SENT_FOLDER") - IMAPconnector(user=request.user.username).push_mail(sentfolder, msg) - ctx = getctx("ok", url=__get_current_url(request)) - else: - ctx = getctx("ko", level=2, listing=render_to_string("webmail/compose.html", - {"form" : form})) if not withctx: return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") return ctx, HttpResponse(simplejson.dumps(ctx), mimetype="application/json") @@ -237,7 +253,7 @@ def send_mail(request, withctx=False, origmsg=None): @is_not_localadmin() def compose(request): if request.method == "POST": - return send_mail(request) + return send_mail(request, posturl=reverse(compose)) form = ComposeMailForm() form.fields["from_"].initial = request.user.username @@ -248,9 +264,12 @@ def compose(request): def reply(request, folder, mail_id): msg = fetchmail(request, folder, mail_id, True) if request.method == "POST": - ctx, r = send_mail(request, True, origmsg=msg) + ctx, r = send_mail(request, True, origmsg=msg, + posturl=reverse(reply, args=[folder, mail_id])) if ctx["status"] == "ok": - IMAPconnector(user=request.user.usernam).msg_answered(folder, mail_id) + IMAPconnector(user=request.user.username, + password=request.session["password"]).msg_answered(folder, + mail_id) return r email = ImapEmail(msg, True) lines = email.body.split('\n') @@ -290,9 +309,12 @@ def reply(request, folder, mail_id): @is_not_localadmin() def forward(request, folder, mail_id): if request.method == "POST": - ctx, response = send_mail(request, True) + ctx, response = send_mail(request, True, + posturl=reverse(forward, args=[folder, mail_id])) if ctx["status"] == "ok": - IMAPconnector(user=request.user.username).msgforwarded(folder, mail_id) + IMAPconnector(user=request.user.username, + password=request.session["password"]).msgforwarded(folder, + mail_id) return response msg = fetchmail(request, folder, mail_id, True)