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("?pre>", "", 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("?pre>", "", 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 %}