From cf124df0594f38c16e8633d5718446518e80c4d1 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 3 Feb 2012 15:26:00 +0100 Subject: [PATCH] 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 %}