From 1016a41ab7057f3e2d87087ffba60f8082fd68c3 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 16 Nov 2011 12:01:59 +0100 Subject: [PATCH 01/13] 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 }}); From 487266ecb5343854ab48a259196774359cf0b357 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 16 Nov 2011 18:44:10 +0100 Subject: [PATCH 02/13] 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"); } }); From f04862b28f9c14ba3a21f65b81240411e99cdb98 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 16 Nov 2011 22:34:44 +0100 Subject: [PATCH 03/13] wip: Webmail big refactoring and performance improvment Better IMAP commands. --- extensions/webmail/imaputils.py | 106 ++++++++++++------- extensions/webmail/templatetags/webextras.py | 2 +- static/js/anchor_nav.js | 4 + 3 files changed, 71 insertions(+), 41 deletions(-) diff --git a/extensions/webmail/imaputils.py b/extensions/webmail/imaputils.py index 98c7a56e8..49dc4f093 100644 --- a/extensions/webmail/imaputils.py +++ b/extensions/webmail/imaputils.py @@ -6,12 +6,14 @@ import imaplib, ssl, email import re from functools import wraps -from imapclient.response_parser import * +from imapclient.response_parser import parse_fetch_response from modoboa.lib import parameters from modoboa.lib.connections import * from modoboa.lib.webutils import static_url from exceptions import ImapError +imaplib.Debug = 4 + class capability(object): """ Simple decorator to check if the server presents the required @@ -66,12 +68,14 @@ class IMAPconnector(object): """ if name in ['FETCH', 'SORT', 'STORE']: try: - typ, data = self.m.uid(name, *args, **kwargs) + typ, data = self.m.uid(name, *args) except imaplib.IMAP4.error, e: raise ImapError(e) if typ == "NO": raise ImapError(data) - return parse_fetch_response(data) + if name == 'FETCH': + return parse_fetch_response(data) + return data try: typ, data = self.m._simple_command(name, *args) @@ -140,20 +144,39 @@ class IMAPconnector(object): 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.select_mailbox(folder, False) + data = self._cmd("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 select_mailbox(self, name, readonly=True): + """Issue a SELECT/EXAMINE command to the server + + The given name is first 'imap-utf7' encoded. + + :param name: mailbox's name + :param readonly: + """ + name = name.encode("imap4-utf-7") + if readonly: + self._cmd("EXAMINE", name) + else: + self._cmd("SELECT", name) + + def unseen_messages(self, mailbox): + """Return the number of unseen messages + + :param mailbox: the mailbox's name + :return: an integer + """ + # self.select_mailbox(mailbox) + # data = self._cmd("SEARCH", "UTF-8", "(NOT DELETED UNSEEN)") + # return len(data[0].split()) + data = self._cmd("STATUS", mailbox.encode("imap4-utf-7"), "(UNSEEN)") + print data + return 0 def _encodefolder(self, folder): if not folder: @@ -205,13 +228,27 @@ class IMAPconnector(object): @capability('LIST-EXTENDED', '_listfolders_simple') def _listfolders(self, topmailbox='', md_mailboxes=[]): resp = self._cmd("LSUB", topmailbox, "%", "RETURN", "(CHILDREN)") - print resp + result = [] for mb in resp: flags, delimiter, name, childinfo = \ self.listextended_response_pattern.match(mb).groups() flags = flags.split(' ') - print flags - return [] + name = name.decode("imap4-utf-7") + present = False + for mdm in md_mailboxes: + if mdm["name"] == name: + present = True + break + if present: + continue + descr = dict(name=name) + if r'\HasChildren' in flags: + descr["path"] = name + descr["sub"] = [] + descr["class"] = "subfolders" + result += [descr] + from operator import itemgetter + return sorted(result, key=itemgetter("name")) def getfolders(self, user, unseen_messages=True): md_mailboxes = [{"name" : "INBOX", "class" : "inbox"}, @@ -314,11 +351,8 @@ class IMAPconnector(object): 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) + def fetch(self, start, stop=None, folder=None, all=False, **kwargs): + self.select_mailbox(folder, False) if start and stop: submessages = self.messages[start - 1:stop] range = ",".join(submessages) @@ -326,27 +360,19 @@ class IMAPconnector(object): submessages = [start] range = start if not all: - query = '(FLAGS BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])' + query = '(FLAGS BODYSTRUCTURE BODY.PEEK[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]] + data = self._cmd("FETCH", range, query) + result = [] + for uid in submessages: + msg = email.message_from_string(data[int(uid)]['BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)]']) + msg['imapid'] = uid + if not r'\Seen' in data[int(uid)]['FLAGS']: + msg['class'] = 'unseen' + if r'\Answered' in data[int(uid)]['FLAGS']: + msg['img_flags'] = static_url('pics/answered.png') + result += [msg] return result def get_imapconnector(request): diff --git a/extensions/webmail/templatetags/webextras.py b/extensions/webmail/templatetags/webextras.py index 13d3a4c0e..74b9bdbd4 100644 --- a/extensions/webmail/templatetags/webextras.py +++ b/extensions/webmail/templatetags/webextras.py @@ -127,7 +127,7 @@ def print_folders(folders, selected=None, withunseen=False, withmenu=False): if selected == name: cssclass += " selected" result += "
  • \n" % (name, cssclass) - if fd.has_key("sub") and len(fd["sub"]): + if fd.has_key("sub"): if selected is not None and selected != name and selected.count(name): ul_state = "visible" div_state = "expanded" diff --git a/static/js/anchor_nav.js b/static/js/anchor_nav.js index 7d7ae4a6f..732f09b4a 100644 --- a/static/js/anchor_nav.js +++ b/static/js/anchor_nav.js @@ -207,6 +207,10 @@ var AnchorNavigation = new Class({ url: query, noCache : true, onSuccess: function(resp) { + if (resp.status == "ko") { + infobox.error(resp.respmsg); + return; + } var callback = ($defined(resp.callback)) ? resp.callback : "default"; this.callbacks[callback](resp); infobox.info(gettext("Done")); From 5730d669790fc23f76b73c6c00d1f898e175edd5 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 17 Nov 2011 15:34:59 +0100 Subject: [PATCH 04/13] wip: webmail big refactoring and performance improvment *On demand* mailboxes loading. --- extensions/webmail/imaputils.py | 31 ++++--- extensions/webmail/static/js/webmail.js | 86 ++++++++++++++++--- .../webmail/templates/webmail/index.html | 3 +- extensions/webmail/urls.py | 1 + extensions/webmail/views.py | 7 ++ 5 files changed, 103 insertions(+), 25 deletions(-) diff --git a/extensions/webmail/imaputils.py b/extensions/webmail/imaputils.py index 49dc4f093..dd6ba51c0 100644 --- a/extensions/webmail/imaputils.py +++ b/extensions/webmail/imaputils.py @@ -43,6 +43,7 @@ class IMAPconnector(object): list_response_pattern = re.compile(list_base_pattern) listextended_response_pattern = \ re.compile(list_base_pattern + r'\s*(?P.*)') + unseen_pattern = re.compile(r'[^\(]+\(UNSEEN (\d+)\)') def __init__(self, user=None, password=None): self.criterions = [] @@ -175,8 +176,10 @@ class IMAPconnector(object): # data = self._cmd("SEARCH", "UTF-8", "(NOT DELETED UNSEEN)") # return len(data[0].split()) data = self._cmd("STATUS", mailbox.encode("imap4-utf-7"), "(UNSEEN)") - print data - return 0 + m = self.unseen_pattern.match(data[0]) + if m is None: + return 0 + return int(m.group(1)) def _encodefolder(self, folder): if not folder: @@ -227,7 +230,8 @@ class IMAPconnector(object): @capability('LIST-EXTENDED', '_listfolders_simple') def _listfolders(self, topmailbox='', md_mailboxes=[]): - resp = self._cmd("LSUB", topmailbox, "%", "RETURN", "(CHILDREN)") + pattern = ("%s.%%" % topmailbox) if len(topmailbox) else "%" + resp = self._cmd("LSUB", "", pattern, "RETURN", "(CHILDREN)") result = [] for mb in resp: flags, delimiter, name, childinfo = \ @@ -250,15 +254,18 @@ class IMAPconnector(object): from operator import itemgetter return sorted(result, key=itemgetter("name")) - def getfolders(self, user, unseen_messages=True): - 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) + def getfolders(self, user, topmailbox='', unseen_messages=True): + if len(topmailbox): + md_mailboxes = [] + else: + 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(topmailbox, md_mailboxes) if unseen_messages: for fd in md_mailboxes: key = fd.has_key("path") and "path" or "name" diff --git a/extensions/webmail/static/js/webmail.js b/extensions/webmail/static/js/webmail.js index c78c1065e..3880b610f 100644 --- a/extensions/webmail/static/js/webmail.js +++ b/extensions/webmail/static/js/webmail.js @@ -8,7 +8,8 @@ var Webmail = new Class({ refreshRate: 0, modify_url: "", delete_url: "", - move_url: "" + move_url: "", + submboxes_url: "" }, /* @@ -23,7 +24,7 @@ var Webmail = new Class({ this.init_menubar(); this.init_folders_buttons(); this.init_folders_browsing(); - + /* FIXME: à faire uniquement pour le mode pushState */ /*this.init_emails_table();*/ @@ -93,6 +94,71 @@ var Webmail = new Class({ }); }, + /* + * Open/Close a mailbox with children + */ + toggle_mbox_state: function(div, ul) { + if (ul.hasClass("hidden")) { + div.removeClass("collapsed").addClass("expanded"); + ul.removeClass("hidden").addClass("visible"); + } else { + div.removeClass("expanded").addClass("collapsed"); + ul.removeClass("visible").addClass("hidden"); + } + }, + + /* + * Inject new mailboxes under a given parent in the tree. + */ + inject_mailboxes: function(parent, mboxes) { + var ul = new Element("ul", { + name: parent.get("name"), + 'class': "hidden" + }).inject(parent); + + for (var i = 0; i < mboxes.length; i++) { + var mbname = mboxes[i].name; + var li = new Element("li", { + name: mbname, + 'class': "droppable folder" + }).inject(ul); + var img = new Element("img", { + 'class': "footer", + src: static_url("pics/go-down.png") + }).inject(li); + var link = new Element("a", { + name: "loadfolder", + 'class': "block", + href: mbname + }).inject(li); + var parts = mbname.split("."); + var displayname = parts[parts.length - 1]; + + if (mboxes[i].unseen != undefined) { + link.addClass("unseen"); + link.set("html", displayname + " (" + mboxes[i].unseen + ")"); + } else { + link.set("html", displayname); + } + } + this.toggle_mbox_state(parent.getFirst("div"), ul); + }, + + /* + * Download sub-mailboxes from the server + */ + get_mailboxes: function(parent) { + new Request.JSON({ + url: this.options.submboxes_url, + onSuccess: function(resp) { + if (resp.status == "ko") { + return; + } + this.inject_mailboxes(parent, resp.mboxes); + }.bind(this) + }).get({topmailbox: parent.get("name")}); + }, + /* * Enables folders opening/closing * @@ -100,21 +166,17 @@ var Webmail = new Class({ */ init_folders_browsing: function() { $$("div[class*=clickbox]").addEvent("click", function(evt) { - var parent = this.getParent(); + var div = evt.target; + var parent = div.getParent(); var ul = parent.getFirst("ul[name=" + parent.get("name") + "]"); - if (!$defined(ul)) { + if (ul == undefined) { + this.get_mailboxes(parent); 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"); - } + this.toggle_mbox_state(div, ul); evt.stop(); - }); + }.bind(this)); }, /* diff --git a/extensions/webmail/templates/webmail/index.html b/extensions/webmail/templates/webmail/index.html index 1e86ea09c..7c23f9e27 100644 --- a/extensions/webmail/templates/webmail/index.html +++ b/extensions/webmail/templates/webmail/index.html @@ -75,7 +75,8 @@ function wm_updatelisting(resp) { 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 %}" + move_url: "{% url extensions.webmail.views.move %}", + submboxes_url: "{% url extensions.webmail.views.submailboxes %}" }); current_anchor.register_callback("folder", function(resp) { diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py index f083f7612..cf6996121 100644 --- a/extensions/webmail/urls.py +++ b/extensions/webmail/urls.py @@ -8,6 +8,7 @@ from django.conf.urls.defaults import * urlpatterns = patterns('modoboa.extensions.webmail.views', #(r'^$', "index"), (r'^$', "newindex"), + (r'^submailboxes', "submailboxes"), (r'^compose/$', "compose"), (r'^move/$', "move"), diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index c6a0cf673..419c27f51 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -518,6 +518,13 @@ def listmailbox(request): return lst.render(request) +@login_required +@is_not_localadmin() +def submailboxes(request): + topmailbox = request.GET.get('topmailbox', '') + mboxes = get_imapconnector(request).getfolders(request.user, topmailbox) + return ajax_simple_response(dict(status="ok", mboxes=mboxes)) + @login_required @is_not_localadmin() def newindex(request): From 0aace4e52168a6f76c790f4b078fd0c6d5a85603 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 17 Nov 2011 18:44:17 +0100 Subject: [PATCH 05/13] wip: webmail big refactoring and performance improvment * Drap&Drop feature fixed. * Mailboxes buttons are all functionnal (even the ones dynamically added) --- extensions/webmail/static/css/webmail.css | 13 +++ extensions/webmail/static/js/webmail.js | 123 +++++++++++----------- extensions/webmail/views.py | 5 +- static/js/anchor_nav.js | 2 +- 4 files changed, 81 insertions(+), 62 deletions(-) diff --git a/extensions/webmail/static/css/webmail.css b/extensions/webmail/static/css/webmail.css index 450941351..ef221f3fc 100644 --- a/extensions/webmail/static/css/webmail.css +++ b/extensions/webmail/static/css/webmail.css @@ -230,3 +230,16 @@ textarea:hover { .folder_menubar { clear: both; } + +.dragbox { + 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; +} \ No newline at end of file diff --git a/extensions/webmail/static/js/webmail.js b/extensions/webmail/static/js/webmail.js index 3880b610f..80e100e22 100644 --- a/extensions/webmail/static/js/webmail.js +++ b/extensions/webmail/static/js/webmail.js @@ -65,33 +65,36 @@ var Webmail = new Class({ }); }, + /* + * Open/Close the menu containing folder buttons + */ + toggle_folder_buttons: function(evt) { + var parent = evt.target.getParent(); + var fdname = parent.getFirst("a[name=loadfolder]").get("href"); + + this.fdmenu.toggle(evt.target, fdname); + evt.stop(); + }, + /* * 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)); + $$("a[name=loadfolder]").addEvent("click", this.listmailbox_loader.bind(this)); SqueezeBox.assign($("folders").getElements('a[class=boxed]'), { parse: 'rel' }); - var fdbuttons = $$("img[class*=footer]"); - var fdmenu = new FdMenu({ + this.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(); - } - }); + $$("img[class*=footer]") + .addEvent("click", this.toggle_folder_buttons.bind(this)); }, /* @@ -140,6 +143,9 @@ var Webmail = new Class({ } else { link.set("html", displayname); } + + link.addEvent("click", this.listmailbox_loader.bind(this)); + img.addEvent("click", this.toggle_folder_buttons.bind(this)); } this.toggle_mbox_state(parent.getFirst("div"), ul); }, @@ -259,24 +265,28 @@ var Webmail = new Class({ } else { this.emails_table.selectNone(); } + evt.stopPropagation(); }.bind(this)); } init_sortable_columns(); update_sortable_column(); + this.dragobjs = new Array(); $$("td[class*=draggable]").each(function(item) { - new Drag.Move(item, { + var pid = item.getParent().get("id"); + + this.dragobjs[pid] = new Drag.Move(item, { droppables: ".droppable" }).addEvents({ - beforeStart: this.bstart_dragging, - start: this.start_dragging, + beforeStart: this.bstart_dragging.bind(this), + start: this.start_dragging.bind(this), enter: this.enter_droppable, leave: this.leave_droppable, - drop: this.drop_element, - cancel: this.cancel_dragging + drop: this.drop_element.bind(this), + cancel: this.cancel_dragging.bind(this) }); - }); + }.bind(this)); $$("tbody>tr").addEvent("dblclick", viewmail); }, @@ -289,34 +299,27 @@ var Webmail = new Class({ * '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); + var dragobj = this.dragobjs[item.getParent().get("id")]; + var block = new Element("div", {id: "draggeditems", 'class': "dragbox"}) + .setStyles({ + top : dragobj.element.getCoordinates()['top'], + left : dragobj.element.getCoordinates()['left'] + }).inject(document.body); + + block.store("parent", dragobj.element); + dragobj.element = block; + clearInterval(this.rtimer); }, - start_dragging: function(item) { - var tr = this.element.retrieve("parent").getParent(); + start_dragging: function(item, event) { + var tr = event.target.getParent(); - if (!emails_table.isSelected(tr)) { - emails_table.selectNone(); - emails_table.selectRow(tr); + if (!this.emails_table.isSelected(tr)) { + this.emails_table.selectNone(); + this.emails_table.selectRow(tr); } - item.set("html", "Moving " + emails_table._selectedRows.length + " messages"); - this.element.setStyle("display", "block"); + item.set("html", "Moving " + this.emails_table._selectedRows.length + " messages"); + item.setStyle("display", "block"); }, enter_droppable: function(item, droppable) { @@ -327,23 +330,24 @@ var Webmail = new Class({ droppable.removeClass("dragging"); }, - restore_parent: function(drag, item) { + restore_parent: function(item) { var parent = item.retrieve("parent"); + var drag = this.dragobjs[parent.getParent().get("id")]; drag.element = parent; this.init_automatic_refresh(); }, drop_element: function(item, droppable, event) { - this.element.dispose(); - delete(this.element); + item.dispose(); if (!droppable) { - restore_parent(this, item); + this.restore_parent(item); return; } droppable.removeClass("dragging"); - dest = gethref(droppable.getFirst("a")); - msgset = new Array(); + var dest = gethref(droppable.getFirst("a")); + var msgset = new Array(); + this.emails_table._selectedRows.each(function(item, index) { msgset.include(item.get("id")); }); @@ -354,9 +358,8 @@ var Webmail = new Class({ }, cancel_dragging: function(item) { - this.element.dispose(); - delete(this.element); - restore_parent(this, item); + item.dispose(); + this.restore_parent(item); }, /* @@ -388,18 +391,20 @@ var Webmail = new Class({ * Onclick callback used to load the content of a particular * folder. (activated when clicking on a folder's name) */ - _folder_loader: function(event, obj) { + _listmailbox_loader: function(event, obj) { event.stop(); - if (!$defined(obj)) { + if (obj == undefined) { obj = $(event.target); } - current_anchor.parse_string(gethref(obj), true).setparams(navparams); - current_anchor.update(); + current_anchor.reset().setparams({ + action: "listmailbox", + name: gethref(obj) + }).update(); }, - folder_loader: function(event) { + listmailbox_loader: function(event) { this.select_folder(event.target); - this._folder_loader(event, event.target); + this._listmailbox_loader(event, event.target); }, /* @@ -416,7 +421,7 @@ var Webmail = new Class({ */ viewmail_callback: function(resp) { wm_updatelisting(resp, "hidden"); - $$("a[name=back]").addEvent("click", this._folder_loader); + $$("a[name=back]").addEvent("click", this._listmailbox_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); @@ -517,7 +522,7 @@ var Webmail = new Class({ $$("a[name=back]") .removeEvents("click") - .addEvent("click", this._folder_loader); + .addEvent("click", this._listmailbox_loader); $$("a[name=sendmail]") .store("editormode", editormode) .removeEvents("click") diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index 419c27f51..4db1e21ec 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -511,7 +511,7 @@ def render_mboxes_list(request, imapc): }) def listmailbox(request): - mbox = request.GET.get("mbox", "INBOX") + mbox = request.GET.get("name", "INBOX") request.session["mbox"] = mbox lst = ImapListing(request.user, request.session["password"], baseurl=mbox, folder=mbox, order="-date") @@ -560,7 +560,8 @@ def newindex(request): if not json: imapc = get_imapconnector(request) - response["refreshrate"] = parameters.get_user(request.user, "REFRESH_INTERVAL") + response["refreshrate"] = \ + int(parameters.get_user(request.user, "REFRESH_INTERVAL")) * 1000 response["mboxes"] = render_mboxes_list(request, imapc) try: menufunc = globals()["%s_menu" % action] diff --git a/static/js/anchor_nav.js b/static/js/anchor_nav.js index 732f09b4a..56531f1a3 100644 --- a/static/js/anchor_nav.js +++ b/static/js/anchor_nav.js @@ -35,7 +35,7 @@ var AnchorNavigation = new Class({ }, reset: function() { - this.base = null; + this.base = ''; this.params.empty(); return this; }, From 132b3a7637e259d8a5cd5de34c5ff25bf4eed50f Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 18 Nov 2011 17:46:26 +0100 Subject: [PATCH 06/13] wip: Webmail big refactoring and performance improvment * some issues with drag&drop and mailboxes list were fixed * the pagination feature was fixed --- extensions/webmail/imaputils.py | 14 +++++---- extensions/webmail/static/js/webmail.js | 31 ++++++++++++++----- extensions/webmail/views.py | 40 +++++++++++++++++-------- static/js/anchor_nav.js | 1 + static/js/email_listing.js | 10 ++++--- templates/common/navbar.html | 8 ++--- 6 files changed, 69 insertions(+), 35 deletions(-) diff --git a/extensions/webmail/imaputils.py b/extensions/webmail/imaputils.py index dd6ba51c0..282e3c652 100644 --- a/extensions/webmail/imaputils.py +++ b/extensions/webmail/imaputils.py @@ -67,7 +67,7 @@ class IMAPconnector(object): :param name: the command's name :return: the command's result """ - if name in ['FETCH', 'SORT', 'STORE']: + if name in ['FETCH', 'SORT', 'STORE', 'COPY']: try: typ, data = self.m.uid(name, *args) except imaplib.IMAP4.error, e: @@ -292,11 +292,13 @@ class IMAPconnector(object): 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 move(self, msgset, oldmailbox, newmailbox): + """Move messages between mailboxes + + """ + self.select_mailbox(oldmailbox, False) + self._cmd("COPY", msgset, newmailbox.encode("imap4-utf-7")) + self._cmd("STORE", msgset, "+FLAGS", r'(\Deleted)') def push_mail(self, folder, msg): now = imaplib.Time2Internaldate(time.time()) diff --git a/extensions/webmail/static/js/webmail.js b/extensions/webmail/static/js/webmail.js index 80e100e22..f882fc796 100644 --- a/extensions/webmail/static/js/webmail.js +++ b/extensions/webmail/static/js/webmail.js @@ -147,6 +147,7 @@ var Webmail = new Class({ link.addEvent("click", this.listmailbox_loader.bind(this)); img.addEvent("click", this.toggle_folder_buttons.bind(this)); } + this.init_draggables(); this.toggle_mbox_state(parent.getFirst("div"), ul); }, @@ -272,11 +273,29 @@ var Webmail = new Class({ init_sortable_columns(); update_sortable_column(); - this.dragobjs = new Array(); + this.init_draggables(); + + $$("tbody>tr").addEvent("dblclick", viewmail); + }, + + /* + * Drag & Drop feature + */ + init_draggables: function(pattern) { + if (this.dragobjs == undefined) { + this.dragobjs = new Array(); + } else { + this.dragobjs.each(function(item) { + item.detach(); + delete(item); + }); + this.dragobjs = new Array(); + } + $$("td[class*=draggable]").each(function(item) { var pid = item.getParent().get("id"); - this.dragobjs[pid] = new Drag.Move(item, { + this.dragobjs[pid] = item.makeDraggable({ droppables: ".droppable" }).addEvents({ beforeStart: this.bstart_dragging.bind(this), @@ -287,13 +306,8 @@ var Webmail = new Class({ cancel: this.cancel_dragging.bind(this) }); }.bind(this)); - - $$("tbody>tr").addEvent("dblclick", viewmail); }, - /* - * Drag & Drop feature - */ /* * 'before start' event callback @@ -574,12 +588,13 @@ var Webmail = new Class({ profile: "gray", spinner: true }); + params["json"] = "1"; new Request.JSON({ url : url, onSuccess : function(response) { if (response.status == "ok") { if (!$defined(response.next)) { - current_anchor.get_callback("folder")(response); + current_anchor.get_callback("listmailbox")(response); } infobox.info(gettext("Done")); if ($defined(response.next)) { diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index 4db1e21ec..888d314f7 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -211,11 +211,12 @@ def getattachment(request, folder, mail_id): def move(request): for arg in ["msgset", "to"]: if not request.GET.has_key(arg): - return - 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) + raise WebmailError(_("Invalid request")) + mbc = get_imapconnector(request) + mbc.move(request.GET["msgset"], request.session["mbox"], request.GET["to"]) + resp = listmailbox(request, request.session["mbox"]) + resp.update(status="ok") + return ajax_simple_response(resp) @login_required @is_not_localadmin() @@ -502,21 +503,33 @@ def delattachment(request): # NEW CODE STARTS HERE # def render_mboxes_list(request, imapc): + curmbox = request.session.get("mbox", "INBOX") return _render_to_string(request, "webmail/folders.html", { "titlebar" : True, - "selected" : request.session["mbox"], + "selected" : curmbox, "folders" : imapc.getfolders(request.user), "withmenu" : True, "withunseen" : True }) -def listmailbox(request): - mbox = request.GET.get("name", "INBOX") - request.session["mbox"] = mbox - lst = ImapListing(request.user, request.session["password"], - baseurl=mbox, folder=mbox, order="-date") +def set_nav_params(request): + if not request.session.has_key("navparams"): + request.session["navparams"] = {} + + request.session["pageid"] = \ + int(request.GET.get("page", 1)) - return lst.render(request) +def listmailbox(request, defmailbox="INBOX"): + mbox = request.GET.get("name", defmailbox) + request.session["mbox"] = mbox + + set_nav_params(request) + + lst = ImapListing(request.user, request.session["password"], + baseurl="?action=listmailbox&name=%s&" % mbox, + folder=mbox, order="-date") + + return lst.render(request, request.session["pageid"]) @login_required @is_not_localadmin() @@ -559,6 +572,7 @@ def newindex(request): defcallback="wm_updatelisting") if not json: + curmbox = request.session.get("mbox", "INBOX") imapc = get_imapconnector(request) response["refreshrate"] = \ int(parameters.get_user(request.user, "REFRESH_INTERVAL")) * 1000 @@ -567,7 +581,7 @@ def newindex(request): menufunc = globals()["%s_menu" % action] except KeyError: menufunc = globals()["listmailbox_menu"] - response["menu"] = menufunc("", request.session["mbox"], request.user) + response["menu"] = menufunc("", curmbox, request.user) return _render(request, "webmail/index.html", response) response.update(status="ok", callback=action) diff --git a/static/js/anchor_nav.js b/static/js/anchor_nav.js index 56531f1a3..f17619c38 100644 --- a/static/js/anchor_nav.js +++ b/static/js/anchor_nav.js @@ -112,6 +112,7 @@ var AnchorNavigation = new Class({ for (var i = 0; i < elems.length; i++) { this.setparamfromstring(elems[i]); } + return this; }, setparamfromstring: function(str) { diff --git a/static/js/email_listing.js b/static/js/email_listing.js index 97f561ecf..89b170616 100644 --- a/static/js/email_listing.js +++ b/static/js/email_listing.js @@ -4,9 +4,11 @@ */ function navbarcallback(event) { - event.stop(); - current_anchor.setparams(navparams); - current_anchor.parse_string($(event.target).getParent().get("href")).update(); + event.stop(); + current_anchor.reset() + .setparams(navparams) + .updateparams(this.get("href")) + .update(); } function updatelisting(resp) { @@ -23,7 +25,7 @@ function updatelisting(resp) { } if ($defined(resp.navbar)) { $("navbar").set("html", resp.navbar); - $("navbar").getChildren("a").addEvent("click", navbarcallback); + $$("#navbar a").addEvent("click", navbarcallback); } } diff --git a/templates/common/navbar.html b/templates/common/navbar.html index 0d5ac4277..182fb4e86 100644 --- a/templates/common/navbar.html +++ b/templates/common/navbar.html @@ -2,18 +2,18 @@ {% if page.previous_page %} - + - + {% endif %} {% if page.next_page %} - + - + {% endif %} From b9061b7f4ab1476d2536804e401816bec448bb0b Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 29 Nov 2011 11:52:26 +0100 Subject: [PATCH 07/13] wip: Big webmail refactoring and performance improvment * Fixed some issues with the navigation plugin * Compose form is reachable --- extensions/webmail/imaputils.py | 60 +++-- extensions/webmail/lib.py | 3 +- extensions/webmail/static/js/webmail.js | 34 ++- extensions/webmail/templatetags/webextras.py | 4 +- extensions/webmail/urls.py | 2 +- extensions/webmail/views.py | 227 +++++++++++++------ static/js/anchor_nav.js | 22 +- static/js/common.js | 27 ++- 8 files changed, 256 insertions(+), 123 deletions(-) diff --git a/extensions/webmail/imaputils.py b/extensions/webmail/imaputils.py index 282e3c652..57bce66aa 100644 --- a/extensions/webmail/imaputils.py +++ b/extensions/webmail/imaputils.py @@ -230,7 +230,7 @@ class IMAPconnector(object): @capability('LIST-EXTENDED', '_listfolders_simple') def _listfolders(self, topmailbox='', md_mailboxes=[]): - pattern = ("%s.%%" % topmailbox) if len(topmailbox) else "%" + pattern = ("%s.%%" % topmailbox.encode("imap4-utf-7")) if len(topmailbox) else "%" resp = self._cmd("LSUB", "", pattern, "RETURN", "(CHILDREN)") result = [] for mb in resp: @@ -275,16 +275,32 @@ class IMAPconnector(object): fd["unseen"] = count return md_mailboxes - def _add_flag(self, folder, mail_id, flag): - self.m.select(self._encodefolder(folder)) - self.m.store(mail_id, "+FLAGS", flag) + def _add_flag(self, mbox, msgset, flag): + """Add flag(s) to a messages set - def msg_unread(self, folder, msgset): - self.m.select(self._encodefolder(folder)) - self.m.store(msgset, "-FLAGS", r'(\Seen)') + :param mbox: the mailbox containing the messages + :param msgset: messages set (uid) + :param flag: the flag to add + """ + self.select_mailbox(mbox, False) + self._cmd("STORE", msgset, "+FLAGS", flag) - def msg_read(self, folder, msgset): - self._add_flag(folder, msgset, r'(\Seen)') + def mark_messages_unread(self, mbox, msgset): + """Mark a set of messages as unread + + :param mbox: the mailbox containing the messages + :param msgset: messages set (uid) + """ + self.select_mailbox(mbox, False) + self._cmd("STORE", msgset, "-FLAGS", r'(\Seen)') + + def mark_messages_read(self, mbox, msgset): + """Mark a set of messages as unread + + :param mbox: the mailbox containing the messages + :param msgset: messages set (uid) + """ + self._add_flag(mbox, msgset, r'(\Seen)') def msgforwarded(self, folder, imapid): self._add_flag(folder, imapid, '($Forwarded)') @@ -304,16 +320,24 @@ class IMAPconnector(object): 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 empty(self, mbox): + self.select_mailbox(mbox, False) + resp = self._cmd("SEARCH", "ALL") + print resp + + # 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 compact(self, mbox): + """Compact a specific mailbox + + Issue an EXPUNGE command for the specified mailbox. + + :param mbox: the mailbox's name + """ + self.select_mailbox(mbox, False) + self._cmd("EXPUNGE") def create_folder(self, name, parent=None): if parent is not None: diff --git a/extensions/webmail/lib.py b/extensions/webmail/lib.py index 748850aa0..09317b315 100644 --- a/extensions/webmail/lib.py +++ b/extensions/webmail/lib.py @@ -110,8 +110,9 @@ class ImapListing(EmailListing): if kwargs.has_key("pattern"): self.parse_search_parameters(kwargs["criteria"], kwargs["pattern"]) - if kwargs.has_key("reset"): + else: self.mbc.criterions = [] + EmailListing.__init__(self, **kwargs) self.extravars["refreshrate"] = \ int(parameters.get_user(user, "REFRESH_INTERVAL")) * 1000 diff --git a/extensions/webmail/static/js/webmail.js b/extensions/webmail/static/js/webmail.js index f882fc796..ec96978a5 100644 --- a/extensions/webmail/static/js/webmail.js +++ b/extensions/webmail/static/js/webmail.js @@ -42,6 +42,7 @@ var Webmail = new Class({ */ init_menubar: function() { searchbox_init(); + $$("a[name=compose]").addEvent("click", this.compose_loader); $$("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) { @@ -58,11 +59,21 @@ var Webmail = new Class({ var ids = new Array(); this.emails_table._selectedRows.each(function(item, index) { - ids.include(item.get("id")); + ids.include(item.get("id")); }); this.simple_request(evt.target.get("href"), { ids : ids.join() - }); + }, function(response) { + if (response.action == "read") { + this.emails_table._selectedRows.removeClass("unseen"); + } else { + this.emails_table._selectedRows.addClass("unseen"); + } + /* + * MAJ de l'affichage du nombre de messages non lus + * (faire fonction à part) + */ + }.bind(this)); }, /* @@ -461,7 +472,7 @@ var Webmail = new Class({ */ compose_loader: function(event) { event.stop(); - current_anchor.baseurl("compose").update(); + current_anchor.reset().setparam("action", this.get("href")).update(); }, /* @@ -505,7 +516,8 @@ var Webmail = new Class({ * It is also shared with other similar actions : reply, forward. */ compose_callback: function(resp) { - wm_updatelisting(resp); + //wm_updatelisting(resp); + updatelisting(resp); window.addEvent("resize", this.resize_window_callback.bind(this)); @@ -534,9 +546,9 @@ var Webmail = new Class({ }); }); - $$("a[name=back]") + /*$$("a[name=back]") .removeEvents("click") - .addEvent("click", this._listmailbox_loader); + .addEvent("click", this._listmailbox_loader);*/ $$("a[name=sendmail]") .store("editormode", editormode) .removeEvents("click") @@ -583,7 +595,7 @@ var Webmail = new Class({ * encoded answer. The infobox is cleared by the request callback * function. */ - simple_request: function(url, params) { + simple_request: function(url, params, callback) { infobox.show(gettext("Waiting..."), { profile: "gray", spinner: true @@ -593,8 +605,12 @@ var Webmail = new Class({ url : url, onSuccess : function(response) { if (response.status == "ok") { - if (!$defined(response.next)) { - current_anchor.get_callback("listmailbox")(response); + if (response.next == undefined) { + if (callback != undefined) { + callback(response); + } else { + current_anchor.get_callback("listmailbox")(response); + } } infobox.info(gettext("Done")); if ($defined(response.next)) { diff --git a/extensions/webmail/templatetags/webextras.py b/extensions/webmail/templatetags/webextras.py index 74b9bdbd4..7fbc923a0 100644 --- a/extensions/webmail/templatetags/webextras.py +++ b/extensions/webmail/templatetags/webextras.py @@ -56,7 +56,7 @@ def viewm_menu(selection, backurl, folder, mail_id, user): def compose_menu(selection, backurl, user): entries = [ {"name" : "back", - "url" : backurl, + "url" : "javascript:history.go(-2);",#backurl, "img" : static_url("pics/back.png"), "label" : _("Back")}, {"name" : "sendmail", @@ -72,7 +72,7 @@ def compose_menu(selection, backurl, user): def listmailbox_menu(selection, folder, user): entries = [ {"name" : "compose", - "url" : reverse(webmail.views.compose), + "url" : "compose", "img" : static_url("pics/edit.png"), "label" : _("New message")}, {"name" : "mark", diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py index cf6996121..c78f24209 100644 --- a/extensions/webmail/urls.py +++ b/extensions/webmail/urls.py @@ -10,7 +10,7 @@ urlpatterns = patterns('modoboa.extensions.webmail.views', (r'^$', "newindex"), (r'^submailboxes', "submailboxes"), - (r'^compose/$', "compose"), +# (r'^compose/$', "compose"), (r'^move/$', "move"), (r'^mark/(?P.+)/$', "mark"), (r'^empty/(?P.+)/$', "empty"), diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index 888d314f7..c281e4351 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -230,87 +230,89 @@ def delete(request, fdname, mail_id): @login_required @is_not_localadmin() 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, - password=request.session["password"]) + status = request.GET.get("status", None) + ids = request.GET.get("ids", None) + if status is None or ids is None: + raise WebmailError(_("Invalid request")) + imapc = get_imapconnector(request) try: - getattr(mbc, "msg_%s" % request.GET["status"])(name, request.GET["ids"]) + getattr(imapc, "mark_messages_%s" % status)(name, ids) except AttributeError: - pass - return folder(request, name, False) + raise WebmailError(_("Unknown action")) + + return ajax_simple_response( + dict(status="ok", action=status, mbox=name, + unseen=imapc.unseen_messages(name)) + ) @login_required @is_not_localadmin() def empty(request, name): if name == parameters.get_user(request.user, "TRASH_FOLDER"): - mbc = IMAPconnector(user=request.user.username, - password=request.session["password"]) - mbc.empty(name) - return folder(request, name, False) + get_imapconnector(request).empty(name) + return ajax_simple_response(dict(status="ok")) @login_required @is_not_localadmin() def compact(request, name): - mbc = IMAPconnector(user=request.user.username, - password=request.session["password"]) - mbc.compact(name) - return folder(request, name, False) + imapc = get_imapconnector(request) + imapc.compact(name) + return ajax_simple_response(dict(status="ok")) -def render_compose(request, form, posturl, email=None, insert_signature=False): - menu = compose_menu("", get_current_url(request), - request.user.get_all_permissions()) - editor = parameters.get_user(request.user, "EDITOR") - if email is None: - body = "" - textheader = "" - else: - body = email.body - textheader = email.textheader - if insert_signature: - signature = EmailSignature(request.user) - body += str(signature) - randid = None - if not request.GET.has_key("id"): - if request.session.has_key("compose_mail"): - clean_attachments(request.session["compose_mail"]["attachments"]) - randid = set_compose_session(request) - elif not request.session.has_key("compose_mail") \ - or request.session["compose_mail"]["id"] != request.GET["id"]: - randid = set_compose_session(request) +# def render_compose(request, form, posturl, email=None, insert_signature=False): +# menu = compose_menu("", get_current_url(request), +# request.user.get_all_permissions()) +# editor = parameters.get_user(request.user, "EDITOR") +# if email is None: +# body = "" +# textheader = "" +# else: +# body = email.body +# textheader = email.textheader +# if insert_signature: +# signature = EmailSignature(request.user) +# body += str(signature) +# randid = None +# if not request.GET.has_key("id"): +# if request.session.has_key("compose_mail"): +# clean_attachments(request.session["compose_mail"]["attachments"]) +# randid = set_compose_session(request) +# elif not request.session.has_key("compose_mail") \ +# or request.session["compose_mail"]["id"] != request.GET["id"]: +# randid = set_compose_session(request) - attachments = request.session["compose_mail"]["attachments"] - if len(attachments): - short_att_list = "(%s)" \ - % ", ".join(map(lambda att: att["fname"], - attachments[:2] + [{"fname" : "..."}] \ - if len(attachments) > 2 else attachments)) - else: - short_att_list = "" - content = _render_to_string(request, "webmail/compose.html", { - "form" : form, "bodyheader" : textheader, - "body" : body, "posturl" : posturl, - "attachments" : attachments, "short_att_list" : short_att_list - }) - mbc = IMAPconnector(user=request.user.username, - password=request.session["password"]) - ctx = getctx("ok", level=2, editor=editor, - **__render_common_components(request, request.session["folder"], - menu=menu, content=content)) - if randid is not None: - ctx["id"] = randid - return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") +# attachments = request.session["compose_mail"]["attachments"] +# if len(attachments): +# short_att_list = "(%s)" \ +# % ", ".join(map(lambda att: att["fname"], +# attachments[:2] + [{"fname" : "..."}] \ +# if len(attachments) > 2 else attachments)) +# else: +# short_att_list = "" +# content = _render_to_string(request, "webmail/compose.html", { +# "form" : form, "bodyheader" : textheader, +# "body" : body, "posturl" : posturl, +# "attachments" : attachments, "short_att_list" : short_att_list +# }) +# mbc = IMAPconnector(user=request.user.username, +# password=request.session["password"]) +# ctx = getctx("ok", level=2, editor=editor, +# **__render_common_components(request, request.session["folder"], +# menu=menu, content=content)) +# if randid is not None: +# ctx["id"] = randid +# return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") -@login_required -@is_not_localadmin() -def compose(request): - if request.method == "POST": - status, resp = send_mail(request, posturl=reverse(compose)) - return resp +# @login_required +# @is_not_localadmin() +# def compose(request): +# if request.method == "POST": +# status, resp = send_mail(request, posturl=reverse(compose)) +# return resp - form = ComposeMailForm() - form.fields["from_"].initial = request.user.username - return render_compose(request, form, reverse(compose), insert_signature=True) +# form = ComposeMailForm() +# form.fields["from_"].initial = request.user.username +# return render_compose(request, form, reverse(compose), insert_signature=True) @login_required @is_not_localadmin() @@ -503,6 +505,12 @@ def delattachment(request): # NEW CODE STARTS HERE # def render_mboxes_list(request, imapc): + """Return the HTML representation of a mailboxes list + + :param request: a ``Request`` object + :param imap: an ``IMAPconnector` object + :return: a string + """ curmbox = request.session.get("mbox", "INBOX") return _render_to_string(request, "webmail/folders.html", { "titlebar" : True, @@ -518,19 +526,83 @@ def set_nav_params(request): request.session["pageid"] = \ int(request.GET.get("page", 1)) + if not request.GET.get("order", False): + if not request.session["navparams"].has_key("order"): + request.session["navparams"]["order"] = "-date" + else: + request.session["navparams"]["order"] = request.GET.get("order") + + for p in ["pattern", "criteria"]: + if request.GET.get(p, False): + request.session["navparams"][p] = request.GET[p] + elif request.session["navparams"].has_key(p): + del request.session["navparams"][p] def listmailbox(request, defmailbox="INBOX"): + """Mailbox content listing + + Return a list of messages contained in the specified mailbox. The + number of elements returned depends on the ``MESSAGES_PER_PAGE`` + parameter. (user preferences) + + :param request: a ``Request`` object + :param defmailbox: the default mailbox (when not present inside request arguments) + :return: a dictionnary + """ mbox = request.GET.get("name", defmailbox) request.session["mbox"] = mbox - set_nav_params(request) lst = ImapListing(request.user, request.session["password"], baseurl="?action=listmailbox&name=%s&" % mbox, - folder=mbox, order="-date") + folder=mbox, **request.session["navparams"]) return lst.render(request, request.session["pageid"]) +def render_compose(request, form, posturl, email=None, insert_signature=False): + editor = parameters.get_user(request.user, "EDITOR") + if email is None: + body = "" + textheader = "" + else: + body = email.body + textheader = email.textheader + if insert_signature: + signature = EmailSignature(request.user) + body += str(signature) + randid = None + if not request.GET.has_key("id"): + if request.session.has_key("compose_mail"): + clean_attachments(request.session["compose_mail"]["attachments"]) + randid = set_compose_session(request) + elif not request.session.has_key("compose_mail") \ + or request.session["compose_mail"]["id"] != request.GET["id"]: + randid = set_compose_session(request) + + attachments = request.session["compose_mail"]["attachments"] + if len(attachments): + short_att_list = "(%s)" \ + % ", ".join(map(lambda att: att["fname"], + attachments[:2] + [{"fname" : "..."}] \ + if len(attachments) > 2 else attachments)) + else: + short_att_list = "" + content = _render_to_string(request, "webmail/compose.html", { + "form" : form, "bodyheader" : textheader, + "body" : body, "posturl" : posturl, + "attachments" : attachments, "short_att_list" : short_att_list + }) + + ctx = dict(listing=content, editor=editor) + if randid is not None: + ctx["id"] = randid + return ctx + +def compose(request): + form = ComposeMailForm() + form.fields["from_"].initial = request.user.username + return render_compose(request, form, "compose", insert_signature=True) + @login_required @is_not_localadmin() def submailboxes(request): @@ -556,6 +628,11 @@ def newindex(request): faudrait pouvoir envoyer le menu par la même occasion, le souci étant de savoir lequel... + Une solution possible : il suffirait de déplacer le menu vers la + droite pour l'aligner avec le contenu, remonter la liste des + dossiers (même hauteur que le menu) et renvoyer le menu en même + temps que le contenu. Le rendu sera plus uniforme je pense. + """ action = request.GET.get("action", None) json = request.GET.get("json", False) @@ -571,8 +648,8 @@ def newindex(request): response = dict(deflocation="?action=listmailbox", defcallback="wm_updatelisting") + curmbox = request.session.get("mbox", "INBOX") if not json: - curmbox = request.session.get("mbox", "INBOX") imapc = get_imapconnector(request) response["refreshrate"] = \ int(parameters.get_user(request.user, "REFRESH_INTERVAL")) * 1000 @@ -580,9 +657,19 @@ def newindex(request): try: menufunc = globals()["%s_menu" % action] except KeyError: - menufunc = globals()["listmailbox_menu"] + if request.session.has_key("lastaction"): + menufunc = globals()["%s_menu" % request.session["lastaction"]] + else: + menufunc = globals()["listmailbox_menu"] response["menu"] = menufunc("", curmbox, request.user) return _render(request, "webmail/index.html", response) + if request.session.get("lastaction", None) != action: + request.session["lastaction"] = action + try: + response["menu"] = globals()["%s_menu" % action]("", curmbox, request.user) + except KeyError: + pass + response.update(status="ok", callback=action) return ajax_simple_response(response) diff --git a/static/js/anchor_nav.js b/static/js/anchor_nav.js index f17619c38..45ea29075 100644 --- a/static/js/anchor_nav.js +++ b/static/js/anchor_nav.js @@ -53,11 +53,13 @@ var AnchorNavigation = new Class({ if (reset) { this.reset(); } - this.base = (splits[0] == "") ? this.deflocation : splits[0]; + this.base = splits[0]; - var re = new RegExp("/$"); - if (!this.base.match(re)) { - this.base += '/'; + if (this.base != "") { + var re = new RegExp("/$"); + if (!this.base.match(re)) { + this.base += '/'; + } } if (splits.length > 1) { var params = splits[1].split('&'); @@ -183,16 +185,14 @@ var AnchorNavigation = new Class({ }, check_anchor: function() { - if (this.serialized == location.hash - && !$defined(this.force)) { + if (this.serialized == location.hash && this.force == undefined) { return; } delete(this.force); - this.from_string(location.hash); - if (!this.serialized) { - location.hash = this.deflocation; - return; - } + + this.from_string((location.hash != "") ? location.hash : "#" + this.deflocation); + location.hash = this.serialized; + if (!this.updatenext) { this.updatenext = true; return; diff --git a/static/js/common.js b/static/js/common.js index 77c5ba472..70b1c21c9 100644 --- a/static/js/common.js +++ b/static/js/common.js @@ -153,8 +153,10 @@ checkTableSelection = function(text) { return confirm(text); }; -searchbox_init = function() { +function searchbox_init() { var myFx = new Fx.Slide($("searchparams")); + var $searchfield = $("searchfield"); + myFx.hide(); $("searchopts").addEvent("click", function(event) { myFx.cancel(); @@ -164,28 +166,31 @@ searchbox_init = function() { myFx.cancel(); myFx.toggle(); }); - $("searchfield").addEvent("change", function(event) { + $searchfield.addEvent("change", function(event) { var criteria = ""; + var pattern = this.get("value"); + $$("input[name=scriteria]:checked").each(function(obj) { criteria = obj.value; }); - var pattern = $("searchfield").get("value"); if (pattern != "") { - var params = "pattern=" + pattern + "&criteria=" + criteria; - current_anchor.updateparams(params); + current_anchor.setparams({ + pattern: pattern, + criteria: criteria + }); } else { current_anchor.deleteParam("pattern"); current_anchor.deleteParam("criteria"); } current_anchor.update(); }); - $("searchfield").addEvent("click", function(event) { - $("searchfield").select(); + $searchfield.addEvent("click", function(event) { + $searchfield.select(); }); - $("searchfield").addEvent("blur", function(event) { - if ($("searchfield").value == "") { - $("searchfield").set("value", gettext("Search...")); + $searchfield.addEvent("blur", function(event) { + if ($searchfield.value == "") { + $searchfield.set("value", gettext("Search...")); } }); $("searchbox").setStyle("float", "left"); @@ -193,7 +198,7 @@ searchbox_init = function() { if (frag.indexOf('?') != -1) { var params = frag.substring(frag.indexOf('?') + 1).parseQueryString(); if ($defined(params.pattern)) { - $("searchfield").set("value", params.pattern); + $searchfield.set("value", params.pattern); } if ($defined(params.criteria)) { $("crit_" + params.criteria).checked = true; From c7cfac8e0a0ce6083a73589e84d64d79d7f6da0a Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 1 Dec 2011 17:36:21 +0100 Subject: [PATCH 08/13] wip: webmail big refactoring and perfomance improvment Misc. changes. --- extensions/webmail/imaputils.py | 88 ++++++++++++- extensions/webmail/static/css/webmail.css | 8 ++ extensions/webmail/static/js/webmail.js | 63 ++++++--- .../webmail/templates/webmail/index.html | 1 + extensions/webmail/templatetags/webextras.py | 10 +- extensions/webmail/urls.py | 6 +- extensions/webmail/views.py | 120 ++++++++++-------- lib/email_listing.py | 2 +- static/css/default.css | 2 +- static/css/spinner.gif | Bin 1569 -> 2545 bytes static/js/anchor_nav.js | 39 +++++- templates/common/email_listing.html | 4 +- templates/nlayout.html | 1 + 13 files changed, 251 insertions(+), 93 deletions(-) diff --git a/extensions/webmail/imaputils.py b/extensions/webmail/imaputils.py index 57bce66aa..1750ac7f6 100644 --- a/extensions/webmail/imaputils.py +++ b/extensions/webmail/imaputils.py @@ -12,7 +12,7 @@ from modoboa.lib.connections import * from modoboa.lib.webutils import static_url from exceptions import ImapError -imaplib.Debug = 4 +#imaplib.Debug = 4 class capability(object): """ @@ -88,6 +88,32 @@ class IMAPconnector(object): return None return self.m.untagged_responses.pop(name) + def __find_content_in_bodystruct(self, bodystruct, mtype, stype, prefix=""): + """Retrieve the number (index) of a specific part + + This part number will generally be used inside a FETCH request + to specify a ``BODY.PEEK`` section. + + :param bodystruct: a BODYSTRUCTURE list + :param mtype: the MIME main type (like text) + :param stype: the MIME sub type (like plain) + :param prefix: the prefix that will be added to the current part number + :return: a tuple (index (None on error), encoding (string), size (int)) + """ + if type(bodystruct[0]) in [list, tuple]: + cpt = 1 + for part in bodystruct[0]: + nprefix = "%s" % cpt if prefix == "" else "%s.%d" % (prefix, cpt) + index, encoding, size = \ + self.__find_content_in_bodystruct(bodystruct[0], mtype, stype, nprefix) + if index: + return (index, encoding, size) + cpt += 1 + + if bodystruct[0] == mtype and bodystruct[1] == stype: + return ("1" if not len(prefix) else prefix, bodystruct[5], int(bodystruct[6])) + return (None, None, 0) + def refresh(self, user, password): """Check if current connection needs a refresh @@ -384,18 +410,24 @@ class IMAPconnector(object): msg = email.message_from_string(data[0][1] + data[1][1]) return msg - def fetch(self, start, stop=None, folder=None, all=False, **kwargs): - self.select_mailbox(folder, False) + def fetch(self, start, stop=None, mbox=None, **kwargs): + """Retrieve information about messages from the server + + Issue a FETCH command to retrieve information about one or + more messages (such as headers) from the server. + + :param start: index of the first message + :param stop: index of the last message (optionnal) + :param mbox: the mailbox that contains the messages + """ + self.select_mailbox(mbox, False) if start and stop: submessages = self.messages[start - 1:stop] range = ",".join(submessages) else: submessages = [start] range = start - if not all: - query = '(FLAGS BODYSTRUCTURE BODY.PEEK[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])' - else: - query = '(RFC822)' + query = '(FLAGS BODY.PEEK[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])' data = self._cmd("FETCH", range, query) result = [] for uid in submessages: @@ -408,6 +440,48 @@ class IMAPconnector(object): result += [msg] return result + def fetchmail(self, mbox, mailid, fmt="plain"): + """Retrieve information about a specific message + + Issue a FETCH command to retrieve a message's content from the + server. In order to not overload the server, we first retrieve + the BODYSTRUCTURE of the message. Then, according to the + result and to the user's preferences, we retrieve the + appropriate content (plain, html, etc.). + + :param mbox: the mailbox containing the message + :param mailid: the message's unique id + :param fmt: the desired content's format + """ + self.select_mailbox(mbox, False) + data = self._cmd("FETCH", mailid, "(BODYSTRUCTURE BODY.PEEK[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])") + content = data[int(mailid)]['BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)]'] + + fallback_fmt = "html" if fmt == "plain" else "plain" + (pnum, encoding, size) = (None, None, 0) + for f in [fmt, fallback_fmt]: + pnum, encoding, size = \ + self.__find_content_in_bodystruct(data[int(mailid)]['BODYSTRUCTURE'], + "text", f) + if pnum is not None and size: + break + if pnum is None: + return None + + data = self._cmd("FETCH", mailid, "(BODY.PEEK[%s])" % pnum) + + data = data[int(mailid)]['BODY[%s]' % pnum] + if encoding == "base64": + import base64 + content += base64.b64decode(data) + elif encoding == "quoted-printable": + import quopri + content += quopri.decodestring(data) + else: + content += data + + return email.message_from_string(content) + def get_imapconnector(request): imapc = IMAPconnector(user=request.user.username, password=request.session["password"]) diff --git a/extensions/webmail/static/css/webmail.css b/extensions/webmail/static/css/webmail.css index ef221f3fc..7c9a7c60c 100644 --- a/extensions/webmail/static/css/webmail.css +++ b/extensions/webmail/static/css/webmail.css @@ -12,6 +12,14 @@ html > body #listing { bottom: 35px; } +body.loading { + display: block; + filter: alpha(opacity=50); + opacity: 0.5; + z-index: 3; + background: #FFF url("../../css/spinner.gif") center center no-repeat; +} + .clearfix:after { content: "."; display: block; diff --git a/extensions/webmail/static/js/webmail.js b/extensions/webmail/static/js/webmail.js index ec96978a5..09fd0c762 100644 --- a/extensions/webmail/static/js/webmail.js +++ b/extensions/webmail/static/js/webmail.js @@ -21,13 +21,13 @@ var Webmail = new Class({ this.rtimer = null; this.editorid = "id_body"; - this.init_menubar(); this.init_folders_buttons(); this.init_folders_browsing(); /* FIXME: à faire uniquement pour le mode pushState */ /*this.init_emails_table();*/ + current_anchor.disable_spinner(); current_anchor.register_callback("listmailbox", this.listmailbox_callback.bind(this)); current_anchor.register_callback("viewmail", this.viewmail_callback.bind(this)); @@ -35,20 +35,17 @@ var Webmail = new Class({ 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=compose]").addEvent("click", this.compose_loader); - $$("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(); + this.gspinner = new Spinner("document.body", { + message: gettext("Loading, please wait..."), + 'class': "spinner-noop" + }); + this.gspinner.addEvent("hide", function(target) { + this.gspinner.destroy(); + delete(this.gspinner); + current_anchor.enable_spinner(); }.bind(this)); + this.gspinner.show(); }, send_mark_request: function(evt) { @@ -286,7 +283,7 @@ var Webmail = new Class({ this.init_draggables(); - $$("tbody>tr").addEvent("dblclick", viewmail); + $$("tbody>tr").addEvent("dblclick", this.viewmail_loader); }, /* @@ -405,7 +402,10 @@ var Webmail = new Class({ * */ page_update: function(response) { - + if (this.gspinner) { + this.gspinner.hide(); + } + updatelisting(response); }, /* @@ -437,16 +437,41 @@ var Webmail = new Class({ */ listmailbox_callback: function(resp) { window.removeEvents("resize"); - updatelisting(resp); + this.page_update(resp); + if (resp.menu != undefined) { + searchbox_init(); + $$("a[name=compose]").addEvent("click", this.compose_loader); + $$("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)); + } this.init_emails_table(); }, + /* + * Loader of the 'viemmail' action + */ + viewmail_loader: function(evt) { + evt.stop(); + current_anchor.reset().setparams({ + action: "viewmail", + mailid: this.get("id") + }).update(); + }, + /* * Callback of the 'viewmail' action */ viewmail_callback: function(resp) { - wm_updatelisting(resp, "hidden"); - $$("a[name=back]").addEvent("click", this._listmailbox_loader); + /*wm_updatelisting(resp, "hidden");*/ + this.page_update(resp); + + //$$("a[name=back]").addEvent("click", this._listmailbox_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); @@ -517,7 +542,7 @@ var Webmail = new Class({ */ compose_callback: function(resp) { //wm_updatelisting(resp); - updatelisting(resp); + this.page_update(resp); window.addEvent("resize", this.resize_window_callback.bind(this)); diff --git a/extensions/webmail/templates/webmail/index.html b/extensions/webmail/templates/webmail/index.html index 7c23f9e27..691b5141a 100644 --- a/extensions/webmail/templates/webmail/index.html +++ b/extensions/webmail/templates/webmail/index.html @@ -8,6 +8,7 @@ {{ block.super }} {% block css %}{% endblock %} From ff9528dbda76fa15e073f58bbcedc50e6cd0331d Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 1 Feb 2012 09:36:42 +0100 Subject: [PATCH 09/13] wip: Webmail big refactoring * New fetch responses parser * Better detection of attachments --- extensions/webmail/fetch_parser.py | 173 +++++++++++++++++++++++++++++ extensions/webmail/imaputils.py | 144 +++++++++++++++++------- extensions/webmail/lib.py | 106 ++++++++++++++++-- extensions/webmail/views.py | 9 +- lib/emailutils.py | 2 +- 5 files changed, 379 insertions(+), 55 deletions(-) create mode 100644 extensions/webmail/fetch_parser.py diff --git a/extensions/webmail/fetch_parser.py b/extensions/webmail/fetch_parser.py new file mode 100644 index 000000000..7ebcb9c0e --- /dev/null +++ b/extensions/webmail/fetch_parser.py @@ -0,0 +1,173 @@ +# coding: utf-8 +""" +:mod:`fetch_parser` --- Simple parser for FETCH responses +--------------------------------------------------------- + +The ``imaplib`` module doesn't parse IMAP responses, it returns raw +values. This is pretty annoying when one FETCH command is issued to +retrieve multiple attributes of a message. + +This simple module tries to fix that *problem*. +""" + +class ParseError(Exception): pass + +def parse_next_token(buf): + """Look for the next *token* + + By *token*, I mean: *literal*, *quoted* or anything else until the + next ' ' or ')' character (number, NIL and others should fall into + this last category). + + :param buf: the buffer to parse + :return: the position of the token's last caracter into ``buf`` + """ + end = -1 + if buf[0] == '{': + # Literal + end = buf.find('}') + elif buf[0] == '"': + # quoted + end = buf.find('"', 1) + else: + for pos, c in enumerate(buf): + if c in [' ', ')']: + end = pos - 1 + break + if end == -1: + raise ParseError("End of buffer reached while looking for a token end") + return end + +def parse_bodystructure(buf, depth=0, prefix=""): + """Special parser for BODYSTRUCTURE response + + This function tries to transform a BODYSTRUCTURE response sent by + the server into the corresponding list structure. + + :param buf: the buffer to parse + :return: a list object and the position of the last scanned + character into ``buf`` + """ + ret = [] + pos = 1 # skip the first ')' + nb_bodystruct = 0 + pnum = 1 + while pos < len(buf): + c = buf[pos] + if c == '(': + nprefix = "%s.%s" % (prefix, pnum) if prefix != "" else "%s" % pnum + subret, end = parse_bodystructure(buf[pos:], depth + 1, nprefix) + pnum += 1 + if nb_bodystruct == 0: + ret.append(subret) + elif nb_bodystruct == 1: + newret = [ret.pop(-1)] + newret.append(subret) + ret.append(newret) + else: + ret[-1].append(subret) + pos += end + 1 + nb_bodystruct += 1 + continue + + if c == ')': + partnum = None + if depth: + if type(ret[0]) == list or len(ret) >= 7: + partnum = prefix + if partnum is not None: + return [dict(partnum=partnum, struct=ret)], pos + return ret, pos + if c == ' ': + pos += 1 + continue + nb_bodystruct = 0 + end = parse_next_token(buf[pos:]) + ret.append(buf[pos:pos + end + 1].strip('"')) + pos += end + 1 + + raise ParseError("End of buffer reached while looking for a BODY/BODYSTRUCTURE end") + +def parse_fetch_response(data): + """Parse a FETCH response, previously issued by a UID command + + We extract the message number, the UID and its value and consider + what remains as the response data. + + :param data: the data returned by the ``imaplib`` command + :return: a dictionnary + """ + result = {} + for part in data[0:-1]: + if part == ')': + continue + buf = "".join(part) + parts = buf.split(' ', 3) + msgdef = result[int(parts[2])] = {} + response = parts[3] + + while len(response): + if response.startswith('BODY') and response[4] == '[': + end = response.find(']', 5) + if response[end + 1] == '<': + end = response.find('>', end + 1) + end += 1 + else: + end = response.find(' ') + cmdname = response[:end] + response = response[end + 1:] + + end = 0 + if cmdname in ['BODY', 'BODYSTRUCTURE', 'FLAGS']: + parendepth = 0 + for pos, c in enumerate(response): + if c == '(': + parendepth += 1 + continue + if c == ')': + parendepth -= 1 + if parendepth == 0: + end = pos + 1 + break + else: + end = parse_next_token(response) + if response[0] == '{': + datalen = int(response[1:end]) + response = response[end + 1:] + end = datalen + + msgdef[cmdname] = response[:end] + response = response[end + 1:] + try: + func = globals()["parse_%s" % cmdname.lower()] + msgdef[cmdname] = func(msgdef[cmdname])[0] + except KeyError: + pass + + return result + +def dump_bodystructure(bs, depth=0): + if depth: + print " " * (depth * 4), + if type(bs[0]) == dict: + print "%s :" % bs[0]["partnum"], + struct = bs[0]["struct"] + else: + struct = bs + + if type(struct[0]) == list: + print "multipart/%s" % struct[1] + for part in struct[0]: + dump_bodystructure(part, depth + 1) + else: + print "%s/%s" % (struct[0], struct[1]) + + +if __name__ == "__main__": + resp = [('855 (UID 46931 BODYSTRUCTURE ((("text" "plain" ("charset" "iso-8859-1") NIL NIL "quoted-printable" 886 32 NIL NIL NIL NIL)("text" "html" ("charset" "us-ascii") NIL NIL "quoted-printable" 1208 16 NIL NIL NIL NIL) "alternative" ("boundary" "----=_NextPart_001_0003_01CCC564.B2F64FF0") NIL NIL NIL)("application" "octet-stream" ("name" "Carte Verte_2.pdf") NIL NIL "base64" 285610 NIL ("attachment" ("filename" "Carte Verte_2.pdf")) NIL NIL) "mixed" ("boundary" "----=_NextPart_000_0002_01CCC564.B2F64FF0") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {153}', 'From: \r\nTo: \r\nCc: \r\nSubject: Notre contact du 28/12/2011 - 192175092\r\nDate: Wed, 28 Dec 2011 13:29:17 +0100\r\n\r\n'), ')'] + + #resp = [('856 (UID 46936 BODYSTRUCTURE (("text" "plain" ("charset" "ISO-8859-1") NIL NIL "quoted-printable" 724 22 NIL NIL NIL NIL)("text" "html" ("charset" "ISO-8859-1") NIL NIL "quoted-printable" 2662 48 NIL NIL NIL NIL) "alternative" ("boundary" "----=_Part_1326887_254624357.1325083973970") NIL NIL NIL) BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)] {258}', 'Date: Wed, 28 Dec 2011 15:52:53 +0100 (CET)\r\nFrom: =?ISO-8859-1?Q?Malakoff_M=E9d=E9ric?= \r\nTo: Antoine Nguyen \r\nSubject: =?ISO-8859-1?Q?Votre_inscription_au_grand_Jeu_Malakoff_M=E9d=E9ric?=\r\n\r\n'), ')'] + + resp = [('856 (UID 11111 BODYSTRUCTURE ((("text" "plain" ("charset" "UTF-8") NIL NIL "7bit" 0 0 NIL NIL NIL NIL) "mixed" ("boundary" "----=_Part_407172_3159001.1321948277321") NIL NIL NIL)("application" "octet-stream" ("name" "26274308.pdf") NIL NIL "base64" 14906 NIL ("attachment" ("filename" "26274308.pdf")) NIL NIL) "mixed" ("boundary" "----=_Part_407171_9686991.1321948277321") NIL NIL NIL)'), ')'] + + print parse_fetch_response(resp) diff --git a/extensions/webmail/imaputils.py b/extensions/webmail/imaputils.py index 1750ac7f6..a29851b60 100644 --- a/extensions/webmail/imaputils.py +++ b/extensions/webmail/imaputils.py @@ -11,6 +11,7 @@ from modoboa.lib import parameters from modoboa.lib.connections import * from modoboa.lib.webutils import static_url from exceptions import ImapError +from fetch_parser import * #imaplib.Debug = 4 @@ -35,6 +36,86 @@ class capability(object): return wrapped_func +class BodyStructure(object): + """ + BODYSTRUCTURE response parser. + + Just a simple class that tries to distinguish content parts from + attachments. + """ + def __init__(self, definition=None): + self.is_multipart = False + self.contents = {} + self.attachments = [] + self.inlines = {} + + if definition is not None: + self.load_from_definition(definition) + + def __store_part(self, definition, pnum, multisubtype): + """Store the given message part in the appropriate category. + + This method sort parts in two categories: + + * contents (what is going to be displayed) + * attachments + + As there is no official definition about what is a content and + what is an attachment, the following rules are applied: + + * If the MIME type is text/plain or text/html: + + * If no previous part of this type has already been seen, it's a content + * Otherwise it's an attachment + + * Else, if the multipart subtype is related, we consider this + part as content because it is certainly an embedded image + + * Any other MIME type is considered as an attachment (for now) + + :param definition: a part definition (list) + :param prefix: the part's number + :param multisubtype: the multipart subtype + """ + pnum = "1" if pnum is None else pnum + params = dict(pnum=pnum, params=definition[2], cid=definition[3], + description=definition[4], encoding=definition[5], + size=definition[6]) + subtype = definition[1].lower() + mtype = "%s/%s" % (definition[0].lower(), subtype) + if mtype in ("text/plain", "text/html"): + if not self.contents.has_key(mtype): + self.contents[subtype] = params + return + elif multisubtype in ["related"]: + self.inlines[params["cid"].strip("<>")] = params + return + + if len(definition) > 7: + params.update(md5=definition[7], disposition=definition[8], + lang=definition[9], location=definition[10]) + self.attachments += [params] + + def load_from_definition(self, definition, multisubtype=None): + if type(definition) == dict: + struct = definition["struct"] + pnum = definition["partnum"] + elif type(definition[0]) == dict: + struct = definition[0]["struct"] + pnum = definition[0]["partnum"] + else: + struct = definition + pnum = None + + if type(struct[0]) == list: + for part in struct[0]: + self.load_from_definition(part, struct[1]) + return + + self.__store_part(struct, pnum, multisubtype) + + def has_attachments(self): + return len(self.attachments) class IMAPconnector(object): __metaclass__ = ConnectionsManager @@ -105,13 +186,14 @@ class IMAPconnector(object): for part in bodystruct[0]: nprefix = "%s" % cpt if prefix == "" else "%s.%d" % (prefix, cpt) index, encoding, size = \ - self.__find_content_in_bodystruct(bodystruct[0], mtype, stype, nprefix) - if index: + self.__find_content_in_bodystruct(part, mtype, stype, nprefix) + if index is not None: return (index, encoding, size) cpt += 1 - - if bodystruct[0] == mtype and bodystruct[1] == stype: - return ("1" if not len(prefix) else prefix, bodystruct[5], int(bodystruct[6])) + else: + if bodystruct[0].lower() == mtype and bodystruct[1].lower() == stype: + return ("1" if not len(prefix) else prefix, + bodystruct[5], int(bodystruct[6])) return (None, None, 0) def refresh(self, user, password): @@ -401,14 +483,10 @@ class IMAPconnector(object): 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 fetchpart(self, uid, mbox, partnum): + self.select_mailbox(mbox, False) + data = self._cmd("FETCH", uid, "(BODY[%s])" % partnum) + return data[int(uid)]["BODY[%s]" % partnum] def fetch(self, start, stop=None, mbox=None, **kwargs): """Retrieve information about messages from the server @@ -427,7 +505,7 @@ class IMAPconnector(object): else: submessages = [start] range = start - query = '(FLAGS BODY.PEEK[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])' + query = '(FLAGS BODYSTRUCTURE BODY.PEEK[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])' data = self._cmd("FETCH", range, query) result = [] for uid in submessages: @@ -437,10 +515,13 @@ class IMAPconnector(object): msg['class'] = 'unseen' if r'\Answered' in data[int(uid)]['FLAGS']: msg['img_flags'] = static_url('pics/answered.png') + bs = BodyStructure(data[int(uid)]['BODYSTRUCTURE']) + if bs.has_attachments(): + msg['img_withatts'] = static_url('pics/attachment.png') result += [msg] return result - def fetchmail(self, mbox, mailid, fmt="plain"): + def fetchmail(self, mbox, mailid): """Retrieve information about a specific message Issue a FETCH command to retrieve a message's content from the @@ -455,34 +536,15 @@ class IMAPconnector(object): """ self.select_mailbox(mbox, False) data = self._cmd("FETCH", mailid, "(BODYSTRUCTURE BODY.PEEK[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])") - content = data[int(mailid)]['BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)]'] - - fallback_fmt = "html" if fmt == "plain" else "plain" - (pnum, encoding, size) = (None, None, 0) - for f in [fmt, fallback_fmt]: - pnum, encoding, size = \ - self.__find_content_in_bodystruct(data[int(mailid)]['BODYSTRUCTURE'], - "text", f) - if pnum is not None and size: - break - if pnum is None: - return None - - data = self._cmd("FETCH", mailid, "(BODY.PEEK[%s])" % pnum) - - data = data[int(mailid)]['BODY[%s]' % pnum] - if encoding == "base64": - import base64 - content += base64.b64decode(data) - elif encoding == "quoted-printable": - import quopri - content += quopri.decodestring(data) - else: - content += data - - return email.message_from_string(content) + return data[int(mailid)] def get_imapconnector(request): imapc = IMAPconnector(user=request.user.username, password=request.session["password"]) return imapc + + +if __name__ == "__main__": + import doctest + doctest.testmod() + diff --git a/extensions/webmail/lib.py b/extensions/webmail/lib.py index 3cd6d3bfa..f420689d7 100644 --- a/extensions/webmail/lib.py +++ b/extensions/webmail/lib.py @@ -7,7 +7,6 @@ sys.setdefaultencoding("utf-8") import re import time import socket -import imaplib import ssl from datetime import datetime, timedelta import email @@ -31,12 +30,13 @@ class WMtable(tables.Table): select = tables.ImgColumn("select", cssclass="draggable left", width="1%", defvalue="%spics/grippy.png" % settings.MEDIA_URL, header="") - flags = tables.ImgColumn("flags", width="4%") + flags = tables.ImgColumn("flags", width="2%") + withatts = tables.ImgColumn("withatts", width="2%") subject = tables.Column("subject", label=__("Subject"), width="50%") from_ = tables.Column("from", width="20%", label=__("From")) date = tables.Column("date", width="20%", label=__("Date")) - cols_order = ["select", "flags", "subject", "from_", "date"] + cols_order = ["select", "withatts", "flags", "subject", "from_", "date"] def parse(self, header, value): try: @@ -148,12 +148,40 @@ class ImapListing(EmailListing): return ImapListing.computequota(self.mbc) class ImapEmail(Email): - def __init__(self, msg, user, dformat="DISPLAYMODE", addrfull=False, - *args, **kwargs): - mformat = parameters.get_user(user, "DISPLAYMODE") - dformat = parameters.get_user(user, dformat) - Email.__init__(self, msg, mformat, dformat, *args, **kwargs) + def __init__(self, mbox, mailid, request, dformat="DISPLAYMODE", addrfull=False, + links=0): + mformat = parameters.get_user(request.user, "DISPLAYMODE") + dformat = parameters.get_user(request.user, dformat) + self.headers = [] + self.attachments = {} + self.imapc = get_imapconnector(request) + msg = self.imapc.fetchmail(mbox, mailid) + self.mbox = mbox + self.mailid = mailid + headers = msg['BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)]'] + fallback_fmt = "html" if dformat == "plain" else "plain" + + self.bs = BodyStructure(msg['BODYSTRUCTURE']) + data = None + + mformat = dformat if self.bs.contents.has_key(dformat) else fallback_fmt + + pnum = self.bs.contents[mformat]['pnum'] + data = self.imapc._cmd("FETCH", mailid, "(BODY.PEEK[%s])" % pnum) + content = self._decode_content(self.bs.contents[mformat]['encoding'], + data[int(mailid)]['BODY[%s]' % pnum]) + charset = self._find_content_charset(mformat) + if charset is not None: + content = content.decode(charset) + + self._find_attachments() + self._fetch_inlines() + + self.body = \ + getattr(self, "viewmail_%s" % mformat)(content, links=links) + + msg = email.message_from_string(headers) fields = ["Subject", "From", "To", "Reply-To", "Cc", "Date"] for f in fields: label = f @@ -173,6 +201,68 @@ class ImapEmail(Email): except: pass + def _find_content_charset(self, subtype): + for pos, elem in enumerate(self.bs.contents[subtype]["params"]): + if elem == "charset": + return self.bs.contents[subtype]["params"][pos + 1] + return None + + def _decode_content(self, encoding, content): + encoding = encoding.lower() + if encoding == "base64": + import base64 + return base64.b64decode(content) + elif encoding == "quoted-printable": + import quopri + return quopri.decodestring(content) + return content + + def _find_attachments(self): + for att in self.bs.attachments: + attname = "part_%s" % att["pnum"] + params = None + key = None + if att.has_key("params") and att["params"] != "NIL": + params = att["params"] + key = "name" + + if key is None and \ + att.has_key("disposition") and len(att["disposition"]) > 1: + params = att["disposition"][1] + key = "filename" + + if key and params: + for pos, value in enumerate(params): + if value == key: + attname = u2u_decode.u2u_decode(params[pos + 1]).strip("\r\t\n") + break + self.attachments[att["pnum"]] = attname + + def _fetch_inlines(self): + for cid, params in self.bs.inlines.iteritems(): + if re.search("\.\.", cid): + continue + fname = "static/tmp/%s_%s" % (self.mailid, cid) + path = os.path.join(settings.MODOBOA_DIR, fname) + params["fname"] = "/%s" % fname + if os.path.exists(path): + continue + + content = self.imapc.fetchpart(self.mailid, self.mbox, params["pnum"]) + fp = open(path, "wb") + fp.write(self._decode_content(params["encoding"], content)) + fp.close() + + + def map_cid(self, url): + import re + + m = re.match(".*cid:(.+)", url) + if m: + if self.bs.inlines.has_key(m.group(1)): + return self.bs.inlines[m.group(1)]["fname"] + return url + def render_headers(self, **kwargs): from django.template.loader import render_to_string diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index 327e72c99..c9b0a1d0c 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -166,13 +166,13 @@ def getmailcontent(request): if mbox is None or mailid is None: raise WebmailError(_("Invalid request")) - content = fetchmail(request, mbox, mailid, True) + #content = fetchmail(request, mbox, mailid, True) # if "class" in msg.keys() and msg["class"] == "unseen": # IMAPconnector(user=request.user.username, # password=request.session["password"]).msg_read(folder, mail_id) - email = ImapEmail(content, request.user, links=request.GET["links"]) + email = ImapEmail(mbox, mailid, request, links=request.GET["links"]) return _render(request, "common/viewmail.html", { "headers" : email.render_headers(folder=mbox, mail_id=mailid), "folder" : mbox, "imapid" : mailid, "mailbody" : email.body @@ -659,10 +659,9 @@ def newindex(request): json = request.GET.get("json", False) if action is not None: - try: - response = globals()[action](request) - except KeyError: + if not globals().has_key(action): raise WebmailError(_("Undefined action")) + response = globals()[action](request) else: if json: raise WebmailError(_("Bad request")) diff --git a/lib/emailutils.py b/lib/emailutils.py index 2099734bd..3d7b3d820 100644 --- a/lib/emailutils.py +++ b/lib/emailutils.py @@ -166,7 +166,7 @@ class Email(object): def viewmail_html(self, content, **kwargs): if content is None or content == "": - return (False, "") + return "" links = kwargs.has_key("links") and kwargs["links"] or "0" html = lxml.html.fromstring(content) if links == "0": From 6e038da10450434a981c99a7b2a2b3a21e4429f5 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 1 Feb 2012 09:59:16 +0100 Subject: [PATCH 10/13] wip: webmail big refactoring Little issues due to the previous merge have been fixed. --- extensions/webmail/__init__.py | 4 +-- extensions/webmail/urls.py | 53 +++++++++++++++++----------------- extensions/webmail/views.py | 4 +-- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/extensions/webmail/__init__.py b/extensions/webmail/__init__.py index a3f2e3bc1..59fe71733 100644 --- a/extensions/webmail/__init__.py +++ b/extensions/webmail/__init__.py @@ -75,8 +75,6 @@ def infos(): @events.observe("UserMenuDisplay") def menu(target, user): - import views - if target != "top_menu": return [] if not user.has_mailbox: @@ -84,7 +82,7 @@ def menu(target, user): return [ {"name" : "webmail", "label" : _("Webmail"), - "url" : reverse(views.index), + "url" : reverse("modoboa.extensions.webmail.views.newindex"), "img" : static_url("pics/webmail.png")} ] diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py index 23b026016..d36d30fd7 100644 --- a/extensions/webmail/urls.py +++ b/extensions/webmail/urls.py @@ -5,30 +5,31 @@ from django.conf.urls.defaults import * # name matches one of the first entries (from "compose" to # "attachments"), the associated folder will not be accessible. # -urlpatterns = patterns('modoboa.extensions.webmail.views', - #(r'^$', "index"), - (r'^$', "newindex"), - (r'^submailboxes', "submailboxes"), - (r'^mailcontent', "getmailcontent"), +urlpatterns = patterns( + 'modoboa.extensions.webmail.views', + #(r'^$', "index"), + (r'^$', "newindex"), + (r'^submailboxes', "submailboxes"), + #(r'^mailcontent', "getmailcontent"), - -# (r'^compose/$', "compose"), - (r'^move/$', "move"), - (r'^mark/(?P.+)/$', "mark"), - (r'^empty/(?P.+)/$', "empty"), - (r'^compact/(?P.+)/$', "compact"), - (r'^newfolder/$', "newfolder"), - (r'^editfolder/$', 'editfolder'), - (r'^delfolder/$', 'delfolder'), - - (r'^attachments/$', 'attachments'), - (r'^delattachment/$', 'delattachment'), - - (r'^(?P.+)/(?P[\w\-\+]+)/part/$', 'getattachment'), - (r'^(?P.+)/(?P[\w\-\+]+)/delete/$', 'delete'), - #(r'^(?P.+)/(?P[\w\-\+]+)/content/$', 'getmailcontent'), - (r'^(?P.+)/(?P[\w\-\+]+)/forward/$', 'forward'), - (r'^(?P.+)/(?P[\w\-\+]+)/reply/$', 'reply'), -# (r'^(?P.+)/(?P[\w\-\+]+)/$', 'viewmail'), - (r'^(?P.+)/$', "folder"), - ) + + # (r'^compose/$', "compose"), + (r'^move/$', "move"), + (r'^mark/(?P.+)/$', "mark"), + (r'^empty/(?P.+)/$', "empty"), + (r'^compact/(?P.+)/$', "compact"), + (r'^newfolder/$', "newfolder"), + (r'^editfolder/$', 'editfolder'), + (r'^delfolder/$', 'delfolder'), + + (r'^attachments/$', 'attachments'), + (r'^delattachment/$', 'delattachment'), + + (r'^(?P.+)/(?P[\w\-\+]+)/part/$', 'getattachment'), + (r'^(?P.+)/(?P[\w\-\+]+)/delete/$', 'delete'), + #(r'^(?P.+)/(?P[\w\-\+]+)/content/$', 'getmailcontent'), + (r'^(?P.+)/(?P[\w\-\+]+)/forward/$', 'forward'), + (r'^(?P.+)/(?P[\w\-\+]+)/reply/$', 'reply'), + # (r'^(?P.+)/(?P[\w\-\+]+)/$', 'viewmail'), + (r'^(?P.+)/$', "folder"), + ) diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index 11025ff08..c7f2c9cf3 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -621,14 +621,14 @@ def viewmail(request): return dict(listing=content, menuargs=dict(mail_id=mailid)) @login_required -@is_not_localadmin() +@needs_mailbox() def submailboxes(request): topmailbox = request.GET.get('topmailbox', '') mboxes = get_imapconnector(request).getfolders(request.user, topmailbox) return ajax_simple_response(dict(status="ok", mboxes=mboxes)) @login_required -@is_not_localadmin() +@needs_mailbox() def newindex(request): """Webmail actions handler From 7bd67eb55ac4432fe28ede46bb0650db05cfb78e Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 2 Feb 2012 14:30:12 +0100 Subject: [PATCH 11/13] Restoring lost modifications during the last merge... --- extensions/webmail/static/js/webmail.js | 11 +++++----- .../webmail/templates/webmail/index.html | 8 +++---- extensions/webmail/urls.py | 3 +-- extensions/webmail/views.py | 21 ++++++++++++++++++- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/extensions/webmail/static/js/webmail.js b/extensions/webmail/static/js/webmail.js index dc3b3ee0e..7072210e3 100644 --- a/extensions/webmail/static/js/webmail.js +++ b/extensions/webmail/static/js/webmail.js @@ -544,11 +544,10 @@ var Webmail = new Class({ //wm_updatelisting(resp); this.page_update(resp); - window.addEvent("resize", this.resize_window_callback.bind(this)); - var editormode = resp.editor; if (editormode == "html") { + window.addEvent("resize", this.resize_window_callback.bind(this)); var instance = CKEDITOR.instances[editorid]; if (instance) { CKEDITOR.remove(instance); @@ -584,12 +583,14 @@ var Webmail = new Class({ * Callback of the 'sendmail' action. */ sendmail_callback: function(evt) { + var link = evt.target; + evt.stop(); infobox.show(gettext("Sending..."), { profile : "gray", spinner : true }); - disable_link(this); + disable_link(link); $("composemail").set("send", { onSuccess: function(resp) { resp = JSON.decode(resp); @@ -603,10 +604,10 @@ var Webmail = new Class({ } current_anchor.parse_string(resp.url, true).setparams(navparams); current_anchor.update(); - this.eliminate("editormode"); + link.eliminate("editormode"); }.bind(this) }); - if (this.retrieve("editormode") == "html") { + if (link.retrieve("editormode") == "html") { CKEDITOR.instances[editorid].updateElement(); } $("composemail").send(); diff --git a/extensions/webmail/templates/webmail/index.html b/extensions/webmail/templates/webmail/index.html index 3fe6ae9fa..5931fc72b 100644 --- a/extensions/webmail/templates/webmail/index.html +++ b/extensions/webmail/templates/webmail/index.html @@ -74,10 +74,10 @@ function wm_updatelisting(resp) { 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 %}", - submboxes_url: "{% url extensions.webmail.views.submailboxes %}" + modify_url: "{% url modoboa.extensions.webmail.views.editfolder %}", + delete_url: "{% url modoboa.extensions.webmail.views.delfolder %}", + move_url: "{% url modoboa.extensions.webmail.views.move %}", + submboxes_url: "{% url modoboa.extensions.webmail.views.submailboxes %}" }); current_anchor.register_callback("folder", function(resp) { diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py index d36d30fd7..58bda3e51 100644 --- a/extensions/webmail/urls.py +++ b/extensions/webmail/urls.py @@ -10,8 +10,7 @@ urlpatterns = patterns( #(r'^$', "index"), (r'^$', "newindex"), (r'^submailboxes', "submailboxes"), - #(r'^mailcontent', "getmailcontent"), - + (r'^getmailcontent', 'getmailcontent'), # (r'^compose/$', "compose"), (r'^move/$', "move"), diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index c7f2c9cf3..007f294a1 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -157,6 +157,7 @@ def fetchmail(request, mbox, mailid, all=False): # menu=menu, content=content)) # return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") + @login_required @needs_mailbox() def viewmail(request, folder, mail_id=None): @@ -174,6 +175,8 @@ def viewmail(request, folder, mail_id=None): "folder" : mbox, "imapid" : mailid, "mailbody" : email.body }) + + @login_required @needs_mailbox() def getattachment(request, folder, mail_id): @@ -602,9 +605,25 @@ def render_compose(request, form, posturl, email=None, insert_signature=False): return ctx def compose(request): + if request.method == "POST": + return send_mail(request) + form = ComposeMailForm() form.fields["from_"].initial = request.user.username - return render_compose(request, form, "compose", insert_signature=True) + return render_compose(request, form, "?action=compose", insert_signature=True) + +@login_required +@needs_mailbox() +def getmailcontent(request): + mbox = request.GET.get("mbox", None) + mailid = request.GET.get("mailid", None) + if mbox is None or mailid is None: + raise WebmailError(_("Invalid request")) + email = ImapEmail(mbox, mailid, request, links=request.GET["links"]) + return _render(request, "common/viewmail.html", { + "headers" : email.render_headers(folder=mbox, mail_id=mailid), + "folder" : mbox, "imapid" : mailid, "mailbody" : email.body + }) def viewmail(request): mailid = request.GET.get("mailid", None) From cf124df0594f38c16e8633d5718446518e80c4d1 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 3 Feb 2012 15:26:00 +0100 Subject: [PATCH 12/13] wip: Big webmail refactoring. --- extensions/webmail/forms.py | 2 + extensions/webmail/imaputils.py | 25 +++- extensions/webmail/lib.py | 139 +++++++++++------- extensions/webmail/static/js/webmail.js | 26 ++-- .../webmail/templates/webmail/compose.html | 2 +- extensions/webmail/templatetags/webextras.py | 8 +- extensions/webmail/urls.py | 2 +- extensions/webmail/views.py | 112 +++++++++----- templates/common/genericform.html | 5 +- 9 files changed, 201 insertions(+), 120 deletions(-) diff --git a/extensions/webmail/forms.py b/extensions/webmail/forms.py index 5bab1fc0b..a1ec4f000 100644 --- a/extensions/webmail/forms.py +++ b/extensions/webmail/forms.py @@ -7,6 +7,8 @@ class ComposeMailForm(forms.Form): cc = forms.CharField(label=_("Cc"), widget=forms.Textarea(), required=False) subject = forms.CharField(label=_("Subject"), max_length=255, required=False) + origmsgid = forms.CharField(widget=forms.HiddenInput(), required=False) + class FolderForm(forms.Form): name = forms.CharField() diff --git a/extensions/webmail/imaputils.py b/extensions/webmail/imaputils.py index a29851b60..e231ec01e 100644 --- a/extensions/webmail/imaputils.py +++ b/extensions/webmail/imaputils.py @@ -5,6 +5,7 @@ """ import imaplib, ssl, email import re +import time from functools import wraps from imapclient.response_parser import parse_fetch_response from modoboa.lib import parameters @@ -410,11 +411,12 @@ class IMAPconnector(object): """ self._add_flag(mbox, msgset, r'(\Seen)') - def msgforwarded(self, folder, imapid): - self._add_flag(folder, imapid, '($Forwarded)') + def msgforwarded(self, mailbox, mailid): + self._add_flag(mailbox, mailid, '($Forwarded)') - def msg_answered(self, folder, imapid): - self._add_flag(folder, imapid, r'(\Answered)') + def msg_answered(self, mailbox, mailid): + """Add the \Answered flag to this email""" + self._add_flag(mailbox, mailid, r'(\Answered)') def move(self, msgset, oldmailbox, newmailbox): """Move messages between mailboxes @@ -521,7 +523,7 @@ class IMAPconnector(object): result += [msg] return result - def fetchmail(self, mbox, mailid): + def fetchmail(self, mbox, mailid, readonly=True, headers=None): """Retrieve information about a specific message Issue a FETCH command to retrieve a message's content from the @@ -532,10 +534,17 @@ class IMAPconnector(object): :param mbox: the mailbox containing the message :param mailid: the message's unique id - :param fmt: the desired content's format + :param readonly: + :param extraheaders: """ - self.select_mailbox(mbox, False) - data = self._cmd("FETCH", mailid, "(BODYSTRUCTURE BODY.PEEK[HEADER.FIELDS (DATE FROM TO CC SUBJECT)])") + self.select_mailbox(mbox, readonly) + if headers is None: + headers = ['DATE', 'FROM', 'TO', 'CC', 'SUBJECT'] + bcmd = "BODY.PEEK" if readonly else "BODY" + data = self._cmd( + "FETCH", mailid, + "(BODYSTRUCTURE %s[HEADER.FIELDS (%s)])" % (bcmd, " ".join(headers)) + ) return data[int(mailid)] def get_imapconnector(request): diff --git a/extensions/webmail/lib.py b/extensions/webmail/lib.py index 819736592..2b9556049 100644 --- a/extensions/webmail/lib.py +++ b/extensions/webmail/lib.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# coding: utf-8 import sys, os reload(sys) @@ -92,10 +92,16 @@ class IMAPheader(object): except ValueError: return value + @staticmethod + def parse_message_id(value, **kwargs): + return value.strip('\n') @staticmethod def parse_subject(value, **kwargs): - return decode(u2u_decode.u2u_decode(value)) + try: + return decode(u2u_decode.u2u_decode(value)) + except UnicodeDecodeError: + return value class ImapListing(EmailListing): tpl = "webmail/index.html" @@ -148,24 +154,33 @@ class ImapListing(EmailListing): return ImapListing.computequota(self.mbc) class ImapEmail(Email): + headernames = [ + ('Date', True), + ('From', True), + ('To', True), + ('Cc', True), + ('Subject', True), + ] + def __init__(self, mbox, mailid, request, dformat="DISPLAYMODE", addrfull=False, links=0): mformat = parameters.get_user(request.user, "DISPLAYMODE") - dformat = parameters.get_user(request.user, dformat) + self.dformat = parameters.get_user(request.user, dformat) self.headers = [] self.attachments = {} self.imapc = get_imapconnector(request) - msg = self.imapc.fetchmail(mbox, mailid) + msg = self.imapc.fetchmail(mbox, mailid, readonly=False, + headers=self.headers_as_list) self.mbox = mbox self.mailid = mailid - headers = msg['BODY[HEADER.FIELDS (DATE FROM TO CC SUBJECT)]'] - fallback_fmt = "html" if dformat == "plain" else "plain" + headers = msg['BODY[HEADER.FIELDS (%s)]' % self.headers_as_text] + fallback_fmt = "html" if self.dformat == "plain" else "plain" self.bs = BodyStructure(msg['BODYSTRUCTURE']) data = None - mformat = dformat if self.bs.contents.has_key(dformat) else fallback_fmt + mformat = self.dformat if self.bs.contents.has_key(self.dformat) else fallback_fmt pnum = self.bs.contents[mformat]['pnum'] data = self.imapc._cmd("FETCH", mailid, "(BODY.PEEK[%s])" % pnum) @@ -182,25 +197,34 @@ class ImapEmail(Email): getattr(self, "viewmail_%s" % mformat)(content, links=links) msg = email.message_from_string(headers) - fields = ["Subject", "From", "To", "Reply-To", "Cc", "Date"] - for f in fields: - label = f - if not f in msg.keys(): - f = f.upper() - if not f in msg.keys(): + for hdr in self.headernames: + label = hdr[0] + name = hdr[0] + if not name in msg.keys(): + name = name.upper() + if not name in msg.keys(): continue try: - key = re.sub("-", "_", f).lower() - value = getattr(IMAPheader, "parse_%s" % key)(msg[f], full=addrfull) - self.headers += [{"name" : label, "value" : value}] + key = re.sub("-", "_", name).lower() + value = getattr(IMAPheader, "parse_%s" % key)(msg[name], full=addrfull) except AttributeError: - self.headers += [{"name" : label, "value" : msg[f]}] + value = msg[name] + if hdr[1]: + self.headers += [{"name" : label, "value" : value}] try: label = re.sub("-", "_", label) setattr(self, label, value) except: pass + @property + def headers_as_list(self): + return map(lambda hdr: hdr[0].upper(), self.headernames) + + @property + def headers_as_text(self): + return " ".join(map(lambda hdr: hdr[0].upper(), self.headernames)) + def _find_content_charset(self, subtype): for pos, elem in enumerate(self.bs.contents[subtype]["params"]): if elem == "charset": @@ -273,26 +297,45 @@ class ImapEmail(Email): }) return res -class ReplyModifier(ImapEmail): - def __init__(self, msg, user, form, all=False, **kwargs): - ImapEmail.__init__(self, msg, user, dformat="EDITOR", **kwargs) - - self.textheader = "%s %s" % (self.From, _("wrote:")) +class Modifier(ImapEmail): + def __init__(self, *args, **kwargs): + super(Modifier, self).__init__(*args, **kwargs) getattr(self, "_modify_%s" % self.dformat)() - form.fields["from_"].initial = user.username - if not "Reply-To" in msg.keys(): + def _modify_plain(self): + self.body = re.sub("", "", self.body) + + def _modify_html(self): + pass + + +class ReplyModifier(Modifier): + headernames = ImapEmail.headernames + \ + [("Reply-To", True), + ("Message-ID", False)] + + def __init__(self, mbox, mailid, request, form, **kwargs): + super(ReplyModifier, self).__init__( + mbox, mailid, request, dformat="EDITOR", **kwargs + ) + + self.textheader = "%s %s" % (self.From, _("wrote:")) + + form.fields["from_"].initial = request.user.username + if hasattr(self, "Message_ID"): + form.fields["origmsgid"].initial = self.Message_ID + if not hasattr(self, "Reply_To"): form.fields["to"].initial = self.From else: form.fields["to"].initial = self.Reply_To - if all: + if all: # reply-all form.fields["cc"].initial = "" - toparse = msg["To"].split(",") - if "Cc" in msg.keys(): - toparse += msg["Cc"].split(",") + toparse = self.To.split(",") + if hasattr(self, 'Cc'): + toparse += self.Cc.split(",") for addr in toparse: tmp = EmailAddress(addr) - if tmp.address and tmp.address == user.username: + if tmp.address and tmp.address == request.user.username: continue if form.fields["cc"].initial != "": form.fields["cc"].initial += ", " @@ -302,9 +345,9 @@ class ReplyModifier(ImapEmail): form.fields["subject"].initial = self.Subject else: form.fields["subject"].initial = "Re: %s" % self.Subject - + def _modify_plain(self): - self.body = re.sub("", "", self.body) + super(ReplyModifier, self)._modify_plain() lines = self.body.split('\n') body = "" for l in lines: @@ -312,27 +355,26 @@ class ReplyModifier(ImapEmail): body += "\n" body += ">%s" % l self.body = body - - def _modify_html(self): - pass - -class ForwardModifier(ImapEmail): - def __init__(self, msg, user, form, **kwargs): - ImapEmail.__init__(self, msg, user, dformat="EDITOR", **kwargs) + +class ForwardModifier(Modifier): + def __init__(self, mbox, mailid, request, form, **kwargs): + super(ForwardModifier, self).__init__( + mbox, mailid, request, dformat="EDITOR", **kwargs + ) - self._header(msg) - form.fields["from_"].initial = user.username + self._header() + form.fields["from_"].initial = request.user.username form.fields["subject"].initial = "Fwd: %s" % self.Subject def __getfunc(self, name): return getattr(self, "%s_%s" % (name, self.dformat)) - def _header(self, msg): + def _header(self): self.textheader = self.__getfunc("_header_begin")() + "\n" self.textheader += \ self.__getfunc("_header_line")(_("Subject"), self.Subject) + "\n" self.textheader += \ - self.__getfunc("_header_line")(_("Date"), msg["Date"]) + "\n" + self.__getfunc("_header_line")(_("Date"), self.Date) + "\n" for hdr in ["From", "To", "Reply-To"]: try: key = re.sub("-", "_", hdr) @@ -528,7 +570,7 @@ def create_mail_attachment(attdef): res.add_header("Content-Disposition", "attachment; filename='%s'" % attdef["fname"]) return res -def send_mail(request, origmsg=None, posturl=None): +def send_mail(request, posturl=None): """Email verification and sending. If the form does not present any error, a new MIME message is @@ -536,7 +578,6 @@ def send_mail(request, origmsg=None, posturl=None): SMTP server and the message is finally sent. :param request: a Request object - :param origmsg: the eventual original message (in case of a reply) :param posturl: the url to post the message form to :return: a 2-uple (True|False, HttpResponse) """ @@ -589,8 +630,8 @@ def send_mail(request, origmsg=None, posturl=None): msg["Message-ID"] = make_msgid() msg["User-Agent"] = "Modoboa" msg["Date"] = formatdate(time.time(), True) - if origmsg and origmsg.has_key("Message-ID"): - msg["References"] = msg["In-Reply-To"] = origmsg["Message-ID"] + if form.cleaned_data.has_key("origmsgid"): + msg["References"] = msg["In-Reply-To"] = form.cleaned_data["origmsgid"] rcpts = msg['To'].split(',') if form.cleaned_data["cc"] != "": msg["Cc"] = form.cleaned_data["cc"] @@ -621,16 +662,14 @@ def send_mail(request, origmsg=None, posturl=None): password=request.session["password"]).push_mail(sentfolder, msg) clean_attachments(request.session["compose_mail"]["attachments"]) del request.session["compose_mail"] - return True, ajax_simple_response(getctx("ok", url=get_current_url(request))) + return True, dict(url=get_current_url(request)) listing = _render_to_string(request, "webmail/compose.html", {"form" : form, "noerrors" : True, "body" : request.POST["id_body"].strip(), "posturl" : posturl}) error = _("Red fields are mandatories") - return False, ajax_simple_response( - getctx("ko", level=2, respmsg=error, listing=listing, editor=editormode) - ) + return False, dict(status="ko", respmsg=error, listing=listing, editor=editormode) class AttachmentUploadHandler(FileUploadHandler): """ diff --git a/extensions/webmail/static/js/webmail.js b/extensions/webmail/static/js/webmail.js index 32cdec3c5..5f57c1073 100644 --- a/extensions/webmail/static/js/webmail.js +++ b/extensions/webmail/static/js/webmail.js @@ -473,7 +473,7 @@ var Webmail = new Class({ //$$("a[name=back]").addEvent("click", this._listmailbox_loader); $$("a[name=reply]").addEvent("click", this.reply_loader); - $$("a[name=replyall]").addEvent("click", this.replyall_loader); + $$("a[name=replyall]").addEvent("click", this.reply_loader); $$("a[name=forward]").addEvent("click", this.forward_loader); $$("a[name=delete]").addEvent("click", function(event) { var lnk = event.target; @@ -504,18 +504,9 @@ var Webmail = new Class({ * Loader of the 'reply' action (called when the associated button * is clicked). */ - reply_loader: function(event) { + reply_loader: function(event, all) { 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"; + current_anchor.reset().updateparams(this.get("href")).update(); }, /* @@ -524,7 +515,7 @@ var Webmail = new Class({ */ forward_loader: function(event) { event.stop(); - location.hash += event.target.get("href"); + current_anchor.reset().updateparams(this.get("href")).update(); }, /* @@ -598,12 +589,15 @@ var Webmail = new Class({ current_anchor.get_callback("compose")(resp); if ($defined(resp.respmsg)) { infobox.error(resp.respmsg); + } else { + infobox.hide(); } - enable_link(this, sendmail_callback); + enable_link(link, sendmail_callback); return; } - current_anchor.parse_string(resp.url, true).setparams(navparams); - current_anchor.update(); + history.go(-2); + infobox.info(gettext("Done")); + infobox.hide(1); link.eliminate("editormode"); }.bind(this) }); diff --git a/extensions/webmail/templates/webmail/compose.html b/extensions/webmail/templates/webmail/compose.html index 204d8b821..5091355d8 100644 --- a/extensions/webmail/templates/webmail/compose.html +++ b/extensions/webmail/templates/webmail/compose.html @@ -19,7 +19,7 @@ {% if body %} + {% else %} {% endif %} diff --git a/extensions/webmail/templatetags/webextras.py b/extensions/webmail/templatetags/webextras.py index 7022acd7f..0a27bdbf6 100644 --- a/extensions/webmail/templatetags/webextras.py +++ b/extensions/webmail/templatetags/webextras.py @@ -18,20 +18,20 @@ def viewmail_menu(selection, folder, user, mail_id=None): "img" : static_url("pics/back.png"), "label" : _("Back")}, {"name" : "reply", - "url" : "reply/", + "url" : "action=reply&mbox=%s&mailid=%s" % (folder, mail_id), "img" : static_url("pics/reply.png"), "label" : _("Reply")}, {"name" : "replyall", - "url" : "reply/", + "url" : "action=reply&mbox=%s&mailid=%s&all=1" % (folder, mail_id), "img" : static_url("pics/reply-all.png"), "label" : _("Reply all")}, {"name" : "forward", - "url" : "forward/", + "url" : "action=forward&mbox=%s&mailid=%s" % (folder, mail_id), "img" : static_url("pics/alias.png"), "label" : _("Forward")}, {"name" : "delete", "img" : static_url("pics/remove.png"), - "url" : reverse(webmail.views.delete, args=[folder, mail_id]), + "url" : "action=delete&mbox=%s&mailid=%s" % (folder, mail_id), "label" : _("Delete")}, {"name" : "display_options", "label" : _("Display options"), diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py index 58bda3e51..17a146d4d 100644 --- a/extensions/webmail/urls.py +++ b/extensions/webmail/urls.py @@ -28,7 +28,7 @@ urlpatterns = patterns( (r'^(?P.+)/(?P[\w\-\+]+)/delete/$', 'delete'), #(r'^(?P.+)/(?P[\w\-\+]+)/content/$', 'getmailcontent'), (r'^(?P.+)/(?P[\w\-\+]+)/forward/$', 'forward'), - (r'^(?P.+)/(?P[\w\-\+]+)/reply/$', 'reply'), +# (r'^(?P.+)/(?P[\w\-\+]+)/reply/$', 'reply'), # (r'^(?P.+)/(?P[\w\-\+]+)/$', 'viewmail'), (r'^(?P.+)/$', "folder"), ) diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index 007f294a1..ee551b267 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -320,42 +320,42 @@ def compact(request, name): # form.fields["from_"].initial = request.user.username # return render_compose(request, form, reverse(compose), insert_signature=True) -@login_required -@needs_mailbox() -def reply(request, folder, mail_id): - msg = fetchmail(request, folder, mail_id, True) - if request.method == "POST": - status, resp = send_mail(request, origmsg=msg, - posturl=reverse(reply, args=[folder, mail_id])) - if status: - IMAPconnector(user=request.user.username, - password=request.session["password"]).msg_answered(folder, - mail_id) - return resp +# @login_required +# @needs_mailbox() +# def reply(request, folder, mail_id): +# msg = fetchmail(request, folder, mail_id, True) +# if request.method == "POST": +# status, resp = send_mail(request, origmsg=msg, +# posturl=reverse(reply, args=[folder, mail_id])) +# if status: +# IMAPconnector(user=request.user.username, +# password=request.session["password"]).msg_answered(folder, +# mail_id) +# return resp - form = ComposeMailForm() - email = ReplyModifier(msg, request.user, form, request.GET.has_key("all"), - addrfull=True, links="1") - return render_compose(request, form, reverse(reply, args=[folder, mail_id]), - email) +# form = ComposeMailForm() +# email = ReplyModifier(msg, request.user, form, request.GET.has_key("all"), +# addrfull=True, links="1") +# return render_compose(request, form, reverse(reply, args=[folder, mail_id]), +# email) -@login_required -@needs_mailbox() -def forward(request, folder, mail_id): - if request.method == "POST": - status, response = send_mail(request, - posturl=reverse(forward, args=[folder, mail_id])) - if status: - IMAPconnector(user=request.user.username, - password=request.session["password"]).msgforwarded(folder, - mail_id) - return response +# @login_required +# @needs_mailbox() +# def forward(request, folder, mail_id): +# if request.method == "POST": +# status, response = send_mail(request, +# posturl=reverse(forward, args=[folder, mail_id])) +# if status: +# IMAPconnector(user=request.user.username, +# password=request.session["password"]).msgforwarded(folder, +# mail_id) +# return response - msg = fetchmail(request, folder, mail_id, True) - form = ComposeMailForm() - email = ForwardModifier(msg, request.user, form, addrfull=True, links="1") - return render_compose(request, form, reverse(forward, args=[folder, mail_id]), - email) +# msg = fetchmail(request, folder, mail_id, True) +# form = ComposeMailForm() +# email = ForwardModifier(msg, request.user, form, addrfull=True, links="1") +# return render_compose(request, form, reverse(forward, args=[folder, mail_id]), +# email) def separate_folder(fullname, sep="."): """Split a folder name @@ -605,12 +605,43 @@ def render_compose(request, form, posturl, email=None, insert_signature=False): return ctx def compose(request): + url = "?action=compose" if request.method == "POST": - return send_mail(request) + status, resp = send_mail(request, posturl=url) + return resp form = ComposeMailForm() form.fields["from_"].initial = request.user.username - return render_compose(request, form, "?action=compose", insert_signature=True) + return render_compose(request, form, url, insert_signature=True) + +def compose_and_send(request, action, callback=None): + mbox = request.GET.get("mbox", None) + mailid = request.GET.get("mailid", None) + if mbox is None or mailid is None: + raise WebmailError(_("Bad request")) + url = "?action=%s&mbox=%s&mailid=%s" % (action, mbox, mailid) + if request.method == "POST": + status, resp = send_mail(request, url) + if status and callback: + callback(mbox, mailid) + return resp + + form = ComposeMailForm() + modclass = globals()["%sModifier" % action.capitalize()] + email = modclass(mbox, mailid, request, form, addrfull=True, links="1") + return render_compose(request, form, url, email) + +def reply(request): + def msg_replied(mbox, mailid): + get_imapconnector(request).msg_answered(mbox, mailid) + + return compose_and_send(request, "reply", msg_replied) + +def forward(request): + def msg_forwarded(mbox, mailid): + get_imapconnector(request).msg_forwarded(mbox, mailid) + + return compose_and_send(request, "reply", msg_forwarded) @login_required @needs_mailbox() @@ -671,20 +702,19 @@ def newindex(request): """ action = request.GET.get("action", None) - json = request.GET.get("json", False) if action is not None: if not globals().has_key(action): raise WebmailError(_("Undefined action")) response = globals()[action](request) else: - if json: + if request.is_ajax(): raise WebmailError(_("Bad request")) response = dict(deflocation="?action=listmailbox", defcallback="wm_updatelisting") curmbox = request.session.get("mbox", "INBOX") - if not json: + if not request.is_ajax(): request.session["lastaction"] = None imapc = get_imapconnector(request) response["refreshrate"] = \ @@ -692,6 +722,8 @@ def newindex(request): response["mboxes"] = render_mboxes_list(request, imapc) return _render(request, "webmail/index.html", response) + if action in ["reply", "forward"]: + action = "compose" if request.session["lastaction"] != action: extra_args = {} if response.has_key("menuargs"): @@ -703,5 +735,7 @@ def newindex(request): except KeyError: pass - response.update(status="ok", callback=action) + response.update(callback=action) + if not response.has_key("status"): + response.update(status="ok") return ajax_simple_response(response) diff --git a/templates/common/genericform.html b/templates/common/genericform.html index 13e60edf2..47170e93d 100644 --- a/templates/common/genericform.html +++ b/templates/common/genericform.html @@ -1,5 +1,8 @@ {% load i18n %} -{% for field in form %} +{% for field in form.hidden_fields %} +{{ field }} +{% endfor %} +{% for field in form.visible_fields %}
    {% if field.help_text %}
    From 8b52b32a7ec8a499281fbf32f87be20994645037 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 19 Mar 2012 15:18:36 +0100 Subject: [PATCH 13/13] wip: Big webmail refactoring --- extensions/webmail/imaputils.py | 22 ++- extensions/webmail/static/js/webmail.js | 7 +- .../webmail/templates/webmail/index.html | 2 +- extensions/webmail/templatetags/webextras.py | 2 +- extensions/webmail/urls.py | 3 +- extensions/webmail/views.py | 172 ++---------------- 6 files changed, 38 insertions(+), 170 deletions(-) diff --git a/extensions/webmail/imaputils.py b/extensions/webmail/imaputils.py index e231ec01e..cec5e83ab 100644 --- a/extensions/webmail/imaputils.py +++ b/extensions/webmail/imaputils.py @@ -470,20 +470,20 @@ class IMAPconnector(object): raise WebmailError(data[0]) return True - def getquota(self, folder): + def getquota(self, mailbox): 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)) + data = self._cmd("GETQUOTAROOT", self._encodefolder(mailbox)) + print data + 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, mbox, partnum): self.select_mailbox(mbox, False) @@ -517,6 +517,8 @@ class IMAPconnector(object): msg['class'] = 'unseen' if r'\Answered' in data[int(uid)]['FLAGS']: msg['img_flags'] = static_url('pics/answered.png') + if r'$Forwarded' in data[int(uid)]['FLAGS']: + msg['img_flags'] = static_url('pics/forwarded.png') bs = BodyStructure(data[int(uid)]['BODYSTRUCTURE']) if bs.has_attachments(): msg['img_withatts'] = static_url('pics/attachment.png') diff --git a/extensions/webmail/static/js/webmail.js b/extensions/webmail/static/js/webmail.js index 5f57c1073..6ec536852 100644 --- a/extensions/webmail/static/js/webmail.js +++ b/extensions/webmail/static/js/webmail.js @@ -23,6 +23,7 @@ var Webmail = new Class({ this.init_folders_buttons(); this.init_folders_browsing(); + this.init_quota_bar(); /* FIXME: à faire uniquement pour le mode pushState */ /*this.init_emails_table();*/ @@ -35,6 +36,7 @@ var Webmail = new Class({ 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)); + current_anchor.register_callback("delete", this.delete_callback); this.gspinner = new Spinner("document.body", { message: gettext("Loading, please wait..."), @@ -478,7 +480,9 @@ var Webmail = new Class({ $$("a[name=delete]").addEvent("click", function(event) { var lnk = event.target; event.stop(); - this.simple_request(lnk.get("href")); + this.simple_request(lnk.get("href"), undefined, function(resp) { + history.go(-1); + }); }.bind(this)); $$("a[name=activate_links]").addEvent("click", function(evt) { evt.stop(); @@ -620,7 +624,6 @@ var Webmail = new Class({ profile: "gray", spinner: true }); - params["json"] = "1"; new Request.JSON({ url : url, onSuccess : function(response) { diff --git a/extensions/webmail/templates/webmail/index.html b/extensions/webmail/templates/webmail/index.html index 5931fc72b..3f1208fc6 100644 --- a/extensions/webmail/templates/webmail/index.html +++ b/extensions/webmail/templates/webmail/index.html @@ -225,6 +225,6 @@ function wm_updatelisting(resp) { {% block menu %}{{ menu }}{% endblock %} {% block footer %}
    {% trans "Mailbox quota" %}
    -
    +
    {{ quota }}
    {{ block.super }} {% endblock %} diff --git a/extensions/webmail/templatetags/webextras.py b/extensions/webmail/templatetags/webextras.py index 0a27bdbf6..43a03a12a 100644 --- a/extensions/webmail/templatetags/webextras.py +++ b/extensions/webmail/templatetags/webextras.py @@ -31,7 +31,7 @@ def viewmail_menu(selection, folder, user, mail_id=None): "label" : _("Forward")}, {"name" : "delete", "img" : static_url("pics/remove.png"), - "url" : "action=delete&mbox=%s&mailid=%s" % (folder, mail_id), + "url" : reverse(webmail.views.delete) + "?mbox=%s&mailid=%s" % (folder, mail_id), "label" : _("Delete")}, {"name" : "display_options", "label" : _("Display options"), diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py index 17a146d4d..64a27c006 100644 --- a/extensions/webmail/urls.py +++ b/extensions/webmail/urls.py @@ -13,6 +13,7 @@ urlpatterns = patterns( (r'^getmailcontent', 'getmailcontent'), # (r'^compose/$', "compose"), + (r'^delete/$', 'delete'), (r'^move/$', "move"), (r'^mark/(?P.+)/$', "mark"), (r'^empty/(?P.+)/$', "empty"), @@ -25,7 +26,7 @@ urlpatterns = patterns( (r'^delattachment/$', 'delattachment'), (r'^(?P.+)/(?P[\w\-\+]+)/part/$', 'getattachment'), - (r'^(?P.+)/(?P[\w\-\+]+)/delete/$', 'delete'), + #(r'^(?P.+)/(?P[\w\-\+]+)/delete/$', 'delete'), #(r'^(?P.+)/(?P[\w\-\+]+)/content/$', 'getmailcontent'), (r'^(?P.+)/(?P[\w\-\+]+)/forward/$', 'forward'), # (r'^(?P.+)/(?P[\w\-\+]+)/reply/$', 'reply'), diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index ee551b267..604d1dac0 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -123,60 +123,6 @@ def index(request): return _render_error(request, user_context={"error" : exp}) return lst.render(request) -def fetchmail(request, mbox, mailid, all=False): - imapc = get_imapconnector(request) - res = imapc.fetchmail(mbox, mailid) - - return res - # 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 - -# @login_required -# @is_not_localadmin() -# def viewmail(request, folder, mail_id=None): -# from templatetags.webextras import viewm_menu - -# if request.GET.has_key("links"): -# links = int(request.GET["links"]) -# else: -# links = parameters.get_user(request.user, "ENABLE_LINKS") == "yes" and 1 or 0 -# url = reverse(getmailcontent, args=[folder, mail_id]) + ("?links=%d" % links) -# content = Template(""" -# -# """).render(Context({"url" : url})) -# menu = viewm_menu("", get_current_url(request), folder, mail_id, -# request.user.get_all_permissions()) -# mbc = IMAPconnector(user=request.user.username, -# password=request.session["password"]) -# ctx = getctx("ok", **__render_common_components(request, folder, -# menu=menu, content=content)) -# return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") - - -@login_required -@needs_mailbox() -def viewmail(request, folder, mail_id=None): - from templatetags.webextras import viewm_menu - - #content = fetchmail(request, mbox, mailid, True) - - # if "class" in msg.keys() and msg["class"] == "unseen": - # IMAPconnector(user=request.user.username, - # password=request.session["password"]).msg_read(folder, mail_id) - - email = ImapEmail(mbox, mailid, request, links=request.GET["links"]) - return _render(request, "common/viewmail.html", { - "headers" : email.render_headers(folder=mbox, mail_id=mailid), - "folder" : mbox, "imapid" : mailid, "mailbody" : email.body - }) - - - @login_required @needs_mailbox() def getattachment(request, folder, mail_id): @@ -226,12 +172,15 @@ def move(request): @login_required @needs_mailbox() -def delete(request, fdname, mail_id): - mbc = IMAPconnector(user=request.user.username, - password=request.session["password"]) - mbc.move(mail_id, fdname, parameters.get_user(request.user, "TRASH_FOLDER")) - ctx = getctx("ok", next=get_current_url(request)) - return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") +def delete(request): + mbox = request.GET.get("mbox", None) + mailid = request.GET.get("mailid", None) + if mbox is None or mailid is None: + raise WebmailError(_("Invalid request")) + mbc = get_imapconnector(request) + mbc.move(mailid, mbox, parameters.get_user(request.user, "TRASH_FOLDER")) + resp = dict(status="ok") + return ajax_simple_response(resp) @login_required @needs_mailbox() @@ -265,98 +214,6 @@ def compact(request, name): imapc.compact(name) return ajax_simple_response(dict(status="ok")) -# def render_compose(request, form, posturl, email=None, insert_signature=False): -# menu = compose_menu("", get_current_url(request), -# request.user.get_all_permissions()) -# editor = parameters.get_user(request.user, "EDITOR") -# if email is None: -# body = "" -# textheader = "" -# else: -# body = email.body -# textheader = email.textheader -# if insert_signature: -# signature = EmailSignature(request.user) -# body += str(signature) -# randid = None -# if not request.GET.has_key("id"): -# if request.session.has_key("compose_mail"): -# clean_attachments(request.session["compose_mail"]["attachments"]) -# randid = set_compose_session(request) -# elif not request.session.has_key("compose_mail") \ -# or request.session["compose_mail"]["id"] != request.GET["id"]: -# randid = set_compose_session(request) - -# attachments = request.session["compose_mail"]["attachments"] -# if len(attachments): -# short_att_list = "(%s)" \ -# % ", ".join(map(lambda att: att["fname"], -# attachments[:2] + [{"fname" : "..."}] \ -# if len(attachments) > 2 else attachments)) -# else: -# short_att_list = "" -# content = _render_to_string(request, "webmail/compose.html", { -# "form" : form, "bodyheader" : textheader, -# "body" : body, "posturl" : posturl, -# "attachments" : attachments, "short_att_list" : short_att_list -# }) -# mbc = IMAPconnector(user=request.user.username, -# password=request.session["password"]) -# ctx = getctx("ok", level=2, editor=editor, -# **__render_common_components(request, request.session["folder"], -# menu=menu, content=content)) -# if randid is not None: -# ctx["id"] = randid -# return HttpResponse(simplejson.dumps(ctx), mimetype="application/json") - -# @login_required -# @is_not_localadmin() -# def compose(request): -# if request.method == "POST": -# status, resp = send_mail(request, posturl=reverse(compose)) -# return resp - -# form = ComposeMailForm() -# form.fields["from_"].initial = request.user.username -# return render_compose(request, form, reverse(compose), insert_signature=True) - -# @login_required -# @needs_mailbox() -# def reply(request, folder, mail_id): -# msg = fetchmail(request, folder, mail_id, True) -# if request.method == "POST": -# status, resp = send_mail(request, origmsg=msg, -# posturl=reverse(reply, args=[folder, mail_id])) -# if status: -# IMAPconnector(user=request.user.username, -# password=request.session["password"]).msg_answered(folder, -# mail_id) -# return resp - -# form = ComposeMailForm() -# email = ReplyModifier(msg, request.user, form, request.GET.has_key("all"), -# addrfull=True, links="1") -# return render_compose(request, form, reverse(reply, args=[folder, mail_id]), -# email) - -# @login_required -# @needs_mailbox() -# def forward(request, folder, mail_id): -# if request.method == "POST": -# status, response = send_mail(request, -# posturl=reverse(forward, args=[folder, mail_id])) -# if status: -# IMAPconnector(user=request.user.username, -# password=request.session["password"]).msgforwarded(folder, -# mail_id) -# return response - -# msg = fetchmail(request, folder, mail_id, True) -# form = ComposeMailForm() -# email = ForwardModifier(msg, request.user, form, addrfull=True, links="1") -# return render_compose(request, form, reverse(forward, args=[folder, mail_id]), -# email) - def separate_folder(fullname, sep="."): """Split a folder name @@ -641,7 +498,7 @@ def forward(request): def msg_forwarded(mbox, mailid): get_imapconnector(request).msg_forwarded(mbox, mailid) - return compose_and_send(request, "reply", msg_forwarded) + return compose_and_send(request, "forward", msg_forwarded) @login_required @needs_mailbox() @@ -650,7 +507,7 @@ def getmailcontent(request): mailid = request.GET.get("mailid", None) if mbox is None or mailid is None: raise WebmailError(_("Invalid request")) - email = ImapEmail(mbox, mailid, request, links=request.GET["links"]) + email = ImapEmail(mbox, mailid, request, links=int(request.GET["links"])) return _render(request, "common/viewmail.html", { "headers" : email.render_headers(folder=mbox, mail_id=mailid), "folder" : mbox, "imapid" : mailid, "mailbody" : email.body @@ -660,8 +517,12 @@ def viewmail(request): mailid = request.GET.get("mailid", None) if mailid is None: raise WebmailError(_("Invalid request")) + links = request.GET.get("links", None) + if links is None: + links = 1 if parameters.get_user(request.user, "ENABLE_LINKS") == "yes" else 0 + else: + links = int(links) - links = 1 url = reverse(getmailcontent) + "?mbox=%s&mailid=%s&links=%d" % \ (request.session["mbox"], mailid, links) content = Template(""" @@ -720,6 +581,7 @@ def newindex(request): response["refreshrate"] = \ int(parameters.get_user(request.user, "REFRESH_INTERVAL")) * 1000 response["mboxes"] = render_mboxes_list(request, imapc) + response["quota"] = ImapListing.computequota(imapc) return _render(request, "webmail/index.html", response) if action in ["reply", "forward"]: