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

Pipeto --as_file #1476

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
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 @@ -249,9 +249,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 @@ -279,7 +278,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 @@ -294,19 +294,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.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.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(
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 @@ -632,7 +635,11 @@ def apply(self, ui):
(['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 @@ -650,7 +657,7 @@ class PipeCommand(Command):
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 @@ -671,7 +678,10 @@ def __init__(self, cmd, all=False, separately=False, background=False,
'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 @@ -692,6 +702,7 @@ def __init__(self, cmd, all=False, separately=False, background=False,
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 @@ -733,15 +744,22 @@ async def apply(self, ui):
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 @@ -750,33 +768,36 @@ async def apply(self, ui):

# 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,
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 @@ -980,7 +1001,7 @@ async def apply(self, ui):
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 Down Expand Up @@ -134,18 +137,14 @@ async def test_no_spawn_stdin_attached(self):
cmd = g_commands.ExternalCommand(
"test -t 0", stdin='0', 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_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