From 487266ecb5343854ab48a259196774359cf0b357 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 16 Nov 2011 18:44:10 +0100 Subject: [PATCH] wip: Webmail big refactoring and performance improvment Using LSUB and LIST-EXTENDED. --- extensions/webmail/exceptions.py | 6 ++ extensions/webmail/imaputils.py | 101 ++++++++++++++++++++---- extensions/webmail/middleware.py | 14 ++-- extensions/webmail/static/js/webmail.js | 12 ++- extensions/webmail/views.py | 7 +- lib/email_listing.py | 52 ++++++------ static/js/anchor_nav.js | 2 +- 7 files changed, 147 insertions(+), 47 deletions(-) diff --git a/extensions/webmail/exceptions.py b/extensions/webmail/exceptions.py index a5d265a8f..f05766371 100644 --- a/extensions/webmail/exceptions.py +++ b/extensions/webmail/exceptions.py @@ -20,3 +20,9 @@ class WebmailError(Exception): def __str__(self): return self.reason +class ImapError(Exception): + def __init__(self, reason): + self.reason = reason + + def __str__(self): + return str(self.reason) diff --git a/extensions/webmail/imaputils.py b/extensions/webmail/imaputils.py index 43fa0b477..98c7a56e8 100644 --- a/extensions/webmail/imaputils.py +++ b/extensions/webmail/imaputils.py @@ -5,13 +5,43 @@ """ import imaplib, ssl, email import re +from functools import wraps +from imapclient.response_parser import * from modoboa.lib import parameters from modoboa.lib.connections import * from modoboa.lib.webutils import static_url +from exceptions import ImapError + +class capability(object): + """ + Simple decorator to check if the server presents the required + capability. If not, a fallback method is called instead. + + :param name: the capability name (upper case) + :param fallback_method: a method's name + """ + def __init__(self, name, fallback_method): + self.name = name + self.fallback_method = fallback_method + + def __call__(self, method): + @wraps(method) + def wrapped_func(cls, *args, **kwargs): + if self.name in cls.m.capabilities: + return method(cls, *args, **kwargs) + return getattr(cls, self.fallback_method)(*args, **kwargs) + + return wrapped_func + class IMAPconnector(object): __metaclass__ = ConnectionsManager + list_base_pattern = r'\((?P.*?)\) "(?P.*)" "(?P[^"]*)"' + list_response_pattern = re.compile(list_base_pattern) + listextended_response_pattern = \ + re.compile(list_base_pattern + r'\s*(?P.*)') + def __init__(self, user=None, password=None): self.criterions = [] self.address = parameters.get_admin("IMAP_SERVER") @@ -20,6 +50,39 @@ class IMAPconnector(object): if not status: raise Exception(msg) + def _cmd(self, name, *args): + """IMAP command wrapper + + To simplify errors handling, this wrapper calls the + appropriate method (``uid`` or FIXME) and then check the + return code. If an error has occured, an ``ImapError`` + exception is raised. + + For specific commands commands (FETCH, ...), the result is + parsed using the IMAPclient module before being returned. + + :param name: the command's name + :return: the command's result + """ + if name in ['FETCH', 'SORT', 'STORE']: + try: + typ, data = self.m.uid(name, *args, **kwargs) + except imaplib.IMAP4.error, e: + raise ImapError(e) + if typ == "NO": + raise ImapError(data) + return parse_fetch_response(data) + + try: + typ, data = self.m._simple_command(name, *args) + except imaplib.IMAP4.error, e: + raise ImapError(e) + if typ == "NO": + raise ImapError(data) + if not name in self.m.untagged_responses: + return None + return self.m.untagged_responses.pop(name) + def refresh(self, user, password): """Check if current connection needs a refresh @@ -113,12 +176,11 @@ class IMAPconnector(object): sdescr["class"] = "subfolders" return True - def _listfolders(self, topfolder='INBOX', md_folders=[]): - list_response_pattern = re.compile(r'\((?P.*?)\) "(?P.*)" (?P.*)') + def _listfolders_simple(self, topmailbox='INBOX', md_mailboxes=[]): (status, data) = self.m.list() result = [] for mb in data: - flags, delimiter, name = list_response_pattern.match(mb).groups() + flags, delimiter, name = self.list_response_pattern.match(mb).groups() name = name.strip('"').decode("imap4-utf-7") if re.search("\%s" % delimiter, name): parts = name.split(".") @@ -131,7 +193,7 @@ class IMAPconnector(object): continue present = False descr = {"name" : name} - for mdf in md_folders: + for mdf in md_mailboxes: if mdf["name"] == name: present = True break @@ -140,23 +202,34 @@ class IMAPconnector(object): from operator import itemgetter return sorted(result, key=itemgetter("name")) + @capability('LIST-EXTENDED', '_listfolders_simple') + def _listfolders(self, topmailbox='', md_mailboxes=[]): + resp = self._cmd("LSUB", topmailbox, "%", "RETURN", "(CHILDREN)") + print resp + for mb in resp: + flags, delimiter, name, childinfo = \ + self.listextended_response_pattern.match(mb).groups() + flags = flags.split(' ') + print flags + return [] + def getfolders(self, user, unseen_messages=True): - md_folders = [{"name" : "INBOX", "class" : "inbox"}, - {"name" : parameters.get_user(user, "DRAFTS_FOLDER"), - "class" : "drafts"}, - {"name" : 'Junk'}, - {"name" : parameters.get_user(user, "SENT_FOLDER")}, - {"name" : parameters.get_user(user, "TRASH_FOLDER"), - "class" : "trash"}] - md_folders += self._listfolders(md_folders=md_folders) + md_mailboxes = [{"name" : "INBOX", "class" : "inbox"}, + {"name" : parameters.get_user(user, "DRAFTS_FOLDER"), + "class" : "drafts"}, + {"name" : 'Junk'}, + {"name" : parameters.get_user(user, "SENT_FOLDER")}, + {"name" : parameters.get_user(user, "TRASH_FOLDER"), + "class" : "trash"}] + md_mailboxes += self._listfolders(md_mailboxes=md_mailboxes) if unseen_messages: - for fd in md_folders: + for fd in md_mailboxes: 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 + return md_mailboxes def _add_flag(self, folder, mail_id, flag): self.m.select(self._encodefolder(folder)) diff --git a/extensions/webmail/middleware.py b/extensions/webmail/middleware.py index 528f65a99..37884dcab 100644 --- a/extensions/webmail/middleware.py +++ b/extensions/webmail/middleware.py @@ -1,11 +1,15 @@ # coding: utf-8 -from modoboa.lib.webutils import ajax_response -from modoboa.extensions.webmail.lib import WebmailError +from modoboa.lib.webutils import ajax_response, _render_error +from modoboa.extensions.webmail.exceptions import * class WebmailErrorMiddleware(object): def process_exception(self, request, exception): - if not isinstance(exception, WebmailError): + if not isinstance(exception, (WebmailError, ImapError)): return None - return ajax_response(request, status="ko", - respmsg=str(exception), norefresh=True) + json = request.GET.get("json", False) if request.method == "GET" \ + else request.POST.get("json", False) + if json: + return ajax_response(request, status="ko", + respmsg=str(exception), norefresh=True) + return _render_error(request, user_context=dict(error=str(exception))) diff --git a/extensions/webmail/static/js/webmail.js b/extensions/webmail/static/js/webmail.js index 249a5e955..c78c1065e 100644 --- a/extensions/webmail/static/js/webmail.js +++ b/extensions/webmail/static/js/webmail.js @@ -23,8 +23,12 @@ var Webmail = new Class({ this.init_menubar(); this.init_folders_buttons(); this.init_folders_browsing(); - this.init_emails_table(); + + /* FIXME: à faire uniquement pour le mode pushState */ + /*this.init_emails_table();*/ + current_anchor.register_callback("listmailbox", + this.listmailbox_callback.bind(this)); current_anchor.register_callback("viewmail", this.viewmail_callback.bind(this)); current_anchor.register_callback("compose", this.compose_callback.bind(this)); current_anchor.register_callback("reply", this.compose_callback.bind(this)); @@ -339,8 +343,10 @@ var Webmail = new Class({ /* * Callback for 'folder' action */ - folder_callback: function(resp) { - + listmailbox_callback: function(resp) { + window.removeEvents("resize"); + updatelisting(resp); + this.init_emails_table(); }, /* diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index 703e77b6c..c6a0cf673 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -12,7 +12,7 @@ from django.contrib import messages from modoboa.admin.models import Mailbox from modoboa.lib import parameters from modoboa.lib.webutils import _render, _render_error, \ - getctx, _render_to_string, ajax_response + getctx, _render_to_string, ajax_response, ajax_simple_response from modoboa.lib.emailutils import split_mailbox from modoboa.lib.email_listing import parse_search_parameters, Paginator from modoboa.admin.lib import is_not_localadmin @@ -516,7 +516,7 @@ def listmailbox(request): lst = ImapListing(request.user, request.session["password"], baseurl=mbox, folder=mbox, order="-date") - return dict(listing=lst.render(request)) + return lst.render(request) @login_required @is_not_localadmin() @@ -561,3 +561,6 @@ def newindex(request): menufunc = globals()["listmailbox_menu"] response["menu"] = menufunc("", request.session["mbox"], request.user) return _render(request, "webmail/index.html", response) + + response.update(status="ok", callback=action) + return ajax_simple_response(response) diff --git a/lib/email_listing.py b/lib/email_listing.py index f85201f11..b5c433347 100644 --- a/lib/email_listing.py +++ b/lib/email_listing.py @@ -123,29 +123,37 @@ class EmailListing(object): """) return tpl.render(Context({"table" : table.render()})) + # def render(self, request, pageid=1, **kwargs): + # listing = "" + # page = None + # if not self.empty: + # page = self.paginator.getpage(pageid) + # if not page: + # listing = _("This folder contains no messages") + # else: + # listing = self.fetch(request, page.id_start, page.id_stop) + # elapsed = kwargs.has_key("start") and time.time() - kwargs["start"] or 0 + # context = { + # "listing" : listing, + # "elapsed" : elapsed, + # "navbar" : self.render_navbar(page), + # "selection" : self.folder, + # "navparams" : self.navparams, + # "deflocation" : self.deflocation, + # "defcallback" : self.defcallback, + # "reset_wm_url" : self.reset_wm_url + # } + # for k, v in self.extravars.iteritems(): + # context[k] = v + # return _render(request, self.tpl, context) + def render(self, request, pageid=1, **kwargs): - listing = "" - page = None - if not self.empty: - page = self.paginator.getpage(pageid) - if not page: - listing = _("This folder contains no messages") - else: - listing = self.fetch(request, page.id_start, page.id_stop) - elapsed = kwargs.has_key("start") and time.time() - kwargs["start"] or 0 - context = { - "listing" : listing, - "elapsed" : elapsed, - "navbar" : self.render_navbar(page), - "selection" : self.folder, - "navparams" : self.navparams, - "deflocation" : self.deflocation, - "defcallback" : self.defcallback, - "reset_wm_url" : self.reset_wm_url - } - for k, v in self.extravars.iteritems(): - context[k] = v - return _render(request, self.tpl, context) + page = self.paginator.getpage(pageid) + if not page: + listing = _("This folder contains no message") + else: + listing = self.fetch(request, page.id_start, page.id_stop) + return dict(listing=listing, navbar=self.render_navbar(page)) def parse_search_parameters(request): if request.GET.has_key("pattern"): diff --git a/static/js/anchor_nav.js b/static/js/anchor_nav.js index 055896f50..7d7ae4a6f 100644 --- a/static/js/anchor_nav.js +++ b/static/js/anchor_nav.js @@ -217,6 +217,6 @@ var AnchorNavigation = new Class({ $(document.body).set("html", xhr.responseText); $(document.body).setStyle("overflow", "auto"); } - }).get(); + }).get("json=1"); } });