From 09eb3dd5f8465299e75fad5900614614e62bd537 Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Sat, 18 Apr 2020 18:20:01 +0200 Subject: db/message: restructure message body handling Instead of allowing the callers to access the email part directly, introduce a new class for representing the MIME tree structure. All interaction with the message content should now happen through this class (some instances of direct access still remain and will be removed later). Encrypted/signed parts are now also handled through this structure rather than using a fragile hack of attaching the decrypted message to the encrypted one and using fake headers to signal encryption/signatures. Message body rendering is now done by walking through the whole MIME tree and considering all the parts for rendering rather than picking one specific part. --- alot/widgets/thread.py | 180 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 141 insertions(+), 39 deletions(-) (limited to 'alot/widgets') diff --git a/alot/widgets/thread.py b/alot/widgets/thread.py index 8f771590..5852967d 100644 --- a/alot/widgets/thread.py +++ b/alot/widgets/thread.py @@ -11,7 +11,6 @@ import urwid from .globals import TagWidget from .globals import AttachmentWidget from ..settings.const import settings -from ..db.message import X_SIGNATURE_MESSAGE_HEADER class MessageSummaryWidget(urwid.WidgetWrap): """ @@ -71,6 +70,145 @@ class MessageSummaryWidget(urwid.WidgetWrap): def keypress(self, size, key): return key +class _MessageBodyWidget(urwid.WidgetWrap): + _QUOTE_CHARS = '>|:}#' + _QUOTE_REGEX = '(([ \t]*[{quote_chars}])+)'.format(quote_chars = _QUOTE_CHARS) + + _prefer_plaintext = None + + def __init__(self, mime_tree): + self._prefer_plaintext = settings.get('prefer_plaintext') + body_wgt = self._build_body(mime_tree); + if body_wgt is None: + body_wgt = urwid.Text('<>>', alignt = 'center') + + super().__init__(body_wgt) + + def _build_body(self, mime_tree): + # handle encrypted/signed parts + if mime_tree.is_signed or mime_tree.is_encrypted: + return self._handle_crypt(mime_tree) + + if mime_tree.children is not None: + # multipart MIME parts + if mime_tree.is_alternative: + return self._handle_alternative(mime_tree) + + return self._handle_mixed(mime_tree) + + # no children - this is a leaf node + # skip attachment parts + if mime_tree.attachment: + return None + + # try rendering the message + text = mime_tree.render_str() + if text is not None: + return self._render_plain_text(text) + + return urwid.Text('Undisplayable "%s" MIME part.' % mime_tree.content_type, + align = 'center') + + def _handle_mixed(self, mime_tree): + children = [] + for child in mime_tree.children: + ch = self._build_body(child) + if ch is not None: + children.append(ch) + + if len(children) > 0: + return urwid.Pile(children) + + def _handle_alternative(self, mime_tree): + # TODO: switching between alternatives + preferred = 'plain' if self._prefer_plaintext else 'html' + + child = None + for ch in mime_tree.children: + if ch.content_subtype == preferred: + child = ch + break + if child is None: + child = mime_tree.children[0] + + return self._build_body(child) + + def _handle_crypt(self, mime_tree): + # handle broken crypto parts where we could not get the payload + if (mime_tree.children is None or + len(mime_tree.children) != 1): + text = 'Invalid "%s" MIME part' % mime_tree.content_type + + if mime_tree.crypt_error: + text += ': ' + mime_tree.crypt_error + + return urwid.Text(text, align = 'center') + + text_parts = [] + + if mime_tree.is_encrypted and mime_tree.is_signed: + desc = 'encrypted+signed' + if mime_tree.is_encrypted: + desc = 'encrypted' + elif mime_tree.is_signed: + desc = 'signed' + + text_parts.append(desc + ' MIME part') + + if mime_tree.is_signed and mime_tree.sig_valid: + trust = 'trusted' if mime_tree.sig_trusted else 'untrusted' + t = 'valid signature from: %s (%s)' % (mime_tree.signer_id, trust) + text_parts.append(t) + + if mime_tree.crypt_error: + text_parts.append('crypto processing error: ' + mime_tree.crypt_error) + + header = urwid.Columns([]) + + div_down = urwid.Divider('↓') + div_up = urwid.Divider('↑') + hdr_sep = (div_down, ('weight', 1, False)) + + header.contents.append(hdr_sep) + + for t in text_parts: + header.contents.append((urwid.Text(t), ('pack', None, False))) + header.contents.append(hdr_sep) + + footer = div_up + + body = self._build_body(mime_tree.children[0]) + + return urwid.Pile([header, body, footer]) + + def _render_plain_text(self, text): + attr_text = settings.get_theming_attribute('thread', 'body') + attrs_quote = settings.get_quote_theming() + + lines = text.splitlines() + + quote_levels = [] + for line in lines: + level = 0 + m = re.match(self._QUOTE_REGEX, line) + if m is not None: + g = m.group(0) + for c in self._QUOTE_CHARS: + level += g.count(c) + + quote_levels.append(level) + + line_widgets = [] + + for level, line in zip(quote_levels, lines): + if level == 0 or len(attrs_quote) < 1: + attr = attr_text + else: + attr = attrs_quote[(level - 1) % len(attrs_quote)] + line_widgets.append(urwid.Text((attr, line))) + + return urwid.Pile(line_widgets) + class HeadersWidget(urwid.WidgetWrap): """ A flow widget displaying message headers. @@ -168,9 +306,6 @@ class MessageWidget(urwid.WidgetWrap): _body_wgt = None _attach_wgt = None - _QUOTE_CHARS = '>|:}#' - _QUOTE_REGEX = '(([ \t]*[{quote_chars}])+)'.format(quote_chars = _QUOTE_CHARS) - def __init__(self, message): """ :param message: Message to display @@ -180,7 +315,7 @@ class MessageWidget(urwid.WidgetWrap): self._headers_wgt = self._get_headers() self._source_wgt = _RawMessageWidget(message.as_bytes()) - self._body_wgt = self._get_body() + self._body_wgt = _MessageBodyWidget(message.body) self._attach_wgt = self._get_attachments() super().__init__(urwid.ListBox(urwid.SimpleListWalker([]))) @@ -206,34 +341,6 @@ class MessageWidget(urwid.WidgetWrap): self._w.body[:] = widgets - def _get_body(self): - attr_body = settings.get_theming_attribute('thread', 'body') - attrs_quote = settings.get_quote_theming() - - body_lines = self._message.get_body_text().splitlines() - - quote_levels = [] - for line in body_lines: - level = 0 - m = re.match(self._QUOTE_REGEX, line) - if m is not None: - g = m.group(0) - for c in self._QUOTE_CHARS: - level += g.count(c) - - quote_levels.append(level) - - line_widgets = [] - - for level, line in zip(quote_levels, body_lines): - if level == 0 or len(attrs_quote) < 1: - attr = attr_body - else: - attr = attrs_quote[(level - 1) % len(attrs_quote)] - line_widgets.append(urwid.Text((attr, line))) - - return urwid.Pile(line_widgets) - def _get_headers(self): key_attr = settings.get_theming_attribute('thread', 'header_key') value_attr = settings.get_theming_attribute('thread', 'header_value') @@ -242,7 +349,7 @@ class MessageWidget(urwid.WidgetWrap): def _get_attachments(self): alist = [] - for a in self._message.get_attachments(): + for a in self._message.iter_attachments(): alist.append(AttachmentWidget(a)) if alist: return urwid.Pile(alist) @@ -285,11 +392,6 @@ class MessageWidget(urwid.WidgetWrap): values.append(t) lines.append((key, ', '.join(values))) - # OpenPGP pseudo headers - # TODO this should be in a separate widget - if X_SIGNATURE_MESSAGE_HEADER in headers: - lines.append(('PGP-Signature', headers[X_SIGNATURE_MESSAGE_HEADER][0])) - self._headers_wgt.set_headers(lines) self._display_all_headers = val -- cgit v1.2.3