diff options
author | Jannis M. Hoffmann <jannis@fehcom.de> | 2023-12-03 19:22:12 +0100 |
---|---|---|
committer | Jannis M. Hoffmann <jannis@fehcom.de> | 2023-12-03 19:22:12 +0100 |
commit | 2cf2a68bd1c25d8fe4f3126f40bd57982cc6b2a4 (patch) | |
tree | b5c8ed0e1cfe8eac311829296a9aca062bb1abc1 /src/jwebmail/render_mail.py |
initial commit
Diffstat (limited to 'src/jwebmail/render_mail.py')
-rw-r--r-- | src/jwebmail/render_mail.py | 160 |
1 files changed, 160 insertions, 0 deletions
diff --git a/src/jwebmail/render_mail.py b/src/jwebmail/render_mail.py new file mode 100644 index 0000000..0e5406c --- /dev/null +++ b/src/jwebmail/render_mail.py @@ -0,0 +1,160 @@ +from flask import current_app, request, url_for +from flask_babel import gettext +from markupsafe import Markup, escape + + +def render_text_plain(_subtype, content, _path): + return f'<div class="jwm-mail-body-text-plain"><pre>{escape(content)}</pre></div>\n' + + +def render_text_html(_subtype, _content, path): + if path: + url = url_for( + "raw", msgid=request.view_args["msgid"], path=".".join(map(str, path)) + ) + else: + url = url_for("raw", msgid=request.view_args["msgid"]) + + return f'<iframe src="{url}" class="jwm-mail-body-text-html" ></iframe>\n' + + +def render_image(subtype, content, _path): + return f'<img src="data:image/{subtype};base64,{escape(content)}" />\n' + + +def render_multipart_alternative(_subtype, content, path): + parts = content["parts"] + T = "<div class=tabs><ul>\n" + C = "<div class=jwm-mail-body-multipart-alternative-bodies>" + init, *rest = reversed(parts) + + T += f"<li class=is-active data=0><a>{to_mime_type(init['head'])}</a></li>" + + C += "<div class=jwm-mail-body-multipart-alternative-body>\n" + C += mime_render( + *to_mime_types(init["head"]), init["body"], path + (len(parts) - 1,) + ) + C += "</div>\n" + + for i, r in enumerate(rest, 1): + T += f"<li data={i}><a>{to_mime_type(r['head'])}</a></li>\n" + + C += '<div class="jwm-mail-body-multipart-alternative-body is-hidden">\n' + C += mime_render( + *to_mime_types(r["head"]), r["body"], path + (len(parts) - 1 - i,) + ) + C += "</div>\n" + + C += "</div>" + T += "</ul></div>" + script_url = url_for("static", filename="src/rendermail.js") + return f'<script src="{script_url}"></script><div class="jwm-mail-body jwm-mail-body-multipart-alternative">\n{T}\n{C}\n</div>\n' + + +def render_multipart(_subtype, content, path): + parts = content["parts"] + R = '<div class="jwm-mail-body jwm-mail-body-multipart">\n' + + for i, p in enumerate(parts): + R += "<div class=media><div class=media-content>\n" + if ( + not p["head"]["content_disposition"] + or p["head"]["content_disposition"].lower() == "none" + or p["head"]["content_disposition"].lower() == "inline" + ): + R += mime_render(*to_mime_types(p["head"]), p["body"], path + (i,)) + elif p["head"]["content_disposition"].lower() == "attachment": + link_text = gettext("Attachment {filename} of type {filetype}").format( + filename=p["head"]["filename"], filetype=to_mime_type(p["head"]) + ) + + ref_url = url_for( + "raw", + msgid=request.view_args["msgid"], + path=".".join(map(str, [*path, i])), + ) + + R += "<p>" + R += f'<a href="{ref_url}" download="{escape(p["head"]["filename"])}">\n' + R += f"{escape(link_text)}</a>\n" + R += "</p>\n" + else: + current_app.log.warning( + "unknown Content-Disposition %s", p["head"]["content_disposition"] + ) + R += f"<p>unknown Content-Disposition {p['head']['content_disposition']}</p>\n" + + R += "</div></div>\n" + + return R + "</div>\n" + + +def _format_header(category, value): + R = "" + + if isinstance(value, list) and value: + R += f"<dt>{escape(category)}</dt>\n" + for v in value: + value = ( + f'"{v["display_name"]}" <{v["address"]}>' + if v["display_name"] + else v["address"] + ) + R += f"<dd>{escape(value)}<dd>\n" + + return R + + +def render_message(subtype, msg, path): + if subtype != "rfc822": + current_app.log.warning("unknown message mime-subtype %s", subtype) + + R = '<div class="jwm-mail">' + + R += '<dl class="jwm-mail-header">' + R += f"<dt>{escape(gettext('Subject'))}</dt>" + R += f"<dd>{escape(msg['head']['subject'])}</dd>\n" + R += _format_header(gettext("From"), msg["head"]["from"]) + R += _format_header(gettext("To"), msg["head"]["to"]) + R += _format_header(gettext("CC"), msg["head"]["cc"]) + R += _format_header(gettext("BCC"), msg["head"]["bcc"]) + R += f"<dt>{escape(gettext('Date'))}</dt>" + R += f"<dd>{escape(msg['head']['date'])}</dd>\n" + R += f"<dt>{escape(gettext('Content-Type'))}</dt>" + R += f"<dd>{to_mime_type(msg['head']['mime'])}</dd>\n" + R += "</dl>\n" + + R += mime_render(*to_mime_types(msg["head"]["mime"]), msg["body"], path + (0,)) + + return R + "</div>\n" + + +MIMERenderSubs = { + ("text", "plain"): render_text_plain, + ("text", "html"): render_text_html, + ("multipart", "alternative"): render_multipart_alternative, + "multipart": render_multipart, + "message": render_message, + "image": render_image, +} + + +def mime_render(maintype, subtype, content, path): + renderer = MIMERenderSubs.get((maintype, subtype)) or MIMERenderSubs.get(maintype) + + if not renderer: + return f'<p class="jwm-body-unsupported">Unsupported MIME type of <code>{maintype}/{subtype}</code>.</p>\n' + + return renderer(subtype, content, path) + + +def to_mime_type(mime): + return escape(f"{mime['content_maintype']}/{mime['content_subtype']}".lower()) + + +def to_mime_types(mime): + return escape(mime["content_maintype"]), escape(mime["content_subtype"]) + + +def format_mail(mail): + return Markup(mime_render("message", "rfc822", mail, tuple())) |