wip: Big webmail refactoring.

This commit is contained in:
Antoine Nguyen
2012-02-03 15:26:00 +01:00
parent 130250afa9
commit cf124df059
9 changed files with 201 additions and 120 deletions
+2
View File
@@ -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()
+17 -8
View File
@@ -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):
+89 -50
View File
@@ -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):
"""
+10 -16
View File
@@ -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)
});
@@ -19,7 +19,7 @@
{% if body %}
<textarea id="id_body" name="id_body" class="editor">{% if bodyheader %}{{ bodyheader }}{% endif %}
{{ body }}
</textarea>
</textarea>
{% else %}
<textarea id="id_body" name="id_body" class="editor"></textarea>
{% endif %}
+4 -4
View File
@@ -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"),
+1 -1
View File
@@ -28,7 +28,7 @@ urlpatterns = patterns(
(r'^(?P<fdname>.+)/(?P<mail_id>[\w\-\+]+)/delete/$', 'delete'),
#(r'^(?P<folder>.+)/(?P<mailid>[\w\-\+]+)/content/$', 'getmailcontent'),
(r'^(?P<folder>.+)/(?P<mail_id>[\w\-\+]+)/forward/$', 'forward'),
(r'^(?P<folder>.+)/(?P<mail_id>[\w\-\+]+)/reply/$', 'reply'),
# (r'^(?P<folder>.+)/(?P<mail_id>[\w\-\+]+)/reply/$', 'reply'),
# (r'^(?P<folder>.+)/(?P<mail_id>[\w\-\+]+)/$', 'viewmail'),
(r'^(?P<name>.+)/$', "folder"),
)
+73 -39
View File
@@ -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)
+4 -1
View File
@@ -1,5 +1,8 @@
{% load i18n %}
{% for field in form %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
{% for field in form.visible_fields %}
<div class="row clearfix">
{% if field.help_text %}
<div id="{{ field.name }}_helptext" class="tip">