wip: webmail big refactoring and performance improvment.

This commit is contained in:
Antoine Nguyen
2011-11-16 12:01:59 +01:00
parent 2e2011e070
commit 1016a41ab7
11 changed files with 915 additions and 289 deletions
+1 -1
View File
@@ -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
+22
View File
@@ -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
+282
View File
@@ -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<flags>.*?)\) "(?P<delimiter>.*)" (?P<name>.*)')
(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
+2 -281
View File
@@ -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<flags>.*?)\) "(?P<delimiter>.*)" (?P<name>.*)')
(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
+1 -1
View File
@@ -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")}
]
+528
View File
@@ -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
*/
@@ -49,7 +49,7 @@ function wm_updatelisting(resp) {
</script>
{% 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 %}
<div id="folders" class="box">
</div>
<div id="folders" class="box">{{ mboxes }}</div>
{% endblock %}
{% block menu %}{{ menu }}{% endblock %}
{% block footer %}
<div id="quotalabel">{% trans "Mailbox quota" %}</div>
<div id="quotabox"></div>
+1 -1
View File
@@ -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),
+3 -1
View File
@@ -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<name>.+)/$', "mark"),
+64
View File
@@ -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)
+1 -1
View File
@@ -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 }});