mu extract/view/verify: allow reading message from stdin

Fixes #1463.
This commit is contained in:
Dirk-Jan C. Binnema 2023-04-29 22:58:55 +03:00
parent 3a05dd8725
commit 9544473e35
7 changed files with 135 additions and 57 deletions

View File

@ -3,11 +3,17 @@
* 1.11.x (unreleased development build) * 1.11.x (unreleased development build)
*** mu
- ~mu~ commands ~extract~, ~verify~ and ~view~ can now read the message from
standard input; see their man-pages for details
*** mu4e *** mu4e
- experimental: support folding message threads (with TAB / S-TAB). - experimental: support folding message threads (with TAB / S-TAB).
See the [[info:mu4e:Folding threads][manual entry]] for further details. See the [[info:mu4e:Folding threads][manual entry]] for further details.
* 1.10 (released on March 26, 2023) * 1.10 (released on March 26, 2023)
*** mu *** mu

View File

@ -8,7 +8,7 @@
* SYNOPSIS * SYNOPSIS
*mu [common-options] extract [options] <file>* *mu [common-options] extract [options] [<file>]*
*mu [common-options] extract [options] <file> <pattern>* *mu [common-options] extract [options] <file> <pattern>*
@ -30,6 +30,10 @@ Without any options, *mu extract* simply outputs the list of leaf MIME-parts in
the message. Only 'leaf' MIME-parts (including RFC822 attachments) are the message. Only 'leaf' MIME-parts (including RFC822 attachments) are
considered, *multipart/** etc. are ignored. considered, *multipart/** etc. are ignored.
Without a filename parameter, ~mu extract~ reads a message from standard-input. In
that case, you cannot use the second, ~<pattern>~ parameter as this would be
ambiguous; instead, use the ~--matches~ option.
* EXTRACT OPTIONS * EXTRACT OPTIONS
** -a, --save-attachments ** -a, --save-attachments
@ -56,6 +60,11 @@ by default, ~mu~ transforms the attachment filenames a bit (such as by replacing
spaces by dashes); with this option, leave that to the minimum for creating spaces by dashes); with this option, leave that to the minimum for creating
a legal filename in the target directory. a legal filename in the target directory.
** --matches=<pattern>
Attachments with filenames matching the pattern will be extracted. The regular
expressions are basic PCRE, and are case-sensitive by default; see *pcre(3)* for
more details.
** --play ** --play
Try to 'play' (open) the attachment with the default application for the Try to 'play' (open) the attachment with the default application for the
particular file type. On MacOS, this uses the *open* program, on other platforms particular file type. On MacOS, this uses the *open* program, on other platforms
@ -87,6 +96,11 @@ To extract an mp3-file, and play it in the default mp3-playing application:
$ mu extract --play msgfile 'whoopsididitagain.mp3' $ mu extract --play msgfile 'whoopsididitagain.mp3'
#+end_example #+end_example
when reading from standard-input, you need ~--matches~, so:
#+begin_example
$ cat msgfile | mu extract --play --matches 'whoopsididitagain.mp3'
#+end_example
#+include: "prefooter.inc" :minlevel 1 #+include: "prefooter.inc" :minlevel 1
* SEE ALSO * SEE ALSO

View File

@ -7,7 +7,7 @@ mu verify - verify message signatures and display information about them
* SYNOPSIS * SYNOPSIS
*mu [common-options] verify [options] <msgfile>* *mu [common-options] verify [options] [<file> ... ]*
* DESCRIPTION * DESCRIPTION
@ -15,6 +15,9 @@ mu verify - verify message signatures and display information about them
signatures) and displaying information about them. The sub-command works on signatures) and displaying information about them. The sub-command works on
message files, and does not require the message to be indexed in the database. message files, and does not require the message to be indexed in the database.
If no message file is provided, the command expects the message on
standard-input.
* VERIFY OPTIONS * VERIFY OPTIONS
** -r, --auto-retrieve ** -r, --auto-retrieve

View File

@ -7,7 +7,7 @@ mu view - display an e-mail message file
* SYNOPSIS * SYNOPSIS
mu [common options] view [options] <file> [<files>] mu [common options] view [options] [<file> ...]
* DESCRIPTION * DESCRIPTION
@ -17,6 +17,9 @@ message files and does =not= require the message to be indexed in the database.
The command shows some common headers (From:, To:, Cc:, Bcc:, Subject: and The command shows some common headers (From:, To:, Cc:, Bcc:, Subject: and
Date:), the list of attachments and the plain-text body of the message (if any). Date:), the list of attachments and the plain-text body of the message (if any).
If no message file is provided, the command expects the message on
standard-input.
* VIEW OPTIONS * VIEW OPTIONS
** --summary-len=<number> ** --summary-len=<number>

View File

@ -49,15 +49,11 @@ save_part(const Message::Part& part, size_t idx, const Options& opts)
} }
static Result<void> static Result<void>
save_parts(const std::string& path, const std::string& filename_rx, save_parts(const Message& message, const std::string& filename_rx,
const Options& opts) const Options& opts)
{ {
auto message{Message::make_from_path(path, message_options(opts.extract))};
if (!message)
return Err(std::move(message.error()));
size_t partnum{}, saved_num{}; size_t partnum{}, saved_num{};
for (auto&& part: message->parts()) { for (auto&& part: message.parts()) {
++partnum; ++partnum;
// should we extract this part? // should we extract this part?
const auto do_extract = std::invoke([&]() { const auto do_extract = std::invoke([&]() {
@ -134,15 +130,11 @@ show_part(const MessagePart& part, size_t index, bool color)
} }
static Mu::Result<void> static Mu::Result<void>
show_parts(const std::string& path, const Options& opts) show_parts(const Message& message, const Options& opts)
{ {
auto msg_res{Message::make_from_path(path, message_options(opts.extract))};
if (!msg_res)
return Err(std::move(msg_res.error()));
size_t index{}; size_t index{};
g_print("MIME-parts in this message:\n"); g_print("MIME-parts in this message:\n");
for (auto&& part: msg_res->parts()) for (auto&& part: message.parts())
show_part(part, ++index, !opts.nocolor); show_part(part, ++index, !opts.nocolor);
return Ok(); return Ok();
@ -151,15 +143,29 @@ show_parts(const std::string& path, const Options& opts)
Mu::Result<void> Mu::Result<void>
Mu::mu_cmd_extract(const Options& opts) Mu::mu_cmd_extract(const Options& opts)
{ {
if (opts.extract.parts.empty() && auto message = std::invoke([&]()->Result<Message>{
!opts.extract.save_attachments && !opts.extract.save_all && const auto mopts{message_options(opts.extract)};
opts.extract.filename_rx.empty()) if (!opts.extract.message.empty())
return show_parts(opts.extract.message, opts); /* show, don't save */ return Message::make_from_path(opts.extract.message, mopts);
const auto msgtxt = read_from_stdin();
if (!msgtxt)
return Err(msgtxt.error());
else
return Message::make_from_text(*msgtxt, {}, mopts);
});
if (!message)
return Err(message.error());
else if (opts.extract.parts.empty() &&
!opts.extract.save_attachments && !opts.extract.save_all &&
opts.extract.filename_rx.empty())
return show_parts(*message, opts); /* show, don't save */
if (!check_dir(opts.extract.targetdir, false/*!readable*/, true/*writeable*/)) if (!check_dir(opts.extract.targetdir, false/*!readable*/, true/*writeable*/))
return Err(Error::Code::File, return Err(Error::Code::File,
"target '%s' is not a writable directory", "target '%s' is not a writable directory",
opts.extract.targetdir.c_str()); opts.extract.targetdir.c_str());
return save_parts(opts.extract.message, opts.extract.filename_rx, opts); return save_parts(*message, opts.extract.filename_rx, opts);
} }

View File

@ -36,6 +36,7 @@
#include "message/mu-mime-object.hh" #include "message/mu-mime-object.hh"
#include "utils/mu-error.hh" #include "utils/mu-error.hh"
#include "utils/mu-utils-file.hh"
#include "utils/mu-utils.hh" #include "utils/mu-utils.hh"
#include "message/mu-message.hh" #include "message/mu-message.hh"
@ -152,19 +153,15 @@ view_msg_plain(const Message& message, const Options& opts)
} }
static Mu::Result<void> static Mu::Result<void>
handle_msg(const std::string& fname, const Options& opts) handle_msg(const Message& message, const Options& opts)
{ {
using Format = Options::View::Format; using Format = Options::View::Format;
auto message{Message::make_from_path(fname, message_options(opts.view))};
if (!message)
return Err(message.error());
switch (opts.view.format) { switch (opts.view.format) {
case Format::Plain: case Format::Plain:
return view_msg_plain(*message, opts); return view_msg_plain(message, opts);
case Format::Sexp: case Format::Sexp:
return view_msg_sexp(*message, opts); return view_msg_sexp(message, opts);
default: default:
g_critical("bug: should not be reached"); g_critical("bug: should not be reached");
return Err(Error::Code::Internal, "error"); return Err(Error::Code::Internal, "error");
@ -175,13 +172,29 @@ static Mu::Result<void>
cmd_view(const Options& opts) cmd_view(const Options& opts)
{ {
for (auto&& file: opts.view.files) { for (auto&& file: opts.view.files) {
if (auto res = handle_msg(file, opts); !res) auto message{Message::make_from_path(
file, message_options(opts.view))};
if (!message)
return Err(message.error());
if (auto res = handle_msg(*message, opts); !res)
return res; return res;
/* add a separator between two messages? */ /* add a separator between two messages? */
if (opts.view.terminate) if (opts.view.terminate)
g_print("%c", VIEW_TERMINATOR); g_print("%c", VIEW_TERMINATOR);
} }
// no files? read from stding
if (opts.view.files.empty()) {
const auto msgtxt = read_from_stdin();
if (!msgtxt)
return Err(msgtxt.error());
auto message = Message::make_from_text(*msgtxt,{}, message_options(opts.view));
if (!message)
return Err(message.error());
else
return handle_msg(*message, opts);
}
return Ok(); return Ok();
} }
@ -312,6 +325,35 @@ verify(const MimeMultipartSigned& sigpart, const Options& opts)
return valid; return valid;
} }
static bool
verify_message(const Message& message, const Options& opts, const std::string& name)
{
if (none_of(message.flags() & Flags::Signed)) {
if (!opts.quiet)
g_print("%s: no signed parts found\n", name.c_str());
return false;
}
bool verified{true}; /* innocent until proven guilty */
for(auto&& part: message.parts()) {
if (!part.is_signed())
continue;
const auto& mobj{part.mime_object()};
if (!mobj.is_multipart_signed())
continue;
if (!verify(MimeMultipartSigned(mobj), opts))
verified = false;
}
return verified;
}
static Mu::Result<void> static Mu::Result<void>
cmd_verify(const Options& opts) cmd_verify(const Options& opts)
{ {
@ -325,29 +367,22 @@ cmd_verify(const Options& opts)
return Err(message.error()); return Err(message.error());
if (!opts.quiet && opts.verify.files.size() > 1) if (!opts.quiet && opts.verify.files.size() > 1)
g_print("verifying %sn\n", file.c_str()); g_print("verifying %s\n", file.c_str());
if (none_of(message->flags() & Flags::Signed)) { if (!verify_message(*message, opts, file))
if (!opts.quiet) all_ok = false;
g_print("%s: no signed parts found\n", file.c_str()); }
continue;
}
bool verified{true}; /* innocent until proven guilty */ // when no messages provided, read from stdin
for(auto&& part: message->parts()) { if (opts.verify.files.empty()) {
const auto msgtxt = read_from_stdin();
if (!msgtxt)
return Err(msgtxt.error());
auto message{Message::make_from_text(*msgtxt, {}, mopts)};
if (!message)
return Err(message.error());
if (!part.is_signed()) all_ok = verify_message(*message, opts, "<stdin>");
continue;
const auto& mobj{part.mime_object()};
if (!mobj.is_multipart_signed())
continue;
if (!verify(MimeMultipartSigned(mobj), opts))
verified = false;
}
all_ok = all_ok && verified;
} }
if (all_ok) if (all_ok)

View File

@ -223,16 +223,27 @@ sub_extract(CLI::App& sub, Options& opts)
"Target directory for saving") "Target directory for saving")
->type_name("<dir>") ->type_name("<dir>")
->default_str("<current>")->default_val("."); ->default_str("<current>")->default_val(".");
sub.add_option("message", opts.extract.message,
"Path to message file")->required()
->type_name("<message-path>");
sub.add_flag("--uncooked,-u", opts.extract.uncooked, sub.add_flag("--uncooked,-u", opts.extract.uncooked,
"Avoid massaging extracted file-names"); "Avoid massaging extracted file-names");
// optional; otherwise use standard-input
sub.add_option("message-path", opts.extract.message,
"Path to message file")
->type_name("<message-path>");
sub.add_option("--matches", opts.extract.filename_rx,
"Regular expression for files to save")
->type_name("<filename-rx>")
->excludes("--parts")
->excludes("--save-attachments")
->excludes("--save-all");
// backward compat: filename-rx as non-option
sub.add_option("filename-rx", opts.extract.filename_rx, sub.add_option("filename-rx", opts.extract.filename_rx,
"Regular expression for files to save") "Regular expression for files to save")
->type_name("<filename-rx>") ->type_name("<filename-rx>")
->excludes("--parts") ->excludes("--parts")
->excludes("--save-attachments") ->excludes("--save-attachments")
->excludes("--matches")
->excludes("--save-all"); ->excludes("--save-all");
} }
@ -421,10 +432,10 @@ sub_verify(CLI::App& sub, Options& opts)
{ {
sub_crypto(sub, opts.verify); sub_crypto(sub, opts.verify);
sub.add_option("files", opts.verify.files, // optional; otherwise use standard-input
sub.add_option("message-paths", opts.verify.files,
"Message files to verify") "Message files to verify")
->type_name("<message-file>") ->type_name("<message-path>");
->required();
} }
static void static void
@ -460,10 +471,10 @@ sub_view(CLI::App& sub, Options& opts)
sub.add_flag("--terminate", opts.view.terminate, sub.add_flag("--terminate", opts.view.terminate,
"Insert form-feed after each message"); "Insert form-feed after each message");
sub.add_option("files", opts.view.files, // optional; otherwise use standard-input
sub.add_option("message-paths", opts.view.files,
"Message files to view") "Message files to view")
->type_name("<file>") ->type_name("<message-path>");
->required();
} }