Webmail refactoring.

see #585
This commit is contained in:
Antoine Nguyen
2014-10-01 16:56:16 +02:00
parent c91e09a1c4
commit ffbf69e70e
13 changed files with 281 additions and 240 deletions
@@ -1,5 +1,8 @@
{% extends "common/generic_modal_form.html" %}
{% load i18n form_tags sfilters_tags %}
{% block modal_css_classes %}modal-lg{% endblock %}
{% block formbody %}
{% if oldname %}<input type="hidden" name="oldname" value="{{ oldname }}" />{% endif %}
{% render_field form.name %}
@@ -6,7 +6,6 @@ from .imapemail import ImapEmail, ReplyModifier, ForwardModifier
from .imaputils import (
BodyStructure, IMAPconnector, get_imapconnector, separate_mailbox
)
from .listing import ImapListing
from .signature import EmailSignature
from .utils import decode_payload, WebmailNavigationParameters
from .sendmail import send_mail
@@ -20,7 +19,6 @@ __all__ = [
'ForwardModifier',
'IMAPconnector',
'ImapEmail',
'ImapListing',
'ReplyModifier',
'WebmailNavigationParameters',
'clean_attachments',
+46 -5
View File
@@ -141,6 +141,9 @@ class BodyStructure(object):
class IMAPconnector(object):
"""The IMAPv4 connector."""
__metaclass__ = ConnectionsManager
list_base_pattern = r'\((?P<flags>.*?)\) "(?P<delimiter>.*)" "?(?P<name>[^"]*)"?'
@@ -300,6 +303,32 @@ class IMAPconnector(object):
del self.m
self.m = None
def parse_search_parameters(self, criterion, pattern):
"""Parse search information and apply them."""
def or_criterion(old, c):
if old == "":
return c
return "OR (%s) (%s)" % (old, c)
if criterion == u"both":
criterion = u"from_addr, subject"
criterions = ""
if type(pattern) is unicode:
pattern = pattern.encode("utf-8")
if type(criterion) is unicode:
criterion = criterion.encode("utf-8")
for c in criterion.split(','):
if c == "from_addr":
key = "FROM"
elif c == "subject":
key = "SUBJECT"
else:
continue
criterions = \
or_criterion(criterions, '(%s "%s")' % (key, pattern))
self.criterions = [criterions]
def messages_count(self, **kwargs):
"""An enhanced version of messages_count
@@ -456,7 +485,9 @@ class IMAPconnector(object):
from operator import itemgetter
mailboxes += sorted(newmboxes, key=itemgetter("name"))
def getmboxes(self, user, topmailbox='', until_mailbox=None, unseen_messages=True):
def getmboxes(
self, user, topmailbox='', until_mailbox=None,
unseen_messages=True):
"""Returns a list of mailboxes for a particular user
By default, only the first level of mailboxes under
@@ -588,23 +619,33 @@ class IMAPconnector(object):
return True
def getquota(self, mailbox):
"""Retrieve quota information from the server.
We also compute the current usage.
"""
if not "QUOTA" in self.capabilities:
self.quota_limit = self.quota_actual = None
self.quota_limit = self.quota_current = None
return
data = self._cmd("GETQUOTAROOT", self._encode_mbox_name(mailbox),
responses=["QUOTAROOT", "QUOTA"])
if data is None:
self.quota_limit = self.quota_actual = None
self.quota_limit = self.quota_current = None
return
quotadef = data[1][0]
m = re.search("\(STORAGE (\d+) (\d+)\)", quotadef)
m = re.search(r"\(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))
self.quota_current = int(m.group(1))
try:
self.quota_usage = (
int(float(self.quota_current) / float(self.quota_limit) * 100)
)
except TypeError:
self.quota_usage = -1
def fetchpart(self, uid, mbox, partnum):
"""Retrieve a specific message part
-107
View File
@@ -1,107 +0,0 @@
from django.conf import settings
from django.utils.translation import ugettext_lazy
from modoboa.lib import parameters, tables
from modoboa.lib.email_listing import EmailListing
from . import IMAPconnector, imapheader
class WMtable(tables.Table):
"""Webmail listing table."""
tableid = "emails"
styles = "table-hover"
idkey = "imapid"
drag = tables.ImgColumn(
"drag", cssclass="draggable left",
defvalue="%spics/grippy.png" % settings.STATIC_URL,
header="<input type='checkbox' name='toggleselect' id='toggleselect' />"
)
selection = tables.SelectionColumn(
"selection", safe=True, header=None, sortable=False
)
flags = tables.ImgColumn("flags")
withatts = tables.ImgColumn("withatts")
subject = tables.Column(
"subject", label=ugettext_lazy("Subject"), limit=60,
cssclass="openable"
)
from_ = tables.Column(
"from", label=ugettext_lazy("From"), limit=30,
cssclass="openable"
)
date = tables.Column(
"date", label=ugettext_lazy("Date"), cssclass="openable"
)
cols_order = [
"drag", "selection", "flags", "from_", "subject", "withatts", "date"
]
def parse(self, header, value):
if value is None:
return ""
try:
value = getattr(imapheader, "parse_%s" % header)(value)
except AttributeError:
pass
return value
class ImapListing(EmailListing):
tpl = "webmail/index.html"
tbltype = WMtable
deflocation = "INBOX/"
defcallback = "wm_updatelisting"
reset_wm_url = False
def __init__(self, user, password, **kwargs):
self.user = user
self.mbc = IMAPconnector(user=user.username, password=password)
if "pattern" in kwargs and kwargs["pattern"]:
self.parse_search_parameters(
kwargs["criteria"], kwargs["pattern"]
)
else:
self.mbc.criterions = []
super(ImapListing, self).__init__(**kwargs)
self.extravars["refreshrate"] = \
int(parameters.get_user(user, "REFRESH_INTERVAL")) * 1000
def parse_search_parameters(self, criterion, pattern):
def or_criterion(old, c):
if old == "":
return c
return "OR (%s) (%s)" % (old, c)
if criterion == u"both":
criterion = u"from_addr, subject"
criterions = ""
if type(pattern) is unicode:
pattern = pattern.encode("utf-8")
if type(criterion) is unicode:
criterion = criterion.encode("utf-8")
for c in criterion.split(','):
if c == "from_addr":
key = "FROM"
elif c == "subject":
key = "SUBJECT"
else:
continue
criterions = \
or_criterion(criterions, '(%s "%s")' % (key, pattern))
self.mbc.criterions = [criterions]
@staticmethod
def computequota(mbc):
try:
return int(float(mbc.quota_actual) \
/ float(mbc.quota_limit) * 100)
except (AttributeError, TypeError):
return -1
def getquota(self):
return ImapListing.computequota(self.mbc)
@@ -46,9 +46,6 @@
}
/* GENERAL */
body, html {
/*overflow: hidden;*/
}
#rightcol {
position: fixed;
@@ -90,10 +87,6 @@ body, html {
width: 8px;
}
#mboxes_container {
/*overflow: auto;*/
}
.nav-sidebar li > a {
padding-top: 7px;
padding-bottom: 7px;
@@ -273,8 +266,9 @@ list {
}
/*
* Folder form
* Folders form
*/
.modal-body {
max-height: 300px;
overflow-y: auto;
@@ -284,6 +278,22 @@ list {
position: relative;
}
#folders2 div.clickbox {
left: 0;
top: 0px;
}
#folders2 li li {
margin-left: 10px;
}
#folders2 .active > a,
#folders2 .active > a:hover,
#folders2 .active > a:focus {
color: #fff;
background-color: #428bca;
}
/*
* Table
*/
@@ -186,7 +186,7 @@ Webmail.prototype = {
return $("#emails").height() - $element.height();
},
get_args: function() {
var args = that.navparams;
var args = $.extend({}, that.navparams);
args.scroll = true;
return args;
},
@@ -281,6 +281,8 @@ Webmail.prototype = {
/**
* Simple helper to retrieve the currently selected mailbox.
*
* @this Webmail
*/
get_current_mailbox: function() {
return this.navobject.getparam("mbox", "INBOX");
@@ -289,9 +291,11 @@ Webmail.prototype = {
/**
* Keep track of interesting navigation parameters in order to
* restore them later.
*
* @this Webmail
*/
store_nav_params: function() {
var params = new Array("page", "order", "pattern", "criteria");
var params = new Array("order", "pattern", "criteria");
this.navparams = {};
for (var idx in params) {
@@ -299,9 +303,11 @@ Webmail.prototype = {
}
},
/*
/**
* Restore navigation parameters previously stored via
* store_nav_params.
*
* @this Webmail
*/
restore_nav_params: function() {
if (this.navparams === undefined) {
@@ -360,12 +366,13 @@ Webmail.prototype = {
}
if (response.menu !== undefined) {
$("#menubar").html(response.menu);
$("#searchfield").searchbar({navobj: this.navobject});
$("#searchfield").searchbar({
navobj: this.navobject,
pattern_changed: function(navobj) {
navobj.setparam("reset_page", "true");
}
});
}
/*if (response.navbar) {
$("#pagination").html(response.navbar);
$("#pagination-responsive").html(response.navbar);
}*/
$("#listing").html(response.listing);
@@ -525,7 +532,12 @@ Webmail.prototype = {
* @param {Object} parent - parent node where mailboxes will be appended
* @param {boolean} async - if true, the ajax call will be asynchronous
*/
get_mailboxes: function(parent, async) {
get_mailboxes: function(parent, async, with_unseen) {
var args = "topmailbox=" + parent.attr("name");
if (with_unseen) {
args += "&unseen=true";
}
if (async === undefined) {
async = true;
}
@@ -533,7 +545,7 @@ Webmail.prototype = {
url: this.options.submboxes_url,
dataType: 'json',
async: async,
data: "topmailbox=" + parent.attr("name")
data: args
}).done($.proxy(function(data) {
this.inject_mailboxes(parent, data);
}, this));
@@ -575,7 +587,9 @@ Webmail.prototype = {
var $ul = $parent.find('ul[name="' + $parent.attr("name") + '"]');
if (!$ul.length) {
this.get_mailboxes($parent);
var $link = $div.next();
var with_unseen = ($link.attr("name") == "loadfolder") ? true : false;
this.get_mailboxes($parent, true, with_unseen);
return;
}
this.toggle_mbox_state($div, $ul);
@@ -642,6 +656,12 @@ Webmail.prototype = {
$('li[name="' + mailbox + '"]').addClass("active");
},
/**
* Click handler : select the parent mailbox of the mailbox being
* created/modified.
*
* @param {Object} e - event object
*/
select_parent_mailbox: function(e) {
e.preventDefault();
var $this = $(this);
@@ -808,6 +828,12 @@ Webmail.prototype = {
}, this));
},
/**
* Click event: empty Trash folder.
*
* @this Webmail
* @param {Object} e - event object
*/
empty: function(e) {
e.preventDefault();
var $link = $(e.target);
@@ -894,7 +920,7 @@ Webmail.prototype = {
}
this.navobject.reset().setparams({
action: "listmailbox",
mbox: obj.attr("href"),
mbox: obj.attr("href")
});
if (reset_page) {
this.navobject.setparam("reset_page", reset_page);
@@ -1061,16 +1087,19 @@ Webmail.prototype = {
this.resize_compose_body();
},
/*
/**
* Callback of the 'viewmail' action
*
* @this Webmail
* @param {Object} resp - AJAX call response (JSON)
*/
viewmail_callback: function(resp) {
this.page_update(resp);
$("#listing").css("overflow", "hidden");
// $("a[name=back]").click($.proxy(function(e) {
// e.preventDefault();
// this.go_back_to_listing();
// }, this));
$("a[name=back]").click($.proxy(function(e) {
e.preventDefault();
this.go_back_to_listing();
}, this));
},
mark_callback: function(data) {
+27 -18
View File
@@ -19,6 +19,7 @@ from django.views.decorators.gzip import gzip_page
from modoboa.lib import parameters
from modoboa.lib.exceptions import ModoboaException, BadRequest
from modoboa.lib.paginator import Paginator
from modoboa.lib.webutils import (
_render_to_string, ajax_response, render_to_json_response
)
@@ -29,7 +30,7 @@ from .forms import (
)
from .lib import (
decode_payload, AttachmentUploadHandler,
save_attachment, ImapListing, EmailSignature,
save_attachment, EmailSignature,
clean_attachments, set_compose_session, send_mail,
ImapEmail, WebmailNavigationParameters, ReplyModifier, ForwardModifier,
get_imapconnector, IMAPconnector, separate_mailbox
@@ -337,7 +338,8 @@ def listmailbox(request, defmailbox="INBOX", update_session=True):
parameter. (user preferences)
:param request: a ``Request`` object
:param defmailbox: the default mailbox (when not present inside request arguments)
:param defmailbox: the default mailbox (when not present inside
request arguments)
:return: a dictionnary
"""
navparams = WebmailNavigationParameters(request, defmailbox)
@@ -345,30 +347,34 @@ def listmailbox(request, defmailbox="INBOX", update_session=True):
if update_session:
navparams.store()
mbox = navparams.get('mbox')
lst = ImapListing(
request.user, request.session["password"],
baseurl="?action=listmailbox&mbox=%s&" % mbox,
folder=mbox,
elems_per_page=int(parameters.get_user(request.user, "MESSAGES_PER_PAGE")),
**request.session["webmail_navparams"]
)
page_id = int(navparams["page"])
page = lst.paginator.getpage(page_id)
mbc = get_imapconnector(request)
mbc.parse_search_parameters(
navparams.get("criteria"), navparams.get("pattern"))
paginator = Paginator(
mbc.messages_count(folder=mbox, order=navparams.get("order")),
int(parameters.get_user(request.user, "MESSAGES_PER_PAGE"))
)
page = paginator.getpage(page_id)
if page is not None:
email_list = lst.mbc.fetch(
page.id_start, page.id_stop, mbox,
nbelems=int(parameters.get_user(request.user, "MESSAGES_PER_PAGE"))
email_list = mbc.fetch(
page.id_start, page.id_stop, mbox, nbelems=page.items
)
content = _render_to_string(request, "webmail/email_list.html", {
"email_list": email_list,
"page": page_id,
"with_top_div": request.GET.get("scroll", "false") == "false"
})
length = len(content)
else:
content = ""
content = "<div class='alert alert-info'>{}</div>".format(
_("Empty mailbox")
)
length = 0
navparams["page"] = previous_page_id
return {"listing": content, "length": len(content), "page": page_id}
#return lst.render(request, navparams.get('page'))
return {"listing": content, "length": length, "page": page_id}
def render_compose(request, form, posturl, email=None, insert_signature=False):
@@ -513,8 +519,11 @@ def viewmail(request):
@login_required
@needs_mailbox()
def submailboxes(request):
"""Retrieve the sub mailboxes of a mailbox."""
topmailbox = request.GET.get('topmailbox', '')
mboxes = get_imapconnector(request).getmboxes(request.user, topmailbox)
with_unseen = request.GET.get('unseen', None)
mboxes = get_imapconnector(request).getmboxes(
request.user, topmailbox, unseen_messages=with_unseen == 'true')
return render_to_json_response(mboxes)
@@ -575,7 +584,7 @@ def index(request):
imapc.getquota(curmbox)
response["refreshrate"] = \
int(parameters.get_user(request.user, "REFRESH_INTERVAL"))
response["quota"] = ImapListing.computequota(imapc)
response["quota"] = imapc.quota_usage
trash = parameters.get_user(request.user, "TRASH_FOLDER")
response["trash"] = trash
response["ro_mboxes"] = [
+1 -66
View File
@@ -4,72 +4,7 @@ from django.template.loader import render_to_string
from django.template import Template, Context
from django.utils.translation import ugettext as _
attached_map = {}
class Page(object):
def __init__(self, pageid, id_start, id_stop, items,
items_per_page, has_previous, has_next,
baseurl=None):
self.number = pageid
self.id_start = id_start
self.id_stop = id_stop
self.items = items
self.items_per_page = items_per_page
self.has_previous = has_previous
self.has_next = has_next
self.baseurl = baseurl
def previous_page_number(self):
if not self.has_previous:
return False
return self.number - 1
def next_page_number(self):
if not self.has_next:
return False
return self.number + 1
def last_page(self):
lid = self.items / self.items_per_page
if not lid:
return 1
if self.items % self.items_per_page:
lid += 1
return lid
class Paginator(object):
def __init__(self, total, elems_per_page):
self.total = total
self.elems_per_page = elems_per_page
self.num_pages = total / elems_per_page
if total % elems_per_page:
self.num_pages += 1
def _indexes(self, page):
id_start = self.elems_per_page * page + 1
id_stop = id_start + self.elems_per_page - 1
return (id_start, id_stop)
def getpage(self, page):
if page < 1:
return None
id_start, id_stop = self._indexes(page - 1)
has_previous = has_next = False
if id_start <= self.total:
if page > 1:
has_previous = True
if id_stop < self.total:
has_next = True
else:
id_stop = self.total
else:
return None
p = Page(page, id_start, id_stop, self.total,
self.elems_per_page, has_previous, has_next)
p.paginator = self
return p
from modoboa.lib.paginator import Paginator
class EmailListing(object):
+99
View File
@@ -0,0 +1,99 @@
"""
Custom pagination tools.
"""
class Page(object):
"""Class to represent a page."""
def __init__(self, paginator, pageid, id_start, id_stop, baseurl=None):
"""Constructor."""
self.paginator = paginator
self.number = pageid
self.id_start = id_start
self.id_stop = id_stop
self.baseurl = baseurl
self._has_previous = None
self._has_next = None
@property
def items_per_page(self):
"""The number of items in a page. (shortcut)"""
return self.paginator.elems_per_page
@property
def items(self):
"""The total number of items. (shortcut)"""
return self.paginator.total
@property
def has_previous(self):
"""Tell if a previous page is available or not."""
if self._has_previous is None:
self._has_previous = True if self.number > 1 else False
return self._has_previous
@property
def previous_page_number(self):
"""Return the index of the previous page."""
if not self.has_previous:
return False
return self.number - 1
@property
def has_next(self):
"""Tell if a next page is available or not."""
if self._has_next is None:
self._has_next = (
True if self.id_stop < self.paginator.total else False
)
return self._has_next
@property
def next_page_number(self):
"""Return the index of the next page."""
if not self.has_next:
return False
return self.number + 1
@property
def last_page(self):
"""Retrieve the id of the last page."""
lid = self.items / self.items_per_page
if not lid:
return 1
if self.items % self.items_per_page:
lid += 1
return lid
class Paginator(object):
"""Pagination class."""
def __init__(self, total, elems_per_page):
self.total = total
self.elems_per_page = elems_per_page
self.num_pages = total / elems_per_page
if total % elems_per_page:
self.num_pages += 1
def _indexes(self, page):
"""Compute start and stop indexes."""
id_start = self.elems_per_page * page + 1
id_stop = id_start + self.elems_per_page - 1
return (id_start, id_stop)
def getpage(self, page_id):
"""Retrieve a specific page."""
if page_id < 1:
return None
id_start, id_stop = self._indexes(page_id - 1)
if id_start <= self.total:
if id_stop >= self.total:
id_stop = self.total
else:
return None
return Page(self, page_id, id_start, id_stop)
+7 -1
View File
@@ -20,6 +20,11 @@ function get_static_url(value) {
return static_url + value;
}
/**
* Open a modal box and load its content using an AJAX request.
*
* @param {Object} e - event object
*/
function modalbox(e, css, defhref, defcb, defclosecb) {
e.preventDefault();
var $this = $(this);
@@ -39,7 +44,8 @@ function modalbox(e, css, defhref, defcb, defclosecb) {
url: href
}).done(function(data) {
var $div = $('<div />', {
id: "modalbox", 'class': "modal fade", html: data
id: "modalbox", 'class': "modal fade", role: "modal",
html: data, 'aria-hidden': true
});
$div.modal({show: false});
+9 -9
View File
@@ -10,7 +10,7 @@
* Creates an instance of InfiniteScroll.
*
* @constructor
* @this {InfiniteScroll}
* @this InfiniteScroll
* @param {object} element
* @param {dictionary} options - instance options
*/
@@ -33,7 +33,7 @@
/**
* Scroll event handler.
*
* @this {InfiniteScroll}
* @this InfiniteScroll
*/
on_scroll: function() {
var bottom = this.$options.calculate_bottom(this.$element);
@@ -49,7 +49,7 @@
* Load the previous page from the server.
*
* @private
* @this {InfiniteScroll}
* @this InfiniteScroll
*/
load_previous_page: function() {
var $this = this;
@@ -74,7 +74,7 @@
/**
* Load the next page from the server.
*
* @this {InfiniteScroll}
* @this InfiniteScroll
*/
load_next_page: function() {
var $this = this;
@@ -95,7 +95,7 @@
* Load a page's content.
*
* @private
* @this {InfiniteScroll}
* @this InfiniteScroll
* @param {string} direction - direction of the scroll (up or down)
*/
load_page: function(direction) {
@@ -117,7 +117,6 @@
}
$this.executing = false;
$this.loaded_pages.push($this.current_page);
console.log("page", $this.current_page, "loaded successfuly");
}
});
},
@@ -125,18 +124,19 @@
/**
* Reset the loaded pages cache.
*
* @this {InfiniteScroll}
* @this InfiniteScroll
* @param {Array} current_page - the new current_page
*/
reset_loaded_pages: function(current_page) {
this.current_page = current_page;
this.loaded_pages = [this.current_page];
this.end_of_results = false;
},
/**
* Suspend the "infinite scroll" mode.
*
* @this {InfiniteScroll}
* @this InfiniteScroll
*/
pause: function() {
this.$element.unbind("scroll");
@@ -145,7 +145,7 @@
/**
* Resume the "infinite scroll" mode.
*
* @this {InfiniteScroll}
* @this InfiniteScroll
*/
resume: function() {
this.$element.scroll($.proxy(this.on_scroll, this));
+24 -6
View File
@@ -1,4 +1,12 @@
(function($) {
/**
* Return an instance of SearchBar.
*
* @constructor
* @param {Object} element -
* @param {Object} options - instance options
*/
var SearchBar = function(element, options) {
this.initialize(element, options);
};
@@ -16,24 +24,30 @@
});
this.$element.blur(function(e) {
var $this = $(this);
if ($this.val() == "") {
if ($this.val() === "") {
$this.val(gettext("Search..."));
}
});
if (this.options.navobj.params.pattern != undefined) {
if (this.options.navobj.params.pattern !== undefined) {
this.$element.val(this.options.navobj.getparam("pattern"));
}
if (this.options.navobj.params.criteria != undefined) {
if (this.options.navobj.params.criteria !== undefined) {
$("#crit_" + this.options.navobj.params.criteria).attr("checked", "checked");
}
},
/**
* Change event: apply search criterion.
*
* @this SearchBar
* @param {Object} e - event object
*/
pattern_change: function(e) {
var $input = $(e.target);
var criteria = $("input[name=scriteria]:checked").val();
var pattern = $input.val();
if (pattern != "") {
if (pattern !== "") {
this.options.navobj.setparams({
pattern: pattern,
criteria: criteria
@@ -43,6 +57,9 @@
this.options.navobj.delparam("criteria");
this.options.navobj.delparam("page");
}
if (this.options.pattern_changed) {
this.options.pattern_changed(this.options.navobj);
}
this.options.navobj.update();
}
};
@@ -63,6 +80,7 @@
};
$.fn.searchbar.defaults = {
navobj: null
navobj: null,
pattern_changed: null
};
})(jQuery);
})(jQuery);
+1 -1
View File
@@ -1,6 +1,6 @@
{% load i18n %}
<div class="modal-dialog">
<div class="modal-dialog {% block modal_css_classes %}{% endblock %}">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span><span class="sr-only">{% trans "Close" %}</span></button>