Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Take 3: Mimetree and mime part support. Resolve #862. #1480

Merged
merged 13 commits into from
May 6, 2020
Merged
2 changes: 2 additions & 0 deletions alot/buffers/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ def get_info(self):
info['message_count'] = self.message_count
info['thread_tags'] = self.translated_tags_str()
info['intersection_tags'] = self.translated_tags_str(intersection=True)
info['mimetype'] = (
self.get_selected_message().mime_part.get_content_type())
return info

def get_selected_thread(self):
Expand Down
51 changes: 46 additions & 5 deletions alot/commands/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
from ..completion.path import PathCompleter
from ..db.utils import decode_header
from ..db.utils import formataddr
from ..db.utils import get_body_part
from ..db.utils import extract_headers
from ..db.utils import extract_body
from ..db.utils import clear_my_address
from ..db.utils import ensure_unique_address
from ..db.envelope import Envelope
Expand Down Expand Up @@ -506,24 +506,36 @@ async def apply(self, ui):
MODE, 'indent', help='change message/reply indentation',
arguments=[(['indent'], {'action': cargparse.ValidatedStoreAction,
'validator': cargparse.is_int_or_pm})])
@registerCommand(
MODE, 'togglemimetree', help='disply mime tree of the message',
forced={'mimetree': 'toggle'},
arguments=[(['query'], {'help': 'query used to filter messages to affect',
'nargs': '*'})])
@registerCommand(
MODE, 'togglemimepart', help='switch between html and plain text message',
forced={'mimepart': 'toggle'},
arguments=[(['query'], {'help': 'query used to filter messages to affect',
'nargs': '*'})])
class ChangeDisplaymodeCommand(Command):

"""fold or unfold messages"""
repeatable = True

def __init__(self, query=None, visible=None, raw=None, all_headers=None,
indent=None, **kwargs):
indent=None, mimetree=None, mimepart=False, **kwargs):
"""
:param query: notmuch query string used to filter messages to affect
:type query: str
:param visible: unfold if `True`, fold if `False`, ignore if `None`
:type visible: True, False, 'toggle' or None
:param raw: display raw message text.
:param raw: display raw message text
:type raw: True, False, 'toggle' or None
:param all_headers: show all headers (only visible if not in raw mode)
:type all_headers: True, False, 'toggle' or None
:param indent: message/reply indentation
:type indent: '+', '-', or int
:param mimetree: show the mime tree of the message
:type mimetree: True, False, 'toggle' or None
"""
self.query = None
if query:
Expand All @@ -532,6 +544,8 @@ def __init__(self, query=None, visible=None, raw=None, all_headers=None,
self.raw = raw
self.all_headers = all_headers
self.indent = indent
self.mimetree = mimetree
self.mimepart = mimepart
Command.__init__(self, **kwargs)

def apply(self, ui):
Expand Down Expand Up @@ -577,6 +591,20 @@ def matches(msgt):
raw = not mt.display_source if self.raw == 'toggle' else self.raw
all_headers = not mt.display_all_headers \
if self.all_headers == 'toggle' else self.all_headers
if self.mimepart:
if self.mimepart == 'toggle':
message = mt.get_message()
mimetype = {'plain': 'html', 'html': 'plain'}[
message.mime_part.get_content_subtype()]
mimepart = get_body_part(message.get_email(), mimetype)
elif self.mimepart is True:
mimepart = ui.get_deep_focus().mimepart
mt.set_mimepart(mimepart)
ui.update()
if self.mimetree == 'toggle':
tbuffer.focus_selected_message()
mimetree = not mt.display_mimetree \
if self.mimetree == 'toggle' else self.mimetree

# collapse/expand depending on new 'visible' value
if visible is False:
Expand All @@ -589,6 +617,8 @@ def matches(msgt):
mt.display_source = raw
if all_headers is not None:
mt.display_all_headers = all_headers
if mimetree is not None:
mt.display_mimetree = mimetree
mt.debug()
# let the messagetree reassemble itself
mt.reassemble()
Expand Down Expand Up @@ -707,7 +737,7 @@ async def apply(self, ui):
pipestrings.append(mail.as_string())
elif self.output_format == 'decoded':
headertext = extract_headers(mail)
bodytext = extract_body(mail)
bodytext = msg.get_body_text()
msgtext = '%s\n\n%s' % (headertext, bodytext)
pipestrings.append(msgtext)

Expand Down Expand Up @@ -986,6 +1016,7 @@ class MoveFocusCommand(MoveCommand):

def apply(self, ui):
logging.debug(self.movement)
original_focus = ui.get_deep_focus()
tbuffer = ui.current_buffer
if self.movement == 'parent':
tbuffer.focus_parent()
Expand Down Expand Up @@ -1016,18 +1047,28 @@ def apply(self, ui):
# TODO add 'next matching' if threadbuffer stores the original query
# TODO: add next by date..

if original_focus != ui.get_deep_focus():
ui.update()


@registerCommand(MODE, 'select')
class ThreadSelectCommand(Command):

"""select focussed element:
- if it is a message summary, toggle visibility of the message;
- if it is an attachment line, open the attachment"""
- if it is an attachment line, open the attachment
- if it is a mimepart, toggle visibility of the mimepart"""
async def apply(self, ui):
focus = ui.get_deep_focus()
if isinstance(focus, AttachmentWidget):
logging.info('open attachment')
await ui.apply_command(OpenAttachmentCommand(focus.get_attachment()))
elif getattr(focus, 'mimepart', False):
if isinstance(focus.mimepart, Attachment):
await ui.apply_command(OpenAttachmentCommand(focus.mimepart))
else:
await ui.apply_command(ChangeDisplaymodeCommand(
mimepart=True, mimetree='toggle'))
else:
await ui.apply_command(ChangeDisplaymodeCommand(visible='toggle'))

Expand Down
4 changes: 3 additions & 1 deletion alot/completion/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,9 @@ def f(completed, pos):
res = self._pathcompleter.complete(params, localpos)
elif self.mode == 'thread' and cmd in ['fold', 'unfold',
'togglesource',
'toggleheaders']:
'toggleheaders',
'togglemimetree',
'togglemimepart']:
res = self._querycompleter.complete(params, localpos)
elif self.mode == 'thread' and cmd in ['tag', 'retag', 'untag',
'toggletags']:
Expand Down
61 changes: 49 additions & 12 deletions alot/db/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from notmuch import NullPointerError

from . import utils
from .utils import extract_body
from .utils import get_body_part, extract_body_part
from .utils import decode_header
from .attachment import Attachment
from .. import helper
Expand Down Expand Up @@ -46,6 +46,7 @@ def __init__(self, dbman, msg, thread=None):
self._filename = msg.get_filename()
self._email = None # will be read upon first use
self._attachments = None # will be read upon first use
self._mime_tree = None # will be read upon first use
self._tags = set(msg.get_tags())

self._session_keys = []
Expand All @@ -67,6 +68,8 @@ def __init__(self, dbman, msg, thread=None):
else:
self._from = '"Unknown" <>'

self.mime_part = get_body_part(self.get_email())

def __str__(self):
"""prettyprint the message"""
aname, aaddress = self.get_author()
Expand Down Expand Up @@ -239,8 +242,6 @@ def get_attachments(self):
if not self._attachments:
self._attachments = []
for part in self.get_message_parts():
cd = part.get('Content-Disposition', '')
filename = part.get_filename()
ct = part.get_content_type()
# replace underspecified mime description by a better guess
if ct in ['octet/stream', 'application/octet-stream']:
Expand All @@ -251,21 +252,57 @@ def get_attachments(self):
'application/pgp-encrypted'):
self._attachments.pop()

if cd.lower().startswith('attachment'):
if ct.lower() not in ['application/pgp-signature']:
self._attachments.append(Attachment(part))
elif cd.lower().startswith('inline'):
if (filename is not None and
ct.lower() != 'application/pgp'):
self._attachments.append(Attachment(part))
if self._is_attachment(part, ct):
self._attachments.append(Attachment(part))
return self._attachments

@staticmethod
def _is_attachment(part, ct_override=None):
"""Takes a mimepart and returns a bool indicating if it's an attachment

Takes an optional argument to override the content type.
"""
cd = part.get('Content-Disposition', '')
filename = part.get_filename()
ct = ct_override or part.get_content_type()

if cd.lower().startswith('attachment'):
if ct.lower() not in ['application/pgp-signature']:
return True
elif cd.lower().startswith('inline'):
if (filename is not None and ct.lower() != 'application/pgp'):
return True

return False

def get_body_text(self):
""" returns bodystring extracted from this mail """
# TODO: allow toggle commands to decide which part is considered body
return extract_body(self.get_email())
return extract_body_part(self.mime_part)

def matches(self, querystring):
"""tests if this messages is in the resultset for `querystring`"""
searchfor = '( {} ) AND id:{}'.format(querystring, self._id)
return self._dbman.count_messages(searchfor) > 0

def get_mime_tree(self):
if not self._mime_tree:
self._mime_tree = self._get_mimetree(self.get_email())
return self._mime_tree

@classmethod
def _get_mimetree(cls, message):
label = cls._get_mime_part_info(message)
if message.is_multipart():
return label, [cls._get_mimetree(m) for m in message.get_payload()]
else:
if cls._is_attachment(message):
message = Attachment(message)
return label, message

@staticmethod
def _get_mime_part_info(mime_part):
contenttype = mime_part.get_content_type()
filename = mime_part.get_filename() or '(no filename)'
charset = mime_part.get_content_charset() or ''
size = helper.humanize_size(len(mime_part.as_string()))
return ' '.join((contenttype, filename, charset, size))
17 changes: 11 additions & 6 deletions alot/db/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,8 +463,8 @@ def remove_cte(part, as_string=False):
"http://alot.rtfd.io/en/latest/faq.html")


def extract_body(mail):
"""Returns a string view of a Message.
def get_body_part(mail, mimetype=None):
"""Returns an EmailMessage.

This consults :ref:`prefer_plaintext <prefer-plaintext>`
to determine if a "text/plain" alternative is preferred over a "text/html"
Expand All @@ -476,15 +476,20 @@ def extract_body(mail):
:rtype: str
"""

if settings.get('prefer_plaintext'):
preferencelist = ('plain', 'html')
else:
preferencelist = ('html', 'plain')
if not mimetype:
mimetype = 'plain' if settings.get('prefer_plaintext') else 'html'
preferencelist = {
'plain': ('plain', 'html'), 'html': ('html', 'plain')}[mimetype]

body_part = mail.get_body(preferencelist)
if body_part is None: # if no part matching preferredlist was found
return ""

return body_part


def extract_body_part(body_part):
"""Returns a string view of a Message."""
displaystring = ""
rendered_payload = render_part(
body_part,
Expand Down
3 changes: 2 additions & 1 deletion alot/defaults/alot.rc.spec
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,9 @@ search_statusbar = mixed_list(string, string, default=list('[{buffer_no}: search
# * `{message_count}`: number of contained messages
# * `{thread_tags}`: displays all tags present in the current thread.
# * `{intersection_tags}`: displays tags common to all messages in the current thread.
# * `{mimetype}`: content type of the mime part displayed in the focused message.

thread_statusbar = mixed_list(string, string, default=list('[{buffer_no}: thread] {subject}','{input_queue} total messages: {total_messages}'))
thread_statusbar = mixed_list(string, string, default=list('[{buffer_no}: thread] {subject}','[{mimetype}] {input_queue} total messages: {total_messages}'))

# Format of the status-bar in taglist mode.
# This is a pair of strings to be left and right aligned in the status-bar.
Expand Down
2 changes: 2 additions & 0 deletions alot/defaults/default.bindings
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ q = exit
s = save
r = reply
| = prompt 'pipeto '
t = togglemimetree
h = togglemimepart

'g j' = move next sibling
'g k' = move previous sibling
Expand Down
5 changes: 4 additions & 1 deletion alot/widgets/ansi.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ class ANSIText(urwid.WidgetWrap):
def __init__(self, txt,
default_attr=None,
default_attr_focus=None,
ansi_background=True, **kwds):
ansi_background=True,
mimepart=False,
**kwds):
self.mimepart = mimepart
ct, focus_map = parse_escapes_to_urwid(txt, default_attr,
default_attr_focus,
ansi_background)
Expand Down
Loading