Skip to content
2 changes: 1 addition & 1 deletion alot/commands/envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ async def apply(self, ui):
edit_headers = edit_headers - blacklist
logging.info('editable headers: %s', edit_headers)

def openEnvelopeFromTmpfile():
def openEnvelopeFromTmpfile(*args):
# This parses the input from the tempfile.
# we do this ourselves here because we want to be able to
# just type utf-8 encoded stuff into the tempfile and let alot
Expand Down
26 changes: 14 additions & 12 deletions alot/commands/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,8 @@ async def apply(self, ui):
if self.stdin is not None:
# wrap strings in StrinIO so that they behaves like a file
if isinstance(self.stdin, str):
# XXX: is utf-8 always safe to use here, or do we need to check
# the terminal encoding first?
stdin = BytesIO(self.stdin.encode('utf-8'))
stdin = BytesIO(
self.stdin.encode(urwid.util.detected_encoding))
else:
stdin = self.stdin

Expand Down Expand Up @@ -286,7 +285,8 @@ async def apply(self, ui):
except OSError as e:
ret = str(e)
else:
_, err = await proc.communicate(stdin.read() if stdin else None)
out, err = await proc.communicate(
stdin.read() if stdin else None)
if proc.returncode == 0:
ret = 'success'
elif err:
Expand All @@ -301,19 +301,21 @@ async def apply(self, ui):
except OSError as e:
ret = str(e)
else:
_, err = proc.communicate(stdin.read() if stdin else None)
if proc and proc.returncode == 0:
ret = 'success'
elif err:
ret = err.decode(urwid.util.detected_encoding)
out, err = proc.communicate(
stdin.read() if stdin else None)
if proc and proc.returncode == 0:
ret = 'success'
elif err:
ret = err.decode(urwid.util.detected_encoding)

if ret == 'success':
if self.on_success is not None:
self.on_success()
self.on_success(out)
else:
msg = "editor has exited with error code {} -- {}".format(
msg = (
"external command has exited with error code {} -- {}".format(
"None" if proc is None else proc.returncode,
ret or "No stderr output")
ret or "No stderr output"))
ui.notify(msg, priority='error')
if self.refocus and callerbuffer in ui.buffers:
logging.info('refocussing')
Expand Down
79 changes: 50 additions & 29 deletions alot/commands/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import logging
import mailcap
import os
import subprocess
import tempfile
import time
import email
import email.policy
from email.utils import getaddresses, parseaddr
Expand All @@ -29,9 +29,12 @@
from ..db.utils import decode_header
from ..db.utils import formataddr
from ..db.utils import get_body_part
from ..db.utils import extract_body_part
from ..db.utils import extract_headers
from ..db.utils import clear_my_address
from ..db.utils import ensure_unique_address
from ..db.utils import remove_cte
from ..db.utils import string_sanitize
from ..db.envelope import Envelope
from ..db.attachment import Attachment
from ..db.errors import DatabaseROError
Expand Down Expand Up @@ -638,7 +641,11 @@
(['cmd'], {'help': 'shellcommand to pipe to', 'nargs': '+'}),
(['--all'], {'action': 'store_true', 'help': 'pass all messages'}),
(['--format'], {'help': 'output format', 'default': 'raw',
'choices': ['raw', 'decoded', 'id', 'filepath']}),
'choices': [
'raw', 'decoded', 'id', 'filepath', 'mimepart',
'plain', 'html']}),
(['--as_file'], {'action': 'store_true',
'help': 'pass mail as a file to the given application'}),
(['--separately'], {'action': 'store_true',
'help': 'call command once for each message'}),
(['--background'], {'action': 'store_true',
Expand All @@ -656,7 +663,7 @@
repeatable = True

def __init__(self, cmd, all=False, separately=False, background=False,
shell=False, notify_stdout=False, format='raw',
shell=False, notify_stdout=False, format='raw', as_file=False,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add_tags=False, noop_msg='no command specified',
confirm_msg='', done_msg=None, **kwargs):
"""
Expand All @@ -677,7 +684,10 @@
'decoded': message content, decoded quoted printable,
'id': message ids, separated by newlines,
'filepath': paths to message files on disk
'mimepart': only pipe the currently selected mime part
:type format: str
:param as_file: pass mail as a file to the given application
:type as_file: bool
:param add_tags: add 'Tags' header to the message
:type add_tags: bool
:param noop_msg: error notification to show if `cmd` is empty
Expand All @@ -698,6 +708,7 @@
self.shell = shell
self.notify_stdout = notify_stdout
self.output_format = format
self.as_file = as_file
self.add_tags = add_tags
self.noop_msg = noop_msg
self.confirm_msg = confirm_msg
Expand Down Expand Up @@ -739,15 +750,22 @@
else:
for msg in to_print:
mail = msg.get_email()
mimepart = (getattr(ui.get_deep_focus(), 'mimepart', False)
or msg.get_mime_part())
if self.add_tags:
mail.add_header('Tags', ', '.join(msg.get_tags()))
if self.output_format == 'raw':
pipestrings.append(mail.as_string())
elif self.output_format == 'decoded':
headertext = extract_headers(mail)
bodytext = msg.get_body_text()
bodytext = extract_body_part(mimepart)
msgtext = '%s\n\n%s' % (headertext, bodytext)
pipestrings.append(msgtext)
elif self.output_format in ['mimepart', 'plain', 'html']:
if self.output_format in ['plain', 'html']:
mimepart = get_body_part(mail, self.output_format)
pipestrings.append(string_sanitize(remove_cte(
mimepart, as_string=True)))

if not self.separately:
pipestrings = [separator.join(pipestrings)]
Expand All @@ -756,33 +774,36 @@

# do the monkey
for mail in pipestrings:
encoded_mail = mail.encode(urwid.util.detected_encoding)
if self.background:
logging.debug('call in background: %s', self.cmd)
proc = subprocess.Popen(self.cmd,
shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, err = proc.communicate(encoded_mail)
cmd = self.cmd

# Pass mail as temporary file rather than piping through stdin.
if self.as_file:
suffix = {'html': '.html'}.get(mimepart.get_content_subtype())
with tempfile.NamedTemporaryFile(
delete=False, suffix=suffix) as tmpfile:
tmpfile.write(mail.encode(urwid.util.detected_encoding))
tempfile_name = tmpfile.name
mail = None
if self.shell:
cmd = [' '.join([cmd[0], tempfile_name])]
else:
cmd.append(tempfile_name)

def callback(out):
if self.as_file:
# Wait to remove file in case it's opened asynchronously.
time.sleep(5)
os.unlink(tempfile_name)
if self.notify_stdout:
ui.notify(out)
else:
with ui.paused():
logging.debug('call: %s', self.cmd)
# if proc.stdout is defined later calls to communicate
# seem to be non-blocking!
proc = subprocess.Popen(self.cmd, shell=True,
stdin=subprocess.PIPE,
# stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, err = proc.communicate(encoded_mail)
if err:
ui.notify(err, priority='error')
return
if self.done_msg:
ui.notify(self.done_msg)

# display 'done' message
if self.done_msg:
ui.notify(self.done_msg)
await ui.apply_command(ExternalCommand(cmd,
stdin=mail,
shell=self.shell,

Check warning on line 804 in alot/commands/thread.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

alot/commands/thread.py#L804

Function call with shell=True parameter identified, possible security issue.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thread=self.background,
on_success=callback))


@registerCommand(MODE, 'remove', arguments=[
Expand Down Expand Up @@ -986,7 +1007,7 @@
tempfile_name = tmpfile.name
self.attachment.write(tmpfile)

def afterwards():
def afterwards(*args):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.unlink(tempfile_name)
else:
handler_stdin = BytesIO()
Expand Down
3 changes: 2 additions & 1 deletion docs/source/usage/modes/thread.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ The following commands are available in thread mode:

optional arguments
:---all: pass all messages
:---format: output format; valid choices are: 'raw','decoded','id','filepath' (defaults to: 'raw')
:---format: output format; valid choices are: 'raw','decoded','id','filepath','mimepart','plain','html' (defaults to: 'raw')
:---as_file: pass mail as a file to the given application
:---separately: call command once for each message
:---background: don't stop the interface
:---add_tags: add 'Tags' header to the message
Expand Down
15 changes: 6 additions & 9 deletions tests/commands/test_global.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ def test_get_template_decode(self):


class TestExternalCommand(unittest.TestCase):
NO_STDERR_MSG = (
'external command has exited with error code 1 -- No stderr output'
)

@utilities.async_test
async def test_no_spawn_no_stdin_success(self):
Expand All @@ -126,9 +129,7 @@ async def test_no_spawn_no_stdin_attached(self):
ui = utilities.make_ui()
cmd = g_commands.ExternalCommand('test -p /dev/stdin', refocus=False)
await cmd.apply(ui)
ui.notify.assert_called_once_with(
'editor has exited with error code 1 -- No stderr output',
priority='error')
ui.notify.assert_called_once_with(self.NO_STDERR_MSG, priority='error')

@utilities.async_test
async def test_no_spawn_stdin_attached(self):
Expand All @@ -143,9 +144,7 @@ async def test_no_spawn_failure(self):
ui = utilities.make_ui()
cmd = g_commands.ExternalCommand('false', refocus=False)
await cmd.apply(ui)
ui.notify.assert_called_once_with(
'editor has exited with error code 1 -- No stderr output',
priority='error')
ui.notify.assert_called_once_with(self.NO_STDERR_MSG, priority='error')

@utilities.async_test
@mock.patch(
Expand Down Expand Up @@ -177,9 +176,7 @@ async def test_spawn_failure(self):
ui = utilities.make_ui()
cmd = g_commands.ExternalCommand('false', refocus=False, spawn=True)
await cmd.apply(ui)
ui.notify.assert_called_once_with(
'editor has exited with error code 1 -- No stderr output',
priority='error')
ui.notify.assert_called_once_with(self.NO_STDERR_MSG, priority='error')


class TestCallCommand(unittest.TestCase):
Expand Down
Loading