From 9544473e35ae2918c909c4b517605b58bb7e7618 Mon Sep 17 00:00:00 2001 From: "Dirk-Jan C. Binnema" Date: Sat, 29 Apr 2023 22:58:55 +0300 Subject: [PATCH] mu extract/view/verify: allow reading message from stdin Fixes #1463. --- NEWS.org | 6 +++ man/mu-extract.1.org | 16 +++++++- man/mu-verify.1.org | 5 ++- man/mu-view.1.org | 5 ++- mu/mu-cmd-extract.cc | 40 ++++++++++--------- mu/mu-cmd.cc | 91 ++++++++++++++++++++++++++++++-------------- mu/mu-options.cc | 29 +++++++++----- 7 files changed, 135 insertions(+), 57 deletions(-) diff --git a/NEWS.org b/NEWS.org index 703be80f..c7fd23fb 100644 --- a/NEWS.org +++ b/NEWS.org @@ -3,11 +3,17 @@ * 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 - experimental: support folding message threads (with TAB / S-TAB). See the [[info:mu4e:Folding threads][manual entry]] for further details. + * 1.10 (released on March 26, 2023) *** mu diff --git a/man/mu-extract.1.org b/man/mu-extract.1.org index 5892eca1..de8f6f0c 100644 --- a/man/mu-extract.1.org +++ b/man/mu-extract.1.org @@ -8,7 +8,7 @@ * SYNOPSIS -*mu [common-options] extract [options] * +*mu [common-options] extract [options] []* *mu [common-options] extract [options] * @@ -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 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, ~~ parameter as this would be +ambiguous; instead, use the ~--matches~ option. + * EXTRACT OPTIONS ** -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 a legal filename in the target directory. +** --matches= +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 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 @@ -87,6 +96,11 @@ To extract an mp3-file, and play it in the default mp3-playing application: $ mu extract --play msgfile 'whoopsididitagain.mp3' #+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 * SEE ALSO diff --git a/man/mu-verify.1.org b/man/mu-verify.1.org index 6d229e01..e7cee9cb 100644 --- a/man/mu-verify.1.org +++ b/man/mu-verify.1.org @@ -7,7 +7,7 @@ mu verify - verify message signatures and display information about them * SYNOPSIS -*mu [common-options] verify [options] * +*mu [common-options] verify [options] [ ... ]* * 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 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 ** -r, --auto-retrieve diff --git a/man/mu-view.1.org b/man/mu-view.1.org index 838bd1cc..db066465 100644 --- a/man/mu-view.1.org +++ b/man/mu-view.1.org @@ -7,7 +7,7 @@ mu view - display an e-mail message file * SYNOPSIS -mu [common options] view [options] [] +mu [common options] view [options] [ ...] * 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 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 ** --summary-len= diff --git a/mu/mu-cmd-extract.cc b/mu/mu-cmd-extract.cc index 504330c9..0f271f05 100644 --- a/mu/mu-cmd-extract.cc +++ b/mu/mu-cmd-extract.cc @@ -49,15 +49,11 @@ save_part(const Message::Part& part, size_t idx, const Options& opts) } static Result -save_parts(const std::string& path, const std::string& filename_rx, +save_parts(const Message& message, const std::string& filename_rx, 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{}; - for (auto&& part: message->parts()) { + for (auto&& part: message.parts()) { ++partnum; // should we extract this part? const auto do_extract = std::invoke([&]() { @@ -134,15 +130,11 @@ show_part(const MessagePart& part, size_t index, bool color) } static Mu::Result -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{}; 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); return Ok(); @@ -151,15 +143,29 @@ show_parts(const std::string& path, const Options& opts) Mu::Result Mu::mu_cmd_extract(const Options& opts) { - if (opts.extract.parts.empty() && - !opts.extract.save_attachments && !opts.extract.save_all && - opts.extract.filename_rx.empty()) - return show_parts(opts.extract.message, opts); /* show, don't save */ + auto message = std::invoke([&]()->Result{ + const auto mopts{message_options(opts.extract)}; + if (!opts.extract.message.empty()) + 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*/)) return Err(Error::Code::File, "target '%s' is not a writable directory", 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); } diff --git a/mu/mu-cmd.cc b/mu/mu-cmd.cc index 3f8a7c9c..1bb501ea 100644 --- a/mu/mu-cmd.cc +++ b/mu/mu-cmd.cc @@ -36,6 +36,7 @@ #include "message/mu-mime-object.hh" #include "utils/mu-error.hh" +#include "utils/mu-utils-file.hh" #include "utils/mu-utils.hh" #include "message/mu-message.hh" @@ -152,19 +153,15 @@ view_msg_plain(const Message& message, const Options& opts) } static Mu::Result -handle_msg(const std::string& fname, const Options& opts) +handle_msg(const Message& message, const Options& opts) { 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) { case Format::Plain: - return view_msg_plain(*message, opts); + return view_msg_plain(message, opts); case Format::Sexp: - return view_msg_sexp(*message, opts); + return view_msg_sexp(message, opts); default: g_critical("bug: should not be reached"); return Err(Error::Code::Internal, "error"); @@ -175,13 +172,29 @@ static Mu::Result cmd_view(const Options& opts) { 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; /* add a separator between two messages? */ if (opts.view.terminate) 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(); } @@ -312,6 +325,35 @@ verify(const MimeMultipartSigned& sigpart, const Options& opts) 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 cmd_verify(const Options& opts) { @@ -325,29 +367,22 @@ cmd_verify(const Options& opts) return Err(message.error()); 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 (!opts.quiet) - g_print("%s: no signed parts found\n", file.c_str()); - continue; - } + if (!verify_message(*message, opts, file)) + all_ok = false; + } - bool verified{true}; /* innocent until proven guilty */ - for(auto&& part: message->parts()) { + // when no messages provided, read from stdin + 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()) - 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; + all_ok = verify_message(*message, opts, ""); } if (all_ok) diff --git a/mu/mu-options.cc b/mu/mu-options.cc index b9637b38..4058f748 100644 --- a/mu/mu-options.cc +++ b/mu/mu-options.cc @@ -223,16 +223,27 @@ sub_extract(CLI::App& sub, Options& opts) "Target directory for saving") ->type_name("") ->default_str("")->default_val("."); - sub.add_option("message", opts.extract.message, - "Path to message file")->required() - ->type_name(""); sub.add_flag("--uncooked,-u", opts.extract.uncooked, "Avoid massaging extracted file-names"); + // optional; otherwise use standard-input + sub.add_option("message-path", opts.extract.message, + "Path to message file") + ->type_name(""); + + sub.add_option("--matches", opts.extract.filename_rx, + "Regular expression for files to save") + ->type_name("") + ->excludes("--parts") + ->excludes("--save-attachments") + ->excludes("--save-all"); + + // backward compat: filename-rx as non-option sub.add_option("filename-rx", opts.extract.filename_rx, "Regular expression for files to save") ->type_name("") ->excludes("--parts") ->excludes("--save-attachments") + ->excludes("--matches") ->excludes("--save-all"); } @@ -421,10 +432,10 @@ sub_verify(CLI::App& sub, Options& opts) { 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") - ->type_name("") - ->required(); + ->type_name(""); } static void @@ -460,10 +471,10 @@ sub_view(CLI::App& sub, Options& opts) sub.add_flag("--terminate", opts.view.terminate, "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") - ->type_name("") - ->required(); + ->type_name(""); }