From 1016a41ab7057f3e2d87087ffba60f8082fd68c3 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 16 Nov 2011 12:01:59 +0100 Subject: [PATCH] wip: webmail big refactoring and performance improvment. --- extensions/stats/grapher.py | 2 +- extensions/webmail/exceptions.py | 22 + extensions/webmail/imaputils.py | 282 ++++++++++ extensions/webmail/lib.py | 283 +--------- extensions/webmail/main.py | 2 +- extensions/webmail/static/js/webmail.js | 528 ++++++++++++++++++ .../webmail/templates/webmail/index.html | 13 +- extensions/webmail/templatetags/webextras.py | 2 +- extensions/webmail/urls.py | 4 +- extensions/webmail/views.py | 64 +++ templates/common/email_listing.html | 2 +- 11 files changed, 915 insertions(+), 289 deletions(-) create mode 100644 extensions/webmail/exceptions.py create mode 100644 extensions/webmail/imaputils.py diff --git a/extensions/stats/grapher.py b/extensions/stats/grapher.py index ad52da760..2dfd5d3e7 100644 --- a/extensions/stats/grapher.py +++ b/extensions/stats/grapher.py @@ -3,7 +3,7 @@ import os import time import datetime -import rrdtool +#import rrdtool from django.utils.translation import ugettext as _ from modoboa.lib import parameters diff --git a/extensions/webmail/exceptions.py b/extensions/webmail/exceptions.py new file mode 100644 index 000000000..a5d265a8f --- /dev/null +++ b/extensions/webmail/exceptions.py @@ -0,0 +1,22 @@ +# coding: utf-8 +""" +:mod:`exceptions` --- Webmail custom exceptions +----------------------------------------------- + +""" +import re + +class WebmailError(Exception): + errorexpr = re.compile("\[([^\]]+)\]\s*([^\.]+)") + + def __init__(self, reason, ajax=False): + m = WebmailError.errorexpr.match(reason) + if m is None: + self.reason = reason + else: + self.reason = "%s: %s" % (_("Server response"), m.group(2)) + self.ajax = ajax + + def __str__(self): + return self.reason + diff --git a/extensions/webmail/imaputils.py b/extensions/webmail/imaputils.py new file mode 100644 index 000000000..43fa0b477 --- /dev/null +++ b/extensions/webmail/imaputils.py @@ -0,0 +1,282 @@ +# coding: utf-8 +""" +:mod:`imaputils` --- Extra IMAPv4 utilities +------------------------------------------- +""" +import imaplib, ssl, email +import re +from modoboa.lib import parameters +from modoboa.lib.connections import * +from modoboa.lib.webutils import static_url + +class IMAPconnector(object): + __metaclass__ = ConnectionsManager + + def __init__(self, user=None, password=None): + self.criterions = [] + self.address = parameters.get_admin("IMAP_SERVER") + self.port = int(parameters.get_admin("IMAP_PORT")) + 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): + try: + secured = parameters.get_admin("IMAP_SECURED") + if secured == "yes": + self.m = imaplib.IMAP4_SSL(self.address, self.port) + else: + self.m = imaplib.IMAP4(self.address, self.port) + except (socket.error, imaplib.IMAP4.error, ssl.SSLError), error: + return False, _("Connection to IMAP server failed, check your configuration") + try: + self.m.login(user, passwd) + except (imaplib.IMAP4.error, ssl.SSLError), error: + return False, _("Authentication failed, check your configuration") + return True, None + + def logout(self): + try: + self.m.select() + except imaplib.IMAP4.error: + pass + self.m.logout() + del self.m + self.m = None + + def messages_count(self, **kwargs): + """An enhanced version of messages_count + + With IMAP, to know how many messages a mailbox contains, we + have to make a request to the server. To avoid requests + multiplications, we sort messages in the same time. This will + be usefull for other methods. + + :param order: sorting order + :param folder: mailbox to scan + """ + if "order" in kwargs.keys() and kwargs["order"]: + sign = kwargs["order"][:1] + criterion = kwargs["order"][1:].upper() + if sign == '-': + criterion = "REVERSE %s" % criterion + else: + criterion = "REVERSE DATE" + folder = kwargs.has_key("folder") and kwargs["folder"] or None + (status, data) = self.m.select(self._encodefolder(folder)) + (status, data) = self.m.sort("(%s)" % criterion, "UTF-8", "(NOT DELETED)", + *self.criterions) + self.messages = data[0].split() + self.getquota(folder) + return len(self.messages) + + def unseen_messages(self, folder): + """Return the number of unseen messages for folder""" + self.m.select(self._encodefolder(folder), True) + status, data = self.m.search("UTF-8", "(NOT DELETED UNSEEN)") + if status != "OK": + return + return len(data[0].split()) + + def _encodefolder(self, folder): + if not folder: + return "INBOX" + return folder.encode("imap4-utf-7") + + def _parse_folder_name(self, descr, prefix, delimiter, parts): + if not len(parts): + return False + path = "%s%s%s" % (prefix, delimiter, parts[0]) + sdescr = None + for d in descr: + if d["path"] == path: + sdescr = d + break + if sdescr is None: + sdescr = {"name" : parts[0], "path" : path, "sub" : []} + descr += [sdescr] + if self._parse_folder_name(sdescr["sub"], path, delimiter, parts[1:]): + sdescr["class"] = "subfolders" + return True + + def _listfolders(self, topfolder='INBOX', md_folders=[]): + list_response_pattern = re.compile(r'\((?P.*?)\) "(?P.*)" (?P.*)') + (status, data) = self.m.list() + result = [] + for mb in data: + flags, delimiter, name = list_response_pattern.match(mb).groups() + name = name.strip('"').decode("imap4-utf-7") + if re.search("\%s" % delimiter, name): + parts = name.split(".") + if not descr.has_key("path"): + descr["path"] = parts[0] + descr["sub"] = [] + if self._parse_folder_name(descr["sub"], parts[0], delimiter, + parts[1:]): + descr["class"] = "subfolders" + continue + present = False + descr = {"name" : name} + for mdf in md_folders: + if mdf["name"] == name: + present = True + break + if not present: + result += [descr] + from operator import itemgetter + return sorted(result, key=itemgetter("name")) + + 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) + if unseen_messages: + for fd in md_folders: + key = fd.has_key("path") and "path" or "name" + count = self.unseen_messages(fd[key]) + if count == 0: + continue + fd["unseen"] = count + return md_folders + + def _add_flag(self, folder, mail_id, flag): + self.m.select(self._encodefolder(folder)) + self.m.store(mail_id, "+FLAGS", flag) + + def msg_unread(self, folder, msgset): + self.m.select(self._encodefolder(folder)) + self.m.store(msgset, "-FLAGS", r'(\Seen)') + + def msg_read(self, folder, msgset): + self._add_flag(folder, msgset, r'(\Seen)') + + def msgforwarded(self, folder, imapid): + self._add_flag(folder, imapid, '($Forwarded)') + + def msg_answered(self, folder, imapid): + self._add_flag(folder, imapid, r'(\Answered)') + + def move(self, msgset, oldfolder, newfolder): + self.m.select(self._encodefolder(oldfolder)) + status, data = self.m.copy(msgset, self._encodefolder(newfolder)) + if status == 'OK': + self.m.store(msgset, "+FLAGS", r'(\Deleted)') + + def push_mail(self, folder, msg): + now = imaplib.Time2Internaldate(time.time()) + self.m.append(self._encodefolder(folder), r'(\Seen)', now, str(msg)) + + def empty(self, folder): + self.m.select(self._encodefolder(folder)) + typ, data = self.m.search(None, 'ALL') + for num in data[0].split(): + self.m.store(num, "+FLAGS", r'(\Deleted)') + self.m.expunge() + + def compact(self, folder): + self.m.select(self._encodefolder(folder)) + self.m.expunge() + + def create_folder(self, name, parent=None): + if parent is not None: + name = "%s.%s" % (parent, name) + typ, data = self.m.create(self._encodefolder(name)) + if typ == "NO": + raise WebmailError(data[0]) + return True + + def rename_folder(self, oldname, newname): + typ, data = self.m.rename(self._encodefolder(oldname), + self._encodefolder(newname)) + if typ == "NO": + raise WebmailError(data[0], ajax=True) + return True + + def delete_folder(self, name): + typ, data = self.m.delete(self._encodefolder(name)) + if typ == "NO": + raise WebmailError(data[0]) + return True + + def getquota(self, folder): + if not "QUOTA" in self.m.capabilities: + self.quota_limit = self.quota_actual = None + return + + status, data = self.m.getquotaroot(self._encodefolder(folder)) + if status == "OK": + quotadef = data[1][0] + m = re.match("[^\s]+ \(STORAGE (\d+) (\d+)\)", quotadef) + if not m: + print "Problem while parsing quota def" + return + self.quota_limit = int(m.group(2)) + self.quota_actual = int(m.group(1)) + + def fetchpart(self, uid, folder, part): + self.m.select(self._encodefolder(folder), True) + typ, data = self.m.fetch(uid, "(BODY[%(p)s.MIME] BODY[%(p)s])" \ + % {"p" : part}) + if typ != "OK": + return None + msg = email.message_from_string(data[0][1] + data[1][1]) + return msg + + def fetch(self, start=None, stop=None, folder=None, all=False, **kwargs): + if not start and not stop: + return [] + result = [] + self.m.select(self._encodefolder(folder), True) + if start and stop: + submessages = self.messages[start - 1:stop] + range = ",".join(submessages) + else: + submessages = [start] + range = start + if not all: + query = '(FLAGS BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])' + else: + query = '(RFC822)' + typ, data = self.m.fetch(range, query) + if not folder: + folder = "INBOX" + tmpdict = {} + + for response_part in data: + if isinstance(response_part, tuple): + imapid = response_part[0].split()[0] + flags = imaplib.ParseFlags(response_part[0]) + msg = email.message_from_string(response_part[1]) + msg["imapid"] = imapid + if not "\\Seen" in flags: + msg["class"] = "unseen" + if "\\Answered" in flags: + msg["img_flags"] = static_url("pics/answered.png") + tmpdict[imapid] = msg + for id in submessages: + result += [tmpdict[id]] + return result + +def get_imapconnector(request): + imapc = IMAPconnector(user=request.user.username, + password=request.session["password"]) + return imapc diff --git a/extensions/webmail/lib.py b/extensions/webmail/lib.py index dd3e759b3..748850aa0 100644 --- a/extensions/webmail/lib.py +++ b/extensions/webmail/lib.py @@ -22,19 +22,8 @@ from modoboa.lib.connections import ConnectionsManager from modoboa.lib.email_listing import MBconnector, EmailListing from modoboa.lib.emailutils import * -class WebmailError(Exception): - errorexpr = re.compile("\[([^\]]+)\]\s*([^\.]+)") - - def __init__(self, reason, ajax=False): - m = WebmailError.errorexpr.match(reason) - if m is None: - self.reason = reason - else: - self.reason = "%s: %s" % (_("Server response"), m.group(2)) - self.ajax = ajax - - def __str__(self): - return self.reason +from imaputils import * +from exceptions import * class WMtable(tables.Table): tableid = "emails" @@ -108,274 +97,6 @@ class IMAPheader(object): def parse_subject(value, **kwargs): return decode(u2u_decode.u2u_decode(value)) -class IMAPconnector(object): - __metaclass__ = ConnectionsManager - - def __init__(self, user=None, password=None): - self.criterions = [] - self.address = parameters.get_admin("IMAP_SERVER") - self.port = int(parameters.get_admin("IMAP_PORT")) - 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: - secured = parameters.get_admin("IMAP_SECURED") - if secured == "yes": - self.m = imaplib.IMAP4_SSL(self.address, self.port) - else: - self.m = imaplib.IMAP4(self.address, self.port) - except (socket.error, imaplib.IMAP4.error, ssl.SSLError), error: - return False, _("Connection to IMAP server failed, check your configuration") - try: - self.m.login(user, passwd) - except (imaplib.IMAP4.error, ssl.SSLError), error: - return False, _("Authentication failed, check your configuration") - return True, None - - def logout(self): - try: - self.m.select() - except imaplib.IMAP4.error: - pass - self.m.logout() - del self.m - self.m = None - - def messages_count(self, **kwargs): - """An enhanced version of messages_count - - With IMAP, to know how many messages a mailbox contains, we - have to make a request to the server. To avoid requests - multiplications, we sort messages in the same time. This will - be usefull for other methods. - - :param order: sorting order - :param folder: mailbox to scan - """ - if "order" in kwargs.keys() and kwargs["order"]: - sign = kwargs["order"][:1] - criterion = kwargs["order"][1:].upper() - if sign == '-': - criterion = "REVERSE %s" % criterion - else: - criterion = "REVERSE DATE" - folder = kwargs.has_key("folder") and kwargs["folder"] or None - (status, data) = self.m.select(self._encodefolder(folder)) - (status, data) = self.m.sort("(%s)" % criterion, "UTF-8", "(NOT DELETED)", - *self.criterions) - self.messages = data[0].split() - self.getquota(folder) - return len(self.messages) - - def unseen_messages(self, folder): - """Return the number of unseen messages for folder""" - self.m.select(self._encodefolder(folder), True) - status, data = self.m.search("UTF-8", "(NOT DELETED UNSEEN)") - if status != "OK": - return - return len(data[0].split()) - - def _encodefolder(self, folder): - if not folder: - return "INBOX" - return folder.encode("imap4-utf-7") - - def _parse_folder_name(self, descr, prefix, delimiter, parts): - if not len(parts): - return False - path = "%s%s%s" % (prefix, delimiter, parts[0]) - sdescr = None - for d in descr: - if d["path"] == path: - sdescr = d - break - if sdescr is None: - sdescr = {"name" : parts[0], "path" : path, "sub" : []} - descr += [sdescr] - if self._parse_folder_name(sdescr["sub"], path, delimiter, parts[1:]): - sdescr["class"] = "subfolders" - return True - - def _listfolders(self, topfolder='INBOX', md_folders=[]): - list_response_pattern = re.compile(r'\((?P.*?)\) "(?P.*)" (?P.*)') - (status, data) = self.m.list() - result = [] - for mb in data: - flags, delimiter, name = list_response_pattern.match(mb).groups() - name = name.strip('"').decode("imap4-utf-7") - if re.search("\%s" % delimiter, name): - parts = name.split(".") - if not descr.has_key("path"): - descr["path"] = parts[0] - descr["sub"] = [] - if self._parse_folder_name(descr["sub"], parts[0], delimiter, - parts[1:]): - descr["class"] = "subfolders" - continue - present = False - descr = {"name" : name} - for mdf in md_folders: - if mdf["name"] == name: - present = True - break - if not present: - result += [descr] - from operator import itemgetter - return sorted(result, key=itemgetter("name")) - - 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) - if unseen_messages: - for fd in md_folders: - key = fd.has_key("path") and "path" or "name" - count = self.unseen_messages(fd[key]) - if count == 0: - continue - fd["unseen"] = count - return md_folders - - def _add_flag(self, folder, mail_id, flag): - self.m.select(self._encodefolder(folder)) - self.m.store(mail_id, "+FLAGS", flag) - - def msg_unread(self, folder, msgset): - self.m.select(self._encodefolder(folder)) - self.m.store(msgset, "-FLAGS", r'(\Seen)') - - def msg_read(self, folder, msgset): - self._add_flag(folder, msgset, r'(\Seen)') - - def msgforwarded(self, folder, imapid): - self._add_flag(folder, imapid, '($Forwarded)') - - def msg_answered(self, folder, imapid): - self._add_flag(folder, imapid, r'(\Answered)') - - def move(self, msgset, oldfolder, newfolder): - self.m.select(self._encodefolder(oldfolder)) - status, data = self.m.copy(msgset, self._encodefolder(newfolder)) - if status == 'OK': - self.m.store(msgset, "+FLAGS", r'(\Deleted)') - - def push_mail(self, folder, msg): - now = imaplib.Time2Internaldate(time.time()) - self.m.append(self._encodefolder(folder), r'(\Seen)', now, str(msg)) - - def empty(self, folder): - self.m.select(self._encodefolder(folder)) - typ, data = self.m.search(None, 'ALL') - for num in data[0].split(): - self.m.store(num, "+FLAGS", r'(\Deleted)') - self.m.expunge() - - def compact(self, folder): - self.m.select(self._encodefolder(folder)) - self.m.expunge() - - def create_folder(self, name, parent=None): - if parent is not None: - name = "%s.%s" % (parent, name) - typ, data = self.m.create(self._encodefolder(name)) - if typ == "NO": - raise WebmailError(data[0]) - return True - - def rename_folder(self, oldname, newname): - typ, data = self.m.rename(self._encodefolder(oldname), - self._encodefolder(newname)) - if typ == "NO": - raise WebmailError(data[0], ajax=True) - return True - - def delete_folder(self, name): - typ, data = self.m.delete(self._encodefolder(name)) - if typ == "NO": - raise WebmailError(data[0]) - return True - - def getquota(self, folder): - if not "QUOTA" in self.m.capabilities: - self.quota_limit = self.quota_actual = None - return - - status, data = self.m.getquotaroot(self._encodefolder(folder)) - if status == "OK": - quotadef = data[1][0] - m = re.match("[^\s]+ \(STORAGE (\d+) (\d+)\)", quotadef) - if not m: - print "Problem while parsing quota def" - return - self.quota_limit = int(m.group(2)) - self.quota_actual = int(m.group(1)) - - def fetchpart(self, uid, folder, part): - self.m.select(self._encodefolder(folder), True) - typ, data = self.m.fetch(uid, "(BODY[%(p)s.MIME] BODY[%(p)s])" \ - % {"p" : part}) - if typ != "OK": - return None - msg = email.message_from_string(data[0][1] + data[1][1]) - return msg - - def fetch(self, start=None, stop=None, folder=None, all=False, **kwargs): - if not start and not stop: - return [] - result = [] - self.m.select(self._encodefolder(folder), True) - if start and stop: - submessages = self.messages[start - 1:stop] - range = ",".join(submessages) - else: - submessages = [start] - range = start - if not all: - query = '(FLAGS BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])' - else: - query = '(RFC822)' - typ, data = self.m.fetch(range, query) - if not folder: - folder = "INBOX" - tmpdict = {} - - for response_part in data: - if isinstance(response_part, tuple): - imapid = response_part[0].split()[0] - flags = imaplib.ParseFlags(response_part[0]) - msg = email.message_from_string(response_part[1]) - msg["imapid"] = imapid - if not "\\Seen" in flags: - msg["class"] = "unseen" - if "\\Answered" in flags: - msg["img_flags"] = static_url("pics/answered.png") - tmpdict[imapid] = msg - for id in submessages: - result += [tmpdict[id]] - return result - class ImapListing(EmailListing): tpl = "webmail/index.html" tbltype = WMtable diff --git a/extensions/webmail/main.py b/extensions/webmail/main.py index 9caaf085c..1f59baa52 100644 --- a/extensions/webmail/main.py +++ b/extensions/webmail/main.py @@ -89,7 +89,7 @@ def menu(**kwargs): return [ {"name" : "webmail", "label" : _("Webmail"), - "url" : reverse(views.index), + "url" : reverse(views.newindex), "img" : static_url("pics/webmail.png")} ] diff --git a/extensions/webmail/static/js/webmail.js b/extensions/webmail/static/js/webmail.js index 30a56a111..249a5e955 100644 --- a/extensions/webmail/static/js/webmail.js +++ b/extensions/webmail/static/js/webmail.js @@ -1,3 +1,531 @@ +var Webmail = new Class({ + Implements: [Options], + + /* + * Default values for options + */ + defaults: { + refreshRate: 0, + modify_url: "", + delete_url: "", + move_url: "" + }, + + /* + * Constructor + */ + initialize: function(options) { + this.setOptions(options); + + this.rtimer = null; + this.editorid = "id_body"; + + this.init_menubar(); + this.init_folders_buttons(); + this.init_folders_browsing(); + this.init_emails_table(); + + 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)); + current_anchor.register_callback("replyall", this.compose_callback.bind(this)); + current_anchor.register_callback("forward", this.compose_callback.bind(this)); + }, + + /* + * Menubar initialization + */ + init_menubar: function() { + searchbox_init(); + $$("a[name=mark-read]").addEvent("click", this.send_mark_request.bind(this)); + $$("a[name=mark-unread]").addEvent("click", this.send_mark_request.bind(this)); + $$("a[name=fdaction]").addEvent("click", function(evt) { + this.simple_request(evt.target.get("href"), {}); + evt.stop(); + }.bind(this)); + }, + + send_mark_request: function(evt) { + evt.stop(); + if (!this.emails_table._selectedRows.length) { + return; + } + var ids = new Array(); + + this.emails_table._selectedRows.each(function(item, index) { + ids.include(item.get("id")); + }); + this.simple_request(evt.target.get("href"), { + ids : ids.join() + }); + }, + + /* + * Initializes the misc. buttons available to manipulate folders. + */ + init_folders_buttons: function() { + $$("li[class*=clickable]").removeEvents(); + + $$("a[name=loadfolder]").addEvent("click", this.folder_loader.bind(this)); + + SqueezeBox.assign($("folders").getElements('a[class=boxed]'), { + parse: 'rel' + }); + + var fdbuttons = $$("img[class*=footer]"); + var fdmenu = new FdMenu({ + modify_url: this.options.modify_url, + delete_url: this.options.delete_url + }); + + fdbuttons.addEvents({ + click: function(evt) { + var parent = this.getParent(); + var fdname = parent.getFirst("a[name=loadfolder]").get("href"); + + fdmenu.toggle(this, fdname); + evt.stop(); + } + }); + }, + + /* + * Enables folders opening/closing + * + * Available for folders with subfolders. + */ + init_folders_browsing: function() { + $$("div[class*=clickbox]").addEvent("click", function(evt) { + var parent = this.getParent(); + var ul = parent.getFirst("ul[name=" + parent.get("name") + "]"); + + if (!$defined(ul)) { + return; + } + if (ul.hasClass("hidden")) { + this.removeClass("collapsed").addClass("expanded"); + ul.removeClass("hidden").addClass("visible"); + } else { + this.removeClass("expanded").addClass("collapsed"); + ul.removeClass("visible").addClass("hidden"); + } + evt.stop(); + }); + }, + + /* + * Select a particular folder + */ + select_folder: function(obj) { + $$("a[name=loadfolder]").each(function(item) { + item.getParent().removeClass("selected"); + item.getParent().addClass("droppable"); + }); + obj.getParent().removeClass("droppable"); + obj.getParent().addClass("selected"); + + $$(obj.getParents("ul[class*=hidden]")).each(function(item) { + item.removeClass("hidden"); + item.addClass("visible"); + $$("li[name=" + item.get("name") + "]").each(function(item) { + item.setStyle( + "background", + "url('" + static_url("pics/folder-open.png") + "') no-repeat" + ); + }); + }); + }, + + /* + * Automatic folder listing refresh + * + * Called at a regular frequency to make an asynchronous request that + * retrieves a folder's content (ie. mails) + */ + refresh_folder: function() { + var query = current_anchor.serialized.substring(1); + + clearInterval(this.rtimer); + infobox.show(gettext("Reloading..."), { + profile : "gray", + spinner : true + }); + new Request.JSON({url: query, noCache : true, onSuccess: function(resp) { + var callback = ($defined(resp.callback)) ? resp.callback : "default"; + + current_anchor.get_callback(callback)(resp); + infobox.info(gettext("Done")); + infobox.hide(1); + }}).get(); + }, + + init_automatic_refresh: function() { + this.rtimer = this.refresh_folder + .periodical(this.options.refreshRate); + }, + + /* + * Emails table initialization. + */ + init_emails_table: function() { + this.emails_table = new HtmlTable($("emails"), { + selectable: true, + shiftForMultiSelect: true + }).addEvents({ + rowFocus: function(tr) { + clearInterval(this.rtimer); + }.bind(this), + rowUnfocus: function(tr) { + if (this.emails_table._selectedRows.length == 0) { + this.init_automatic_refresh(); + } + }.bind(this) + }); + + /* The IE way for disabling default selection */ + /* Actually it doesn't work :p */ + $$("tbody>tr").set("unselectable", "on"); + + if ($("toggleselect")) { + $("toggleselect").addEvent("click", function(evt) { + if (evt.target.checked) { + this.emails_table.selectAll(); + } else { + this.emails_table.selectNone(); + } + }.bind(this)); + } + + init_sortable_columns(); + update_sortable_column(); + + $$("td[class*=draggable]").each(function(item) { + new Drag.Move(item, { + droppables: ".droppable" + }).addEvents({ + beforeStart: this.bstart_dragging, + start: this.start_dragging, + enter: this.enter_droppable, + leave: this.leave_droppable, + drop: this.drop_element, + cancel: this.cancel_dragging + }); + }); + + $$("tbody>tr").addEvent("dblclick", viewmail); + }, + + /* + * Drag & Drop feature + */ + + /* + * 'before start' event callback + */ + bstart_dragging: function(item) { + var block = new Element("div", {id: "draggeditems"}) + .setStyles({"position" : "absolute", + "opacity" : 0.8, + "width" : "200px", + "padding" : "5px", + "text-align" : "center", + "height" : "25px", + "border" : "1px solid #333", + "background" : "white", + display: "none", + "z-index" : 3, + top : this.element.getCoordinates()['top'], + left : this.element.getCoordinates()['left']}) + .inject(document.body); + block.store("parent", this.element); + this.element = block; + clearInterval(rtimer); + }, + + start_dragging: function(item) { + var tr = this.element.retrieve("parent").getParent(); + + if (!emails_table.isSelected(tr)) { + emails_table.selectNone(); + emails_table.selectRow(tr); + } + item.set("html", "Moving " + emails_table._selectedRows.length + " messages"); + this.element.setStyle("display", "block"); + }, + + enter_droppable: function(item, droppable) { + droppable.addClass("dragging"); + }, + + leave_droppable: function(item, droppable) { + droppable.removeClass("dragging"); + }, + + restore_parent: function(drag, item) { + var parent = item.retrieve("parent"); + + drag.element = parent; + this.init_automatic_refresh(); + }, + + drop_element: function(item, droppable, event) { + this.element.dispose(); + delete(this.element); + if (!droppable) { + restore_parent(this, item); + return; + } + droppable.removeClass("dragging"); + dest = gethref(droppable.getFirst("a")); + msgset = new Array(); + this.emails_table._selectedRows.each(function(item, index) { + msgset.include(item.get("id")); + }); + this.simple_request(this.options.move_url, { + msgset : msgset.join(), + to : dest + }); + }, + + cancel_dragging: function(item) { + this.element.dispose(); + delete(this.element); + restore_parent(this, item); + }, + + /* + * Bottom left quota bar initialisation. + */ + init_quota_bar: function() { + this.quota_pb = new ProgressBar({ + container: $("quotabox"), + startPercentage: 0, + boxID: "box", + percentageID: "perc", + displayText: true, + displayID: "quotatext" + }); + }, + + /* + * + */ + page_update: function(response) { + + }, + + /* + * Webmail actions start here + */ + + /* + * Onclick callback used to load the content of a particular + * folder. (activated when clicking on a folder's name) + */ + _folder_loader: function(event, obj) { + event.stop(); + if (!$defined(obj)) { + obj = $(event.target); + } + current_anchor.parse_string(gethref(obj), true).setparams(navparams); + current_anchor.update(); + }, + + folder_loader: function(event) { + this.select_folder(event.target); + this._folder_loader(event, event.target); + }, + + /* + * Callback for 'folder' action + */ + folder_callback: function(resp) { + + }, + + /* + * Callback of the 'viewmail' action + */ + viewmail_callback: function(resp) { + wm_updatelisting(resp, "hidden"); + $$("a[name=back]").addEvent("click", this._folder_loader); + $$("a[name=reply]").addEvent("click", this.reply_loader); + $$("a[name=replyall]").addEvent("click", this.replyall_loader); + $$("a[name=forward]").addEvent("click", this.forward_loader); + $$("a[name=delete]").addEvent("click", function(event) { + var lnk = event.target; + event.stop(); + this.simple_request(lnk.get("href")); + }.bind(this)); + $$("a[name=activate_links]").addEvent("click", function(evt) { + evt.stop(); + current_anchor.setparam("links", "1").update(); + }); + $$("a[name=disable_links]").addEvent("click", function(evt) { + evt.stop(); + current_anchor.setparam("links", "0").update(); + }); + setDivHeight("mailcontent", 5, 0); + }, + + /* + * Loader of the 'compose' action (called when the associated button + * is clicked) + */ + compose_loader: function(event) { + event.stop(); + current_anchor.baseurl("compose").update(); + }, + + /* + * Loader of the 'reply' action (called when the associated button + * is clicked). + */ + reply_loader: function(event) { + event.stop(); + location.hash += event.target.get("href"); + }, + + /* + * Loader of the 'replyall' action (called when the associated button + * is clicked). + */ + replyall_loader: function(event) { + event.stop(); + location.hash += event.target.get("href") + "?all=1"; + }, + + /* + * Loader of the 'forward' action (called when the associated button + * is clicked). + */ + forward_loader: function(event) { + event.stop(); + location.hash += event.target.get("href"); + }, + + /* + * Resize the current editor on page resizing. + */ + resize_window_callback: function(event) { + CKEDITOR.instances[this.editorid] + .resize("100%", $("body_container").getSize().y); + }, + + /* + * Callback of the 'compose' action. + * + * It is also shared with other similar actions : reply, forward. + */ + compose_callback: function(resp) { + wm_updatelisting(resp); + + window.addEvent("resize", this.resize_window_callback.bind(this)); + + var editormode = resp.editor; + + if (editormode == "html") { + var instance = CKEDITOR.instances[editorid]; + if (instance) { + CKEDITOR.remove(instance); + } + CKEDITOR.replace(editorid, { + customConfig: static_url("js/editor_config.js") + }); + CKEDITOR.on("instanceReady", function(evt) { + this.resize_window_callback(); + }.bind(this)); + } + if (resp.id) { + current_anchor.setparam("id", resp.id).update(0, 1); + } + $("attachments").addEvent("click", function(evt) { + SqueezeBox.open(this.get("name"), { + handler: "iframe", + size: {x: 350, y: 400}, + closeBtn: false + }); + }); + + $$("a[name=back]") + .removeEvents("click") + .addEvent("click", this._folder_loader); + $$("a[name=sendmail]") + .store("editormode", editormode) + .removeEvents("click") + .addEvent("click", this.sendmail_callback.bind(this)); + }, + + /* + * Callback of the 'sendmail' action. + */ + sendmail_callback: function(evt) { + evt.stop(); + infobox.show(gettext("Sending..."), { + profile : "gray", + spinner : true + }); + disable_link(this); + $("composemail").set("send", { + onSuccess: function(resp) { + resp = JSON.decode(resp); + if (resp.status == "ko") { + current_anchor.get_callback("compose")(resp); + if ($defined(resp.respmsg)) { + infobox.error(resp.respmsg); + } + enable_link(this, sendmail_callback); + return; + } + current_anchor.parse_string(resp.url, true).setparams(navparams); + current_anchor.update(); + this.eliminate("editormode"); + }.bind(this) + }); + if (this.retrieve("editormode") == "html") { + CKEDITOR.instances[editorid].updateElement(); + } + $("composemail").send(); + }, + + + /* + * Simple AJAX request + * + * Initialize the infobox with a loading message and wait for a JSON + * encoded answer. The infobox is cleared by the request callback + * function. + */ + simple_request: function(url, params) { + infobox.show(gettext("Waiting..."), { + profile: "gray", + spinner: true + }); + new Request.JSON({ + url : url, + onSuccess : function(response) { + if (response.status == "ok") { + if (!$defined(response.next)) { + current_anchor.get_callback("folder")(response); + } + infobox.info(gettext("Done")); + if ($defined(response.next)) { + (function(response) { + current_anchor.parse_string(response.next).update(); + select_folder($$("a[href=" + current_anchor.getbaseurl() + "]")); + }).delay(500, this, response); + } else { + infobox.hide(1); + } + } else { + infobox.error(response.respmsg); + } + } + }).get(params); + } +}); + + /* * Globals */ diff --git a/extensions/webmail/templates/webmail/index.html b/extensions/webmail/templates/webmail/index.html index aadd73756..1e86ea09c 100644 --- a/extensions/webmail/templates/webmail/index.html +++ b/extensions/webmail/templates/webmail/index.html @@ -49,7 +49,7 @@ function wm_updatelisting(resp) { {% endblock %} {% block js %} - quota_pb = new ProgressBar({ + /*quota_pb = new ProgressBar({ container: $("quotabox"), startPercentage: 0, boxID: "box", @@ -69,6 +69,13 @@ function wm_updatelisting(resp) { clearInterval(rtimer); rtimer = null; } + });*/ + + new Webmail({ + refreshRate: {{ refreshrate }}, + modify_url: "{% url extensions.webmail.views.editfolder %}", + delete_url: "{% url extensions.webmail.views.delfolder %}", + move_url: "{% url extensions.webmail.views.move as moveurl %}" }); current_anchor.register_callback("folder", function(resp) { @@ -211,9 +218,9 @@ function wm_updatelisting(resp) { {% endblock %} {% block middletitle %}{% trans "Webmail" %}{% endblock %} {% block leftcol %} -
-
+
{{ mboxes }}
{% endblock %} +{% block menu %}{{ menu }}{% endblock %} {% block footer %}
{% trans "Mailbox quota" %}
diff --git a/extensions/webmail/templatetags/webextras.py b/extensions/webmail/templatetags/webextras.py index 7c8dd1fd4..13d3a4c0e 100644 --- a/extensions/webmail/templatetags/webextras.py +++ b/extensions/webmail/templatetags/webextras.py @@ -69,7 +69,7 @@ def compose_menu(selection, backurl, user): "user" : user}) @register.simple_tag -def listing_menu(selection, folder, user): +def listmailbox_menu(selection, folder, user): entries = [ {"name" : "compose", "url" : reverse(webmail.views.compose), diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py index e950fd222..f083f7612 100644 --- a/extensions/webmail/urls.py +++ b/extensions/webmail/urls.py @@ -6,7 +6,9 @@ from django.conf.urls.defaults import * # "attachments"), the associated folder will not be accessible. # urlpatterns = patterns('modoboa.extensions.webmail.views', - (r'^$', "index"), + #(r'^$', "index"), + (r'^$', "newindex"), + (r'^compose/$', "compose"), (r'^move/$', "move"), (r'^mark/(?P.+)/$', "mark"), diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index 1a76a3a9e..703e77b6c 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -497,3 +497,67 @@ def delattachment(request): if error is None: error = _("Unknown attachment") return ajax_response(request, "ko", respmsg=error) + +# +# NEW CODE STARTS HERE +# +def render_mboxes_list(request, imapc): + return _render_to_string(request, "webmail/folders.html", { + "titlebar" : True, + "selected" : request.session["mbox"], + "folders" : imapc.getfolders(request.user), + "withmenu" : True, + "withunseen" : True + }) + +def listmailbox(request): + mbox = request.GET.get("mbox", "INBOX") + request.session["mbox"] = mbox + lst = ImapListing(request.user, request.session["password"], + baseurl=mbox, folder=mbox, order="-date") + + return dict(listing=lst.render(request)) + +@login_required +@is_not_localadmin() +def newindex(request): + """Webmail actions handler + + Problèmes liés à la navigation 'anchor based' + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Lors d'un rafraichissemt complet, une première requête est envoyée + vers /webmail/. On ne connait pas encore l'action qui va être + demandée mais on peut déjà envoyer des informations indépendantes + (comme les dossiers, le quota). + + Si on se contente de cela, l'affichage donnera un aspect décomposé + qui n'est pas très séduisant (à cause de la latence notamment). Il + faudrait pouvoir envoyer le menu par la même occasion, le souci + étant de savoir lequel... + + """ + action = request.GET.get("action", None) + json = request.GET.get("json", False) + + if action is not None: + try: + response = globals()[action](request) + except KeyError: + raise WebmailError(_("Undefined action")) + else: + if json: + raise WebmailError(_("Bad request")) + response = dict(deflocation="?action=listmailbox", + defcallback="wm_updatelisting") + + if not json: + imapc = get_imapconnector(request) + response["refreshrate"] = parameters.get_user(request.user, "REFRESH_INTERVAL") + response["mboxes"] = render_mboxes_list(request, imapc) + try: + menufunc = globals()["%s_menu" % action] + except KeyError: + menufunc = globals()["listmailbox_menu"] + response["menu"] = menufunc("", request.session["mbox"], request.user) + return _render(request, "webmail/index.html", response) diff --git a/templates/common/email_listing.html b/templates/common/email_listing.html index 54fce2f75..ed6d19bdc 100644 --- a/templates/common/email_listing.html +++ b/templates/common/email_listing.html @@ -56,7 +56,7 @@ function update_sortable_column() { window.addEvent("domready", function() { navparams = new Hash({ - {% join navparams %} + {% if navparams %}{% join navparams %}{% endif %} }); {% block js %}{% endblock %} current_anchor.set_default_callback({{ defcallback }});