mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
* Webmail: better message display. * Webmail: added optional Cc and Cci fields. see #204
520 lines
19 KiB
Python
520 lines
19 KiB
Python
# coding: utf-8
|
|
from django.http import HttpResponse, Http404
|
|
from django.template import Template, Context
|
|
from django.utils.translation import ugettext as _
|
|
from django.core.urlresolvers import reverse
|
|
from django.contrib.auth.decorators import login_required
|
|
from modoboa.lib import parameters
|
|
from modoboa.lib.webutils import _render, _render_error, \
|
|
getctx, _render_to_string, ajax_response, ajax_simple_response
|
|
from modoboa.lib.email_listing import parse_search_parameters
|
|
from modoboa.lib.decorators import needs_mailbox
|
|
from modoboa.auth.lib import *
|
|
from lib import *
|
|
from forms import *
|
|
from templatetags import webextras
|
|
|
|
@login_required
|
|
@needs_mailbox()
|
|
def getattachment(request):
|
|
"""Fetch a message attachment
|
|
|
|
FIXME: par manque de caching, le bodystructure du message est
|
|
redemandé pour accéder aux headers de cette pièce jointe.
|
|
|
|
:param request: a ``Request`` object
|
|
:param folder
|
|
"""
|
|
mbox = request.GET.get("mbox", None)
|
|
mailid = request.GET.get("mailid", None)
|
|
pnum = request.GET.get("partnumber", None)
|
|
if not mbox or not mailid or not pnum:
|
|
raise WebmailError(_("Invalid request"))
|
|
|
|
headers = {"Content-Type" : "text/plain",
|
|
"Content-Transfer-Encoding" : None}
|
|
imapc = get_imapconnector(request)
|
|
partdef, payload = imapc.fetchpart(mailid, mbox, pnum)
|
|
resp = HttpResponse(decode_payload(partdef["encoding"], payload))
|
|
resp["Content-Type"] = partdef["Content-Type"]
|
|
resp["Content-Transfer-Encoding"] = partdef["encoding"]
|
|
if partdef["disposition"] != 'NIL':
|
|
disp = partdef["disposition"]
|
|
cd = "%s; %s=%s" % (disp[0], disp[1][0], disp[1][1])
|
|
else:
|
|
cd = "attachment; filename=%s" % request.GET["fname"]
|
|
resp["Content-Disposition"] = cd
|
|
resp["Content-Length"] = partdef["size"]
|
|
return resp
|
|
|
|
@login_required
|
|
@needs_mailbox()
|
|
def move(request):
|
|
for arg in ["msgset", "to"]:
|
|
if not request.GET.has_key(arg):
|
|
raise WebmailError(_("Invalid request"))
|
|
mbc = get_imapconnector(request)
|
|
mbc.move(request.GET["msgset"], request.session["mbox"], request.GET["to"])
|
|
resp = listmailbox(request, request.session["mbox"])
|
|
resp.update(status="ok")
|
|
return ajax_simple_response(resp)
|
|
|
|
@login_required
|
|
@needs_mailbox()
|
|
def delete(request):
|
|
mbox = request.GET.get("mbox", None)
|
|
mailid = request.GET.get("mailid", None)
|
|
if mbox is None or mailid is None:
|
|
raise WebmailError(_("Invalid request"))
|
|
mbc = get_imapconnector(request)
|
|
mbc.move(mailid, mbox, parameters.get_user(request.user, "TRASH_FOLDER"))
|
|
resp = dict(status="ok")
|
|
return ajax_simple_response(resp)
|
|
|
|
@login_required
|
|
@needs_mailbox()
|
|
def mark(request, name):
|
|
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(imapc, "mark_messages_%s" % status)(name, ids)
|
|
except AttributeError:
|
|
raise WebmailError(_("Unknown action"))
|
|
|
|
return ajax_simple_response(
|
|
dict(status="ok", action=status, mbox=name,
|
|
unseen=imapc.unseen_messages(name))
|
|
)
|
|
|
|
@login_required
|
|
@needs_mailbox()
|
|
def empty(request, name):
|
|
if name != parameters.get_user(request.user, "TRASH_FOLDER"):
|
|
raise WebmailError(_("Invalid request"))
|
|
get_imapconnector(request).empty(name)
|
|
content = "<div class='alert alert-info'>%s</div>" % _("Empty mailbox")
|
|
return ajax_simple_response(dict(
|
|
status="ok", listing=content, mailbox=name
|
|
))
|
|
|
|
@login_required
|
|
@needs_mailbox()
|
|
def compact(request, name):
|
|
imapc = get_imapconnector(request)
|
|
imapc.compact(name)
|
|
return ajax_simple_response(dict(status="ok"))
|
|
|
|
@login_required
|
|
@needs_mailbox()
|
|
def newfolder(request, tplname="webmail/folder.html"):
|
|
mbc = IMAPconnector(user=request.user.username,
|
|
password=request.session["password"])
|
|
ctx = {"title" : _("Create a new mailbox"),
|
|
"formid" : "mboxform",
|
|
"action" : reverse(newfolder),
|
|
"action_label" : _("Create"),
|
|
"action_classes" : "submit",
|
|
"withunseen" : False,
|
|
"selectonly" : True}
|
|
|
|
ctx["mboxes"] = mbc.getmboxes(request.user)
|
|
if request.method == "POST":
|
|
form = FolderForm(request.POST)
|
|
if form.is_valid():
|
|
pf = request.POST.get("parent_folder", None)
|
|
mbc.create_folder(form.cleaned_data["name"], pf)
|
|
return ajax_simple_response(dict(
|
|
status="ok", respmsg=_("Mailbox created"), newmb=form.cleaned_data["name"], parent=pf
|
|
))
|
|
|
|
ctx["form"] = form
|
|
ctx["selected"] = None
|
|
return ajax_response(request, status="ko", template=tplname, **ctx)
|
|
|
|
ctx["form"] = FolderForm()
|
|
ctx["selected"] = None
|
|
return _render(request, tplname, ctx)
|
|
|
|
@login_required
|
|
@needs_mailbox()
|
|
def editfolder(request, tplname="webmail/folder.html"):
|
|
mbc = IMAPconnector(user=request.user.username,
|
|
password=request.session["password"])
|
|
ctx = {"title" : _("Edit mailbox"),
|
|
"formid" : "mboxform",
|
|
"action" : reverse(editfolder),
|
|
"action_label" : _("Update"),
|
|
"action_classes" : "submit",
|
|
"withunseen" : False,
|
|
"selectonly" : True}
|
|
|
|
if request.method == "POST":
|
|
form = FolderForm(request.POST)
|
|
if form.is_valid():
|
|
pf = request.POST.has_key("parent_folder") \
|
|
and request.POST["parent_folder"] or None
|
|
ctx["selected"] = pf
|
|
oldname, oldparent = separate_mailbox(request.POST["oldname"])
|
|
res = dict(status="ok", respmsg=_("Mailbox updated"))
|
|
if form.cleaned_data["name"] != oldname \
|
|
or (pf != oldparent):
|
|
newname = form.cleaned_data["name"] if pf is None \
|
|
else "%s.%s" % (pf, form.cleaned_data["name"])
|
|
mbc.rename_folder(request.POST["oldname"], newname)
|
|
res["oldmb"] = oldname
|
|
res["newmb"] = form.cleaned_data["name"]
|
|
res["oldparent"] = oldparent
|
|
res["newparent"] = pf
|
|
del request.session["mbox"]
|
|
return ajax_simple_response(res)
|
|
|
|
ctx["mboxes"] = mbc.getmboxes(request.user)
|
|
ctx["form"] = form
|
|
return ajax_response(request, status="ko", template=tplname, **ctx)
|
|
|
|
name = request.GET.get("name", None)
|
|
if name is None:
|
|
raise WebmailError(_("Invalid request"))
|
|
shortname, parent = separate_mailbox(name)
|
|
ctx["mboxes"] = mbc.getmboxes(request.user, until_mailbox=parent)
|
|
ctx["form"] = FolderForm()
|
|
ctx["form"].fields["oldname"].initial = name
|
|
ctx["form"].fields["name"].initial = shortname
|
|
ctx["selected"] = parent
|
|
return _render(request, tplname, ctx)
|
|
|
|
@login_required
|
|
@needs_mailbox()
|
|
def delfolder(request):
|
|
name = request.GET.get("name", None)
|
|
if name is None:
|
|
raise WebmailError(_("Bad request"))
|
|
mbc = IMAPconnector(user=request.user.username,
|
|
password=request.session["password"])
|
|
mbc.delete_folder(name)
|
|
if request.session.has_key("mbox"):
|
|
del request.session["mbox"]
|
|
return ajax_response(request)
|
|
|
|
@login_required
|
|
@needs_mailbox()
|
|
def attachments(request, tplname="webmail/attachments.html"):
|
|
if request.method == "POST":
|
|
csuploader = AttachmentUploadHandler()
|
|
request.upload_handlers.insert(0, csuploader)
|
|
error = None
|
|
form = AttachmentForm(request.POST, request.FILES)
|
|
if form.is_valid():
|
|
try:
|
|
fobj = request.FILES["attachment"]
|
|
tmpname = save_attachment(fobj)
|
|
request.session["compose_mail"]["attachments"] \
|
|
+= [{"fname" : str(fobj),
|
|
"content-type" : fobj.content_type,
|
|
"size" : fobj.size,
|
|
"tmpname" : os.path.basename(tmpname)}]
|
|
request.session.modified = True
|
|
return _render(request, "webmail/upload_done.html", {
|
|
"status" : "ok", "fname" : request.FILES["attachment"],
|
|
"tmpname" : os.path.basename(tmpname)
|
|
});
|
|
except WebmailError, inst:
|
|
error = _("Failed to save attachment: ") + str(inst)
|
|
|
|
if csuploader.toobig:
|
|
error = _("Attachment is too big (limit: %s)" \
|
|
% parameters.get_admin("MAX_ATTACHMENT_SIZE"))
|
|
return _render(request, "webmail/upload_done.html", {
|
|
"status" : "ko", "error" : error
|
|
});
|
|
ctx = {
|
|
"title" : _("Attachments"),
|
|
"formid" : "uploadfile",
|
|
"target" : "upload_target",
|
|
"enctype" : "multipart/form-data",
|
|
"form" : AttachmentForm(),
|
|
"action" : reverse(attachments),
|
|
"attachments" : request.session["compose_mail"]["attachments"]
|
|
}
|
|
return _render(request, tplname, ctx)
|
|
|
|
@login_required
|
|
@needs_mailbox()
|
|
def delattachment(request):
|
|
if not request.session.has_key("compose_mail") \
|
|
or not request.GET.has_key("name") \
|
|
or not request.GET["name"]:
|
|
return ajax_response(request, "ko", respmsg=_("Bad query"))
|
|
|
|
error = None
|
|
for att in request.session["compose_mail"]["attachments"]:
|
|
if att["tmpname"] == request.GET["name"]:
|
|
request.session["compose_mail"]["attachments"].remove(att)
|
|
fullpath = os.path.join(settings.MEDIA_ROOT, "tmp", att["tmpname"])
|
|
try:
|
|
os.remove(fullpath)
|
|
except OSError, e:
|
|
error = _("Failed to remove attachment: ") + str(e)
|
|
break
|
|
request.session.modified = True
|
|
return ajax_response(request)
|
|
if error is None:
|
|
error = _("Unknown attachment")
|
|
return ajax_response(request, "ko", respmsg=error)
|
|
|
|
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", {
|
|
"selected" : curmbox,
|
|
"mboxes" : imapc.getmboxes(request.user),
|
|
"withunseen" : True
|
|
})
|
|
|
|
def set_nav_params(request):
|
|
if not request.session.has_key("navparams"):
|
|
request.session["navparams"] = {}
|
|
|
|
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)
|
|
set_nav_params(request)
|
|
lst = ImapListing(request.user, request.session["password"],
|
|
baseurl="?action=listmailbox&name=%s&" % mbox,
|
|
folder=mbox,
|
|
elems_per_page=int(parameters.get_user(request.user, "MESSAGES_PER_PAGE")),
|
|
**request.session["navparams"])
|
|
request.session["mbox"] = mbox
|
|
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):
|
|
url = "?action=compose"
|
|
if request.method == "POST":
|
|
status, resp = send_mail(request, posturl=url)
|
|
return resp
|
|
|
|
form = ComposeMailForm()
|
|
form.fields["from_"].initial = request.user.username
|
|
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, "forward", msg_forwarded)
|
|
|
|
@login_required
|
|
@needs_mailbox()
|
|
def getmailcontent(request):
|
|
mbox = request.GET.get("mbox", None)
|
|
mailid = request.GET.get("mailid", None)
|
|
if mbox is None or mailid is None:
|
|
raise WebmailError(_("Invalid request"))
|
|
email = ImapEmail(mbox, mailid, request, links=int(request.GET["links"]))
|
|
return _render(request, "common/viewmail.html", {
|
|
"headers" : email.render_headers(folder=mbox, mail_id=mailid),
|
|
"folder" : mbox, "imapid" : mailid, "mailbody" : email.body
|
|
})
|
|
|
|
def viewmail(request):
|
|
mbox = request.GET.get("mbox", None)
|
|
mailid = request.GET.get("mailid", None)
|
|
if mbox is None or mailid is None:
|
|
raise WebmailError(_("Invalid request"))
|
|
links = request.GET.get("links", None)
|
|
if links is None:
|
|
links = 1 if parameters.get_user(request.user, "ENABLE_LINKS") == "yes" else 0
|
|
else:
|
|
links = int(links)
|
|
|
|
url = reverse(getmailcontent) + "?mbox=%s&mailid=%s&links=%d" % \
|
|
(mbox, mailid, links)
|
|
content = Template("""
|
|
<iframe src="{{ url }}" id="mailcontent"></iframe>
|
|
""").render(Context({"url" : url}))
|
|
|
|
return dict(listing=content, menuargs=dict(mail_id=mailid))
|
|
|
|
@login_required
|
|
@needs_mailbox()
|
|
def submailboxes(request):
|
|
topmailbox = request.GET.get('topmailbox', '')
|
|
mboxes = get_imapconnector(request).getmboxes(request.user, topmailbox)
|
|
return ajax_simple_response(dict(status="ok", mboxes=mboxes))
|
|
|
|
@login_required
|
|
@needs_mailbox()
|
|
def check_unseen_messages(request):
|
|
mboxes = request.GET.get("mboxes", None)
|
|
if not mboxes:
|
|
raise WebmailError(_("Invalid request"))
|
|
mboxes = mboxes.split(",")
|
|
counters = dict()
|
|
imapc = get_imapconnector(request)
|
|
for mb in mboxes:
|
|
counters[mb] = imapc.unseen_messages(mb)
|
|
return ajax_simple_response(dict(status="ok", counters=counters))
|
|
|
|
@login_required
|
|
@needs_mailbox()
|
|
def index(request):
|
|
"""Webmail actions handler
|
|
|
|
Problèmes liés à la navigation 'anchor based'
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
Lors d'un rafraichissemt complet, une première requête est envoyée
|
|
vers /webmail/. On ne connait pas encore l'action qui va être
|
|
demandée mais on peut déjà envoyer des informations indépendantes
|
|
(comme les dossiers, le quota).
|
|
|
|
Si on se contente de cela, l'affichage donnera un aspect décomposé
|
|
qui n'est pas très séduisant (à cause de la latence notamment). Il
|
|
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)
|
|
|
|
if action is not None:
|
|
if not globals().has_key(action):
|
|
raise WebmailError(_("Undefined action"))
|
|
response = globals()[action](request)
|
|
else:
|
|
if request.is_ajax():
|
|
raise WebmailError(_("Bad request"))
|
|
response = dict(selection="webmail", deflocation="?action=listmailbox",
|
|
defcallback="wm_updatelisting")
|
|
|
|
curmbox = request.session.get("mbox", "INBOX")
|
|
if not request.is_ajax():
|
|
request.session["lastaction"] = None
|
|
imapc = get_imapconnector(request)
|
|
response["mboxes"] = render_mboxes_list(request, imapc)
|
|
imapc.getquota(curmbox)
|
|
response["refreshrate"] = \
|
|
int(parameters.get_user(request.user, "REFRESH_INTERVAL"))
|
|
response["quota"] = ImapListing.computequota(imapc)
|
|
response["ro_mboxes"] = ["INBOX", "Junk",
|
|
parameters.get_user(request.user, "SENT_FOLDER"),
|
|
parameters.get_user(request.user, "TRASH_FOLDER"),
|
|
parameters.get_user(request.user, "DRAFTS_FOLDER")]
|
|
|
|
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"):
|
|
extra_args = response["menuargs"]
|
|
del response["menuargs"]
|
|
try:
|
|
response["menu"] = \
|
|
getattr(webextras, "%s_menu" % action)("", curmbox, request.user, **extra_args)
|
|
except KeyError:
|
|
pass
|
|
|
|
response.update(callback=action)
|
|
if not response.has_key("status"):
|
|
response.update(status="ok")
|
|
return ajax_simple_response(response)
|