From 1d273581639becaa50847a02a38f04338a035350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Wed, 30 Nov 2022 23:16:24 +0100 Subject: Embedding and attachements. --- mu4web/main.py | 113 ++++++++++++++++++++++++++++++++++++++++++------ mu4web/static/style.css | 9 ++++ 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/mu4web/main.py b/mu4web/main.py index 60ebbb8..86a0b38 100644 --- a/mu4web/main.py +++ b/mu4web/main.py @@ -24,6 +24,7 @@ from user.pam import PamUser import subprocess +import flask from flask import ( Flask, session, @@ -34,6 +35,12 @@ from flask import ( get_flashed_messages ) +# +# A few operations depend on the index of attachements. These index +# are all pre-order traversal indexes of the attachement tree, which +# is also the order message.walk() returns them in. +# + login_manager = LoginManager() @@ -60,13 +67,15 @@ def header_format(key: str, value) -> HTML: return value -def attachement_tree(mail: EmailMessage) -> HTML: +def attachement_tree(id: str, mail: EmailMessage, idx=0) -> tuple[HTML, int]: ct = mail.get_content_type() fn = mail.get_filename() children = [] + _idx = idx for child in mail.iter_parts(): - children.append(attachement_tree(cast(EmailMessage, child))) + tree, idx = attachement_tree(id, cast(EmailMessage, child), idx + 1) + children.append(tree) content: HTML if children: @@ -78,7 +87,10 @@ def attachement_tree(mail: EmailMessage) -> HTML: body = f'{ct} {fn}' else: body = str(ct) - return ('li', body, content) + return ('li', ('a', {'data-idx': str(_idx), + 'href': '/part?' + urlencode({'id': id, + 'idx': _idx}), + }, body), content), idx # -------------------------------------------------- @@ -158,6 +170,8 @@ def page_base(title: Optional[str] = None, def response_for(id: str, username: Optional[str] = None) -> str: + # TODO option to show raw message + mail = cast(EmailMessage, get_mail(id)) headers = {} @@ -165,32 +179,54 @@ def response_for(id: str, username: Optional[str] = None) -> str: headers[key.lower()] = value head = [] + # TODO Make the defalut set of headers configurable for h in ['date', 'from', 'to', 'cc', 'bcc', 'subject', 'x-original-to', - 'in-reply-to']: + 'in-reply-to', 'message-id']: if x := headers.get(h.lower()): head += [('dt', h.title()), ('dd', header_format(h.lower(), x))] - body_part = mail.get_body(preferencelist=('html', 'plain')) - if not body_part: - raise ValueError("No suitable body in email") - ct = body_part.get_content_type() - body: HTML - if ct == 'text/html': - body = lambda: cast(EmailMessage, body_part).get_content() - else: - body = ('pre', cast(EmailMessage, body_part).get_content()) + all_heads = [] + for key, value in mail.items(): + all_heads += [('dt', key.title()), + ('dd', value)] + + full_headers = ('details', + ('summary', 'Alla mailhuvuden'), + ('dl', *all_heads)) if t := headers.get('subject'): title = f'Mail — {t}' else: title = 'Mail' + body: HTML = [] + # Manual walk to preserve attachement index + for idx, at in enumerate(mail.walk()): + # body.append(('h2', at.get_content_type())) + if at.is_multipart(): + continue + elif at.get_content_type() == 'text/html': + # ct = at.get_content_type() + url = '/part?' + urlencode({'id': id, 'idx': idx}) + body.append(('iframe', {'src': url, + 'height': '300', + })) + elif at.get_content_type() == 'text/plain': + body.append(('pre', at.get_content())) + else: + url = '/part?' + urlencode({'id': id, 'idx': idx}) + body.append(('a', {'href': url}, + at.get_filename() or at.get_content_type())) + + tree, idx = attachement_tree(id, mail) + main_body = [('dl', *head), + full_headers, ('hr',), ('main', body), ('hr',), - ('ul', attachement_tree(mail)), + ('ul', tree), ] html_str = render_document(page_base(title=title, body=main_body)) @@ -337,6 +373,55 @@ def search_page_(): request.args.get('by', None)) +def multipart_page(msg_id: str, + attachement: EmailMessage, + attachement_idx: int) -> str: + """ + Build HTML response for a multi-part attachement. + + Multi part attachements are simply containers, and can't directly be opened. Instead, build a tree of all components. + + [Parameters] + msg_id - Message ID of the mail in question + attachement - The attachement to work with, must be multipart + attachement_idx - Index of attachement in top level mail. + Needed for links to work. + """ + tree, idx = attachement_tree(msg_id, attachement, attachement_idx) + body = [('a', {'href': '/?' + urlencode({'id': msg_id})}, + 'Återvänd till brev'), + ('ul', tree), + ] + return render_document(page_base(title='Multipart', + body=body)) + + +def attachement_response(attachement: EmailMessage): + """ + Builds a response for a given attachement. + + Gets content type and encoding from the attachements headers. + """ + response = flask.Response() + response.charset = attachement.get_content_charset() + response.mimetype = attachement.get_content_type() + response.set_data(attachement.get_content()) + + return response + +@app.route('/part') +@login_required +def attachement_part_page(): + msg_id = request.args.get('id') + attachement_idx = int(request.args.get('idx')) + mail = cast(EmailMessage, get_mail(msg_id)) + attachement = list(mail.walk())[attachement_idx] + + if attachement.is_multipart(): + return multipart_page() + else: + return attachement_response(attachement) + # TODO this page is really weird if you are already logged in @app.route('/login', methods=['GET']) def login_page_(): diff --git a/mu4web/static/style.css b/mu4web/static/style.css index 2223f33..8297aab 100644 --- a/mu4web/static/style.css +++ b/mu4web/static/style.css @@ -105,3 +105,12 @@ footer { grid-column: 2; } + +iframe { + width: 100%; +} + +main pre { + white-space: pre-wrap; + border: 2px solid black; +} -- cgit v1.2.3