From 5e87fc764c4599d24929b500c242619aa5a2a803 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 17 Sep 2013 08:43:23 +0200 Subject: [PATCH] Properly encode filenames using RFC6266. see #473 fixes #481 --- modoboa/admin/views.py | 2 +- modoboa/extensions/sievefilters/views.py | 3 ++- modoboa/extensions/webmail/lib.py | 19 ++++++++++++------- modoboa/extensions/webmail/views.py | 7 ++++--- modoboa/lib/emailutils.py | 6 +++--- requirements.txt | 1 + 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/modoboa/admin/views.py b/modoboa/admin/views.py index 3582e9b33..fac30eb03 100644 --- a/modoboa/admin/views.py +++ b/modoboa/admin/views.py @@ -804,7 +804,7 @@ def _export(content, filename): resp = HttpResponse(content) resp["Content-Type"] = "text/csv" resp["Content-Length"] = len(content) - resp["Content-Disposition"] = 'attachment; filename="%s"' % filename + resp["Content-Disposition"] = build_header(filename) return resp diff --git a/modoboa/extensions/sievefilters/views.py b/modoboa/extensions/sievefilters/views.py index d730eff2b..9cae88c11 100644 --- a/modoboa/extensions/sievefilters/views.py +++ b/modoboa/extensions/sievefilters/views.py @@ -1,5 +1,6 @@ # coding: utf-8 from sievelib.managesieve import Error +from rfc6266 import build_header from django.http import HttpResponse from django.template.loader import render_to_string from django.contrib.auth.decorators import login_required @@ -263,7 +264,7 @@ def download_filters_set(request, name): resp = HttpResponse(script) resp["Content-Type"] = "text/plain; charset=utf-8" resp["Content-Length"] = len(script) - resp["Content-Disposition"] = 'attachment; filename="%s.txt"' % name + resp["Content-Disposition"] = build_header('%s.txt' % name) return resp @login_required diff --git a/modoboa/extensions/webmail/lib.py b/modoboa/extensions/webmail/lib.py index 8d39667cb..b8011d852 100644 --- a/modoboa/extensions/webmail/lib.py +++ b/modoboa/extensions/webmail/lib.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta import email import lxml import chardet +from rfc6266 import build_header, parse_headers from django.core.files.uploadhandler import FileUploadHandler, SkipFile from django.utils.translation import ugettext as _, ugettext_lazy @@ -277,20 +278,24 @@ class ImapEmail(Email): attname = "part_%s" % att["pnum"] params = None key = None - if att.has_key("params") and att["params"] != "NIL": + if "params" in att and att["params"] != "NIL": params = att["params"] key = "name" - if key is None and \ - att.has_key("disposition") and len(att["disposition"]) > 1: + if key is None and "disposition" in att and len(att["disposition"]) > 1: params = att["disposition"][1] key = "filename" if key and params: for pos, value in enumerate(params): - if value == key: + if not value.startswith(key): + continue + header = "%s; %s=%s" % (att['disposition'][0], value, + u2u_decode.u2u_decode(params[pos + 1]).strip("\r\t\n")) + attname = parse_headers(header).filename_unsafe + if attname is None: attname = u2u_decode.u2u_decode(params[pos + 1]).strip("\r\t\n") - break + break self.attachments[att["pnum"]] = attname def _fetch_inlines(self): @@ -302,7 +307,7 @@ class ImapEmail(Email): params["fname"] = os.path.join(settings.MEDIA_URL, fname) if os.path.exists(path): continue - + pdef, content = self.imapc.fetchpart(self.mailid, self.mbox, params["pnum"]) fp = open(path, "wb") fp.write(decode_payload(params["encoding"], content)) @@ -613,7 +618,7 @@ def create_mail_attachment(attdef): res.set_payload(fp.read()) fp.close() Encoders.encode_base64(res) - res.add_header("Content-Disposition", 'attachment', filename=attdef["fname"]) + res['Content-Disposition'] = build_header(attdef['fname'].decode('utf-8')) return res diff --git a/modoboa/extensions/webmail/views.py b/modoboa/extensions/webmail/views.py index 113b831da..06234f912 100644 --- a/modoboa/extensions/webmail/views.py +++ b/modoboa/extensions/webmail/views.py @@ -43,16 +43,17 @@ def getattachment(request): resp = HttpResponse(decode_payload(partdef["encoding"], payload)) resp["Content-Type"] = partdef["Content-Type"] resp["Content-Transfer-Encoding"] = partdef["encoding"] + print partdef if partdef["disposition"] != 'NIL': disp = partdef["disposition"] # FIXME : ugly hack, see fetch_parser.py for more explanation # :p if type(disp[1][0]) != dict: - cd = '%s; %s="%s"' % (disp[0], disp[1][0], disp[1][1]) + cd = '%s; %s=%s' % (disp[0], disp[1][0], disp[1][1]) else: - cd = '%s; %s="%s"' % (disp[0], disp[1][0]['struct'][0], disp[1][0]['struct'][1]) + cd = '%s; %s=%s' % (disp[0], disp[1][0]['struct'][0], disp[1][0]['struct'][1]) else: - cd = 'attachment; filename="%s"' % request.GET["fname"] + cd = build_header(request.GET["fname"]) resp["Content-Disposition"] = cd resp["Content-Length"] = partdef["size"] return resp diff --git a/modoboa/lib/emailutils.py b/modoboa/lib/emailutils.py index 66fe831c1..a10381ffe 100644 --- a/modoboa/lib/emailutils.py +++ b/modoboa/lib/emailutils.py @@ -79,15 +79,15 @@ class Email(object): must be displayed and not attached. """ if self.dformat == "html" and self.links \ - and msg.has_key("Content-Disposition"): + and "Content-Disposition" in msg: if msg["Content-Disposition"].startswith("inline"): cid = None - if msg.has_key("Content-ID"): + if "Content-ID" in msg: m = re.match("<(.+)>", msg["Content-ID"]) cid = m is not None and m.group(1) or msg["Content-ID"] fname = msg.get_filename() if fname is None: - if msg.has_key("Content-Location"): + if "Content-Location" in msg: fname = msg["Content-Location"] elif cid is not None: fname = cid diff --git a/requirements.txt b/requirements.txt index 195f44718..33a9fecab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ pycrypto sievelib chardet argparse +rfc6266