diff --git a/extensions/webmail/imaputils.py b/extensions/webmail/imaputils.py index 8c1f758ed..bbc284832 100644 --- a/extensions/webmail/imaputils.py +++ b/extensions/webmail/imaputils.py @@ -93,6 +93,7 @@ class BodyStructure(object): self.inlines[params["cid"].strip("<>")] = params return + params["Content-Type"] = ftype if len(definition) > 7: extensions = ["md5", "disposition", "language", "location"] if mtype == "text": @@ -124,6 +125,12 @@ class BodyStructure(object): def has_attachments(self): return len(self.attachments) + def find_attachment(self, pnum): + for att in self.attachments: + if pnum == att["pnum"]: + return att + return None + class IMAPconnector(object): __metaclass__ = ConnectionsManager @@ -551,9 +558,21 @@ class IMAPconnector(object): self.quota_actual = int(m.group(1)) def fetchpart(self, uid, mbox, partnum): + """Retrieve a specific message part + + Useful to fetch attachments from the server. Part headers and + the payload are returned separatly. + + :param uid: a message UID + :param mbox: the mailbox containing the message + :param partnum: the part number + :return: a 2uple (dict, string) + """ self.select_mailbox(mbox, False) - data = self._cmd("FETCH", uid, "(BODY[%s])" % partnum) - return data[int(uid)]["BODY[%s]" % partnum] + data = self._cmd("FETCH", uid, "(BODYSTRUCTURE BODY[%s])" % partnum) + bs = BodyStructure(data[int(uid)]["BODYSTRUCTURE"]) + attdef = bs.find_attachment(partnum) + return attdef, data[int(uid)]["BODY[%s]" % partnum] def fetch(self, start, stop=None, mbox=None, **kwargs): """Retrieve information about messages from the server @@ -604,7 +623,6 @@ class IMAPconnector(object): :param readonly: :param extraheaders: """ - print mbox self.select_mailbox(mbox, readonly) if headers is None: headers = ['DATE', 'FROM', 'TO', 'CC', 'SUBJECT'] diff --git a/extensions/webmail/lib.py b/extensions/webmail/lib.py index 6e734fd17..5e4956c1d 100644 --- a/extensions/webmail/lib.py +++ b/extensions/webmail/lib.py @@ -182,8 +182,8 @@ class ImapEmail(Email): pnum = self.bs.contents[mformat]['pnum'] data = self.imapc._cmd("FETCH", mailid, "(BODY.PEEK[%s])" % pnum) - content = self._decode_content(self.bs.contents[mformat]['encoding'], - data[int(mailid)]['BODY[%s]' % pnum]) + content = decode_payload(self.bs.contents[mformat]['encoding'], + data[int(mailid)]['BODY[%s]' % pnum]) charset = self._find_content_charset(mformat) if charset is not None: content = content.decode(charset) @@ -229,16 +229,6 @@ class ImapEmail(Email): return self.bs.contents[subtype]["params"][pos + 1] return None - def _decode_content(self, encoding, content): - encoding = encoding.lower() - if encoding == "base64": - import base64 - return base64.b64decode(content) - elif encoding == "quoted-printable": - import quopri - return quopri.decodestring(content) - return content - def _find_attachments(self): for att in self.bs.attachments: attname = "part_%s" % att["pnum"] @@ -270,9 +260,9 @@ class ImapEmail(Email): if os.path.exists(path): continue - content = self.imapc.fetchpart(self.mailid, self.mbox, params["pnum"]) + pdef, content = self.imapc.fetchpart(self.mailid, self.mbox, params["pnum"]) fp = open(path, "wb") - fp.write(self._decode_content(params["encoding"], content)) + fp.write(decode_payload(params["encoding"], content)) fp.close() @@ -428,6 +418,25 @@ class EmailSignature(object): def __repr__(self): return self._sig +def decode_payload(encoding, payload): + """Decode the payload according to the given encoding + + Supported encodings: base64, quoted-printable. + + :param encoding: the encoding's name + :param payload: the value to decode + :return: a string + """ + print payload + encoding = encoding.lower() + if encoding == "base64": + import base64 + return base64.b64decode(payload) + elif encoding == "quoted-printable": + import quopri + return quopri.decodestring(payload) + return payload + def find_images_in_body(body): """Looks for images inside a HTML body diff --git a/extensions/webmail/templates/webmail/headers.html b/extensions/webmail/templates/webmail/headers.html index 1b5616f6c..ea795ecdb 100644 --- a/extensions/webmail/templates/webmail/headers.html +++ b/extensions/webmail/templates/webmail/headers.html @@ -1,13 +1,11 @@ -{% extends "common/mailheaders.html" %} -{% load i18n %} +{% extends "common/mailheaders.html" %}{% load i18n %} {% block extraheaders %} {% if attachments %} {% trans "Attachments" %} - - {% url modoboa.extensions.webmail.views.getattachment folder mail_id as test %} + {% url modoboa.extensions.webmail.views.getattachment as url %} {% for key, fname in attachments.items %} - {{ fname }} + {{ fname }} {% endfor %} diff --git a/extensions/webmail/urls.py b/extensions/webmail/urls.py index 2149baa05..50d72939d 100644 --- a/extensions/webmail/urls.py +++ b/extensions/webmail/urls.py @@ -18,6 +18,5 @@ urlpatterns = patterns( (r'^attachments/$', 'attachments'), (r'^delattachment/$', 'delattachment'), - - (r'^(?P.+)/(?P[\w\-\+]+)/part/$', 'getattachment'), + (r'^getattachment/$', 'getattachment'), ) diff --git a/extensions/webmail/views.py b/extensions/webmail/views.py index a1409ba01..efd69d86c 100644 --- a/extensions/webmail/views.py +++ b/extensions/webmail/views.py @@ -16,49 +16,35 @@ from templatetags import webextras @login_required @needs_mailbox() -def getattachment(request, folder, mail_id): +def getattachment(request): """Fetch a message attachment - Problème lié aux optimisations apportées : la méthode fetchpart du - client IMAP ne renvoie que le payload. Pour obtenir les headers, - deux solutions : + FIXME: par manque de caching, le bodystructure du message est + redemandé pour accéder aux headers de cette pièce jointe. - * Conserver le bodystructure parsé précédemment (nécessite du caching) - - * Reparser le bodystructure :p + :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 pnum: - raise Http404 + 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) - part = imapc.fetchpart(mail_id, folder, pnum) - if part is None: - raise Http404 - if part.get_content_maintype() == "message": - payload = part.get_payload(0) + 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: - payload = part.get_payload(decode=True) - resp = HttpResponse(payload) - for hdr, default in headers.iteritems(): - if not part.has_key(hdr): - if default is not None: - resp[hdr] = default - else: - continue - resp[hdr] = re.sub("\s", "", part[hdr]) - # I would add this part into the previous loop if I was - # able to use functions as default values... but I'm a bit - # lazy :p - if part.has_key("Content-Disposition"): - resp["Content-Disposition"] = \ - re.sub("\s", "", part["Content-Disposition"]) - else: - resp["Content-Disposition"] = \ - "attachment; filename=%s" % request.GET["fname"] - resp["Content-Length"] = len(payload) + cd = "attachment; filename=%s" % request.GET["fname"] + resp["Content-Disposition"] = cd + resp["Content-Length"] = partdef["size"] return resp @login_required