mail/view: add pipe-attachment command

You can pipe individual attachments to binaries with the following
     command:

     pipe-attachment INDEX binary ARGS

     Example usage with the less(1) pager:
           pipe-attachment 0 less
     If the binary does not wait for your input before exiting, you will
     probably not see its output since you will return back to the user
     interface immediately.  You can write a wrapper script that pipes your
     binary's output to
           less
     or
           less -r
     if you want to preserve the ANSI escape codes in the pager's output.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
This commit is contained in:
Manos Pitsidianakis 2024-11-30 11:24:58 +02:00
parent fcab855fda
commit 5e77821f78
No known key found for this signature in database
GPG key ID: 7729C7707F7E09D0
4 changed files with 117 additions and 8 deletions

View file

@ -214,14 +214,30 @@ See
for the location of the mailcap files and
.Xr mailcap 5
for their syntax.
You can save individual attachments with the
.Command save-attachment Ar INDEX Ar path-to-file
command.
You can save individual attachments with the following command:
.Command save\-attachment Ar INDEX Ar path\-to\-file
.Ar INDEX
is the attachment's index in the listing.
If the path provided is a directory, the attachment is saved with its filename set to the filename in the attachment, if any.
If the 0th index is provided, the entire message is saved.
If the path provided is a directory, the message is saved as an eml file with its filename set to the messages message-id.
.Bl -tag -compact -width 8n
.It If the path provided is a directory, the attachment is saved with its filename set to the filename in the attachment, if any.
.It If the 0th index is provided, the entire message is saved.
.It If the path provided is a directory, the message is saved as an eml file with its filename set to the messages message\-id.
.El
.Pp
You can pipe individual attachments to binaries with the following command:
.Command pipe\-attachment Ar INDEX Ar binary Ar ARGS
Example usage with the
.Xr less 1
pager:
.D1 pipe\-attachment 0 less
If the binary does not wait for your input before exiting, you will probably
not see its output since you will return back to the user interface
immediately.
You can write a wrapper script that pipes your binary's output to
.Dl less
or
.Dl less \-r
if you want to preserve the ANSI escape codes in the pager's output.
.Sh SEARCH
Each e\-mail storage backend has a default search method assigned.
.Em IMAP
@ -582,7 +598,7 @@ open list archive with
.Bl -tag -width 36n
.It Cm mailto Ar MAILTO_ADDRESS
Opens a composer tab with initial values parsed from the
.Li mailto:
.Li mailto :
address.
.It Cm add-attachment Ar PATH
in composer, add
@ -603,7 +619,7 @@ Launch command
\&.
The command should print file paths in stdout, separated by NUL bytes.
Example usage with
.Xr fzf Ns
.Xr fzf 1 Ns
:
.D1 add-attachment-file-picker < fzf --print0
.It Cm remove-attachment Ar INDEX

View file

@ -102,6 +102,7 @@ pub enum ViewAction {
Pipe(String, Vec<String>),
Filter(Option<String>),
SaveAttachment(usize, String),
PipeAttachment(usize, String, Vec<String>),
ExportMail(String),
AddAddressesToContacts,
}

View file

@ -139,6 +139,7 @@ pub fn view(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
filter,
pipe,
save_attachment,
pipe_attachment,
export_mail,
add_addresses_to_contacts,
))(input)
@ -895,6 +896,35 @@ pub fn save_attachment(input: &[u8]) -> IResult<&[u8], Result<Action, CommandErr
let (input, _) = eof(input)?;
Ok((input, Ok(View(SaveAttachment(idx, path.to_string())))))
}
pub fn pipe_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:2, max_arg:{u8::MAX}, pipe_attachment};
let (input, _) = tag("pipe-attachment")(input.trim())?;
arg_chk!(start check, input);
let (input, _) = is_a(" ")(input)?;
arg_chk!(inc check, input);
let (input, idx) = map_res(quoted_argument, usize::from_str)(input)?;
let (input, _) = is_a(" ")(input)?;
arg_chk!(inc check, input);
let (input, bin) = quoted_argument(input)?;
arg_chk!(inc check, input);
let (input, args) = alt((
|input: &'a [u8]| -> IResult<&'a [u8], Vec<String>> {
let (input, _) = is_a(" ")(input)?;
let (input, args) = separated_list1(is_a(" "), quoted_argument)(input)?;
let (input, _) = eof(input)?;
Ok((
input,
args.into_iter().map(String::from).collect::<Vec<String>>(),
))
},
|input: &'a [u8]| -> IResult<&'a [u8], Vec<String>> {
let (input, _) = eof(input)?;
Ok((input, Vec::with_capacity(0)))
},
))(input)?;
arg_chk!(finish check, input);
Ok((input, Ok(View(PipeAttachment(idx, bin.to_string(), args)))))
}
pub fn export_mail(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:1, max_arg: 1, export_mail};
let (input, _) = tag("export-mail")(input.trim())?;

View file

@ -1521,6 +1521,68 @@ impl Component for EnvelopeView {
}
return true;
}
UIEvent::Action(View(ViewAction::PipeAttachment(a_i, ref bin, ref args))) => {
use std::borrow::Cow;
let bytes =
if let Some(u) = self.open_attachment(a_i, context) {
Cow::Owned(u.decode(Default::default()))
} else if a_i == 0 {
Cow::Borrowed(&self.mail.bytes)
} else {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!("Attachment `{}` not found.", a_i)),
));
return true;
};
// Kill input thread so that spawned command can be sole receiver of stdin
{
context.input_kill();
}
let pipe_command = format!("{} {}", bin, args.as_slice().join(" "));
log::trace!("Executing: {}", &pipe_command);
match Command::new(bin)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.map_err(Error::from)
.and_then(|mut child| {
let Some(mut stdin) = child.stdin.take() else {
let _ = child.wait();
return Err(Error::new(format!(
"Could not open standard input of {bin}"
))
.set_kind(ErrorKind::External));
};
stdin.write_all(&bytes).chain_err_summary(|| {
format!("Could not write to standard input of {bin}")
})?;
Ok(child)
}) {
Ok(mut child) => {
let _ = child.wait();
}
Err(err) => {
context.replies.push_back(UIEvent::Notification {
title: Some(
format!("Failed to execute {}: {}", pipe_command, err).into(),
),
source: None,
body: err.to_string().into(),
kind: Some(NotificationType::Error(melib::error::ErrorKind::External)),
});
context.replies.push_back(UIEvent::Fork(ForkType::Finished));
context.restore_input();
self.set_dirty(true);
return true;
}
}
context.replies.push_back(UIEvent::Fork(ForkType::Finished));
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::ENVELOPE_VIEW]["open_attachment"])
&& !self.cmd_buf.is_empty() =>