wip: Webmail big refactoring and performance improvment

Using LSUB and LIST-EXTENDED.
This commit is contained in:
Antoine Nguyen
2011-11-16 18:44:10 +01:00
parent 1016a41ab7
commit 487266ecb5
7 changed files with 147 additions and 47 deletions
+6
View File
@@ -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)
+87 -14
View File
@@ -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<flags>.*?)\) "(?P<delimiter>.*)" "(?P<name>[^"]*)"'
list_response_pattern = re.compile(list_base_pattern)
listextended_response_pattern = \
re.compile(list_base_pattern + r'\s*(?P<childinfo>.*)')
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<flags>.*?)\) "(?P<delimiter>.*)" (?P<name>.*)')
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))
+9 -5
View File
@@ -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)))
+9 -3
View File
@@ -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();
},
/*
+5 -2
View File
@@ -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)
+30 -22
View File
@@ -123,29 +123,37 @@ class EmailListing(object):
</form>""")
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"):
+1 -1
View File
@@ -217,6 +217,6 @@ var AnchorNavigation = new Class({
$(document.body).set("html", xhr.responseText);
$(document.body).setStyle("overflow", "auto");
}
}).get();
}).get("json=1");
}
});