summaryrefslogtreecommitdiff
path: root/alot/widgets
diff options
context:
space:
mode:
authorAnton Khirnov <anton@khirnov.net>2020-04-18 18:20:01 +0200
committerAnton Khirnov <anton@khirnov.net>2020-04-18 18:26:38 +0200
commit09eb3dd5f8465299e75fad5900614614e62bd537 (patch)
tree8dc4f6727394306233ca336cbfea5311d7cd75b2 /alot/widgets
parenta06b892704982f3bc65ee7d6176efa22c24a4fb4 (diff)
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.
Diffstat (limited to 'alot/widgets')
-rw-r--r--alot/widgets/thread.py180
1 files changed, 141 insertions, 39 deletions
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('<<Message body is empty>>>', 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