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;