summaryrefslogtreecommitdiff
path: root/src/jwebmail/render_mail.py
diff options
context:
space:
mode:
authorJannis M. Hoffmann <jannis@fehcom.de>2023-12-03 19:22:12 +0100
committerJannis M. Hoffmann <jannis@fehcom.de>2023-12-03 19:22:12 +0100
commit2cf2a68bd1c25d8fe4f3126f40bd57982cc6b2a4 (patch)
treeb5c8ed0e1cfe8eac311829296a9aca062bb1abc1 /src/jwebmail/render_mail.py
initial commit
Diffstat (limited to 'src/jwebmail/render_mail.py')
-rw-r--r--src/jwebmail/render_mail.py160
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()))