aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHugo Hörnquist <hugo@lysator.liu.se>2022-11-30 23:16:24 +0100
committerHugo Hörnquist <hugo@lysator.liu.se>2022-11-30 23:16:24 +0100
commit1d273581639becaa50847a02a38f04338a035350 (patch)
tree6d6c47c25c9c1dcb122201914f8e21e0ac9bde4d
parentMinor style improvements. (diff)
downloadmu4web-1d273581639becaa50847a02a38f04338a035350.tar.gz
mu4web-1d273581639becaa50847a02a38f04338a035350.tar.xz
Embedding and attachements.
-rw-r--r--mu4web/main.py113
-rw-r--r--mu4web/static/style.css9
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;
+}