mirror of https://github.com/djcb/mu.git
parent
3a05dd8725
commit
9544473e35
6
NEWS.org
6
NEWS.org
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
91
mu/mu-cmd.cc
91
mu/mu-cmd.cc
|
@ -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)
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue