diff --git a/mu/meson.build b/mu/meson.build index 1c0d5d26..7a33259b 100644 --- a/mu/meson.build +++ b/mu/meson.build @@ -1,4 +1,4 @@ -## Copyright (C) 2021-2022 Dirk-Jan C. Binnema +## Copyright (C) 2021-2023 Dirk-Jan C. Binnema ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by @@ -18,13 +18,20 @@ mu = executable( 'mu', [ 'mu.cc', 'mu-options.cc', + 'mu-cmd-add.cc', 'mu-cmd-cfind.cc', 'mu-cmd-extract.cc', 'mu-cmd-fields.cc', 'mu-cmd-find.cc', + 'mu-cmd-info.cc', + 'mu-cmd-init.cc', 'mu-cmd-index.cc', + 'mu-cmd-mkdir.cc', + 'mu-cmd-remove.cc', 'mu-cmd-script.cc', 'mu-cmd-server.cc', + 'mu-cmd-verify.cc', + 'mu-cmd-view.cc', 'mu-cmd.cc' ], dependencies: [ glib_dep, gmime_dep, lib_mu_dep, thread_dep, config_h_dep ], diff --git a/mu/mu-cmd-info.cc b/mu/mu-cmd-info.cc new file mode 100644 index 00000000..3f4bb577 --- /dev/null +++ b/mu/mu-cmd-info.cc @@ -0,0 +1,77 @@ +/* +** Copyright (C) 2023 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" +#include "mu-cmd.hh" + +#include + +using namespace Mu; + +Result +Mu::mu_cmd_info(const Mu::Store& store, const Options& opts) +{ + using namespace tabulate; + + if (!locale_workaround()) + return Err(Error::Code::User, "failed to find a working locale"); + + auto colorify = [](Table& table) { + for (auto&& row: table) { + + if (row.cells().size() < 2) + continue; + + row.cells().at(0)->format().font_style({FontStyle::bold}) + .font_color(Color::green); + row.cells().at(1)->format().font_color(Color::blue); + } + }; + + auto tstamp = [](::time_t t)->std::string { + if (t == 0) + return "never"; + else + return time_to_string("%c", t); + + }; + + Table info; + const auto conf{store.config()}; + info.add_row({"maildir", store.root_maildir()}); + info.add_row({"database-path", store.path()}); + info.add_row({"schema-version", + format("%zu", conf.get())}); + info.add_row({"max-message-size", format("%zu", conf.get())}); + info.add_row({"batch-size", format("%zu", conf.get())}); + info.add_row({"created", tstamp(conf.get())}); + for (auto&& c : conf.get()) + info.add_row({"personal-address", c}); + + info.add_row({"messages in store", format("%zu", store.size())}); + info.add_row({"last-change", tstamp(store.statistics().last_change)}); + info.add_row({"last-index", tstamp(store.statistics().last_index)}); + + if (!opts.nocolor) + colorify(info); + + std::cout << info << '\n'; + + return Ok(); +} diff --git a/mu/mu-cmd-init.cc b/mu/mu-cmd-init.cc new file mode 100644 index 00000000..84b0fb19 --- /dev/null +++ b/mu/mu-cmd-init.cc @@ -0,0 +1,70 @@ +/* +** Copyright (C) 2023 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" + +#include "mu-cmd.hh" + +using namespace Mu; + +Result +Mu::mu_cmd_init(const Options& opts) +{ + auto store = std::invoke([&]()->Result { + + /* + * reinit + */ + if (opts.init.reinit) + return Store::make(opts.runtime_path(RuntimePath::XapianDb), + Store::Options::ReInit|Store::Options::Writable); + /* + * full init + */ + + /* not provided, nor could we find a good default */ + if (opts.init.maildir.empty()) + return Err(Error::Code::InvalidArgument, + "missing --maildir parameter and could " + "not determine default"); + + MemDb mdb; + Config conf{mdb}; + if (opts.init.max_msg_size) + conf.set(*opts.init.max_msg_size); + if (opts.init.batch_size) + conf.set(*opts.init.batch_size); + + return Store::make_new(opts.runtime_path(RuntimePath::XapianDb), + opts.init.maildir, conf); + }); + + if (!store) + return Err(store.error()); + + if (!opts.quiet) { + mu_cmd_info(*store, opts); + std::cout << "database " + << (opts.init.reinit ? "reinitialized" : "created") + << "; use the 'index' command to fill/update it.\n"; + } + + return Ok(); +} + diff --git a/mu/mu-cmd-verify.cc b/mu/mu-cmd-verify.cc new file mode 100644 index 00000000..28df0993 --- /dev/null +++ b/mu/mu-cmd-verify.cc @@ -0,0 +1,181 @@ +/* +** Copyright (C) 2023 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" +#include "mu-cmd.hh" + +#include "message/mu-message.hh" +#include "message/mu-mime-object.hh" + +#include +#include + +using namespace Mu; + +template +static void +key_val(const Mu::MaybeAnsi& col, const std::string& key, T val) +{ + using Color = Mu::MaybeAnsi::Color; + + std::cout << col.fg(Color::BrightBlue) << std::left << std::setw(18) << key << col.reset() + << ": "; + + std::cout << col.fg(Color::Green) << val << col.reset() << "\n"; +} + + +static void +print_signature(const Mu::MimeSignature& sig, const Options& opts) +{ + Mu::MaybeAnsi col{!opts.nocolor}; + + const auto created{sig.created()}; + key_val(col, "created", + created == 0 ? "unknown" : + time_to_string("%c", sig.created()).c_str()); + + const auto expires{sig.expires()}; + key_val(col, "expires", expires==0 ? "never" : + time_to_string("%c", sig.expires()).c_str()); + + const auto cert{sig.certificate()}; + key_val(col, "public-key algo", + to_string_view_opt(cert.pubkey_algo()).value_or("unknown")); + key_val(col, "digest algo", + to_string_view_opt(cert.digest_algo()).value_or("unknown")); + key_val(col, "id-validity", + to_string_view_opt(cert.id_validity()).value_or("unknown")); + key_val(col, "trust", + to_string_view_opt(cert.trust()).value_or("unknown")); + key_val(col, "issuer-serial", cert.issuer_serial().value_or("unknown")); + key_val(col, "issuer-name", cert.issuer_name().value_or("unknown")); + key_val(col, "finger-print", cert.fingerprint().value_or("unknown")); + key_val(col, "key-id", cert.key_id().value_or("unknown")); + key_val(col, "name", cert.name().value_or("unknown")); + key_val(col, "user-id", cert.user_id().value_or("unknown")); +} + + +static bool +verify(const MimeMultipartSigned& sigpart, const Options& opts) +{ + using VFlags = MimeMultipartSigned::VerifyFlags; + const auto vflags{opts.verify.auto_retrieve ? + VFlags::EnableKeyserverLookups: VFlags::None}; + + auto ctx{MimeCryptoContext::make_gpg()}; + if (!ctx) + return false; + + const auto sigs{sigpart.verify(*ctx, vflags)}; + Mu::MaybeAnsi col{!opts.nocolor}; + + if (!sigs || sigs->empty()) { + + if (!opts.quiet) + g_print("cannot find signatures in part\n"); + + return true; + } + + bool valid{true}; + for (auto&& sig: *sigs) { + + const auto status{sig.status()}; + + if (!opts.quiet) + key_val(col, "status", to_string(status)); + + if (opts.verbose) + print_signature(sig, opts); + + if (none_of(sig.status() & MimeSignature::Status::Green)) + valid = false; + } + + 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; +} + + + +Mu::Result +Mu::mu_cmd_verify(const Options& opts) +{ + bool all_ok{true}; + const auto mopts = message_options(opts.verify); + + for (auto&& file: opts.verify.files) { + + auto message{Message::make_from_path(file, mopts)}; + if (!message) + return Err(message.error()); + + if (!opts.quiet && opts.verify.files.size() > 1) + g_print("verifying %s\n", file.c_str()); + + if (!verify_message(*message, opts, file)) + all_ok = false; + } + + // 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()); + + all_ok = verify_message(*message, opts, ""); + } + + if (all_ok) + return Ok(); + else + return Err(Error::Code::UnverifiedSignature, + "failed to verify one or more signatures"); +} diff --git a/mu/mu-cmd-view.cc b/mu/mu-cmd-view.cc new file mode 100644 index 00000000..2758442d --- /dev/null +++ b/mu/mu-cmd-view.cc @@ -0,0 +1,184 @@ +/* +** Copyright (C) 2023 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-cmd.hh" + +#include "message/mu-message.hh" + +#include +#include + +using namespace Mu; + + +#define VIEW_TERMINATOR '\f' /* form-feed */ + +using namespace Mu; + +static Mu::Result +view_msg_sexp(const Message& message, const Options& opts) +{ + ::fputs(message.sexp().to_string().c_str(), stdout); + ::fputs("\n", stdout); + + return Ok(); +} + + +static std::string /* return comma-sep'd list of attachments */ +get_attach_str(const Message& message, const Options& opts) +{ + std::string str; + seq_for_each(message.parts(), [&](auto&& part) { + if (auto fname = part.raw_filename(); fname) { + if (str.empty()) + str = fname.value(); + else + str += ", " + fname.value(); + } + }); + + return str; +} + +#define color_maybe(C) \ + do { \ + if (color) \ + fputs((C), stdout); \ + } while (0) + +static void +print_field(const std::string& field, const std::string& val, bool color) +{ + if (val.empty()) + return; + + color_maybe(MU_COLOR_MAGENTA); + fputs_encoded(field, stdout); + color_maybe(MU_COLOR_DEFAULT); + fputs(": ", stdout); + + color_maybe(MU_COLOR_GREEN); + fputs_encoded(val, stdout); + + color_maybe(MU_COLOR_DEFAULT); + fputs("\n", stdout); +} + +/* a summary_len of 0 mean 'don't show summary, show body */ +static void +body_or_summary(const Message& message, const Options& opts) +{ + gboolean color; + + color = !opts.nocolor; + + const auto body{message.body_text()}; + if (!body || body->empty()) { + if (any_of(message.flags() & Flags::Encrypted)) { + color_maybe(MU_COLOR_CYAN); + g_print("[No text body found; " + "message has encrypted parts]\n"); + } else { + color_maybe(MU_COLOR_MAGENTA); + g_print("[No text body found]\n"); + } + color_maybe(MU_COLOR_DEFAULT); + return; + } + + if (opts.view.summary_len) { + const auto summ{summarize(body->c_str(), *opts.view.summary_len)}; + print_field("Summary", summ, color); + } else { + print_encoded("%s", body->c_str()); + if (!g_str_has_suffix(body->c_str(), "\n")) + g_print("\n"); + } +} + +/* we ignore fields for now */ +/* summary_len == 0 means "no summary */ +static Mu::Result +view_msg_plain(const Message& message, const Options& opts) +{ + const auto color{!opts.nocolor}; + + print_field("From", to_string(message.from()), color); + print_field("To", to_string(message.to()), color); + print_field("Cc", to_string(message.cc()), color); + print_field("Bcc", to_string(message.bcc()), color); + print_field("Subject", message.subject(), color); + + if (auto&& date = message.date(); date != 0) + print_field("Date", time_to_string("%c", date), color); + + print_field("Tags", join(message.tags(), ", "), color); + + print_field("Attachments",get_attach_str(message, opts), color); + body_or_summary(message, opts); + + return Ok(); +} + +static Mu::Result +handle_msg(const Message& message, const Options& opts) +{ + using Format = Options::View::Format; + + switch (opts.view.format) { + case Format::Plain: + return view_msg_plain(message, opts); + case Format::Sexp: + return view_msg_sexp(message, opts); + default: + g_critical("bug: should not be reached"); + return Err(Error::Code::Internal, "error"); + } +} + +Mu::Result +Mu::mu_cmd_view(const Options& opts) +{ + for (auto&& file: opts.view.files) { + 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(); +} diff --git a/mu/mu-cmd.cc b/mu/mu-cmd.cc index 1bb501ea..f39c2845 100644 --- a/mu/mu-cmd.cc +++ b/mu/mu-cmd.cc @@ -38,451 +38,11 @@ #include "utils/mu-error.hh" #include "utils/mu-utils-file.hh" #include "utils/mu-utils.hh" -#include "message/mu-message.hh" #include -#define VIEW_TERMINATOR '\f' /* form-feed */ - using namespace Mu; -static Mu::Result -view_msg_sexp(const Message& message, const Options& opts) -{ - ::fputs(message.sexp().to_string().c_str(), stdout); - ::fputs("\n", stdout); - - return Ok(); -} - - -static std::string /* return comma-sep'd list of attachments */ -get_attach_str(const Message& message, const Options& opts) -{ - std::string str; - seq_for_each(message.parts(), [&](auto&& part) { - if (auto fname = part.raw_filename(); fname) { - if (str.empty()) - str = fname.value(); - else - str += ", " + fname.value(); - } - }); - - return str; -} - -#define color_maybe(C) \ - do { \ - if (color) \ - fputs((C), stdout); \ - } while (0) - -static void -print_field(const std::string& field, const std::string& val, bool color) -{ - if (val.empty()) - return; - - color_maybe(MU_COLOR_MAGENTA); - fputs_encoded(field, stdout); - color_maybe(MU_COLOR_DEFAULT); - fputs(": ", stdout); - - color_maybe(MU_COLOR_GREEN); - fputs_encoded(val, stdout); - - color_maybe(MU_COLOR_DEFAULT); - fputs("\n", stdout); -} - -/* a summary_len of 0 mean 'don't show summary, show body */ -static void -body_or_summary(const Message& message, const Options& opts) -{ - gboolean color; - - color = !opts.nocolor; - - const auto body{message.body_text()}; - if (!body || body->empty()) { - if (any_of(message.flags() & Flags::Encrypted)) { - color_maybe(MU_COLOR_CYAN); - g_print("[No text body found; " - "message has encrypted parts]\n"); - } else { - color_maybe(MU_COLOR_MAGENTA); - g_print("[No text body found]\n"); - } - color_maybe(MU_COLOR_DEFAULT); - return; - } - - if (opts.view.summary_len) { - const auto summ{summarize(body->c_str(), *opts.view.summary_len)}; - print_field("Summary", summ, color); - } else { - print_encoded("%s", body->c_str()); - if (!g_str_has_suffix(body->c_str(), "\n")) - g_print("\n"); - } -} - -/* we ignore fields for now */ -/* summary_len == 0 means "no summary */ -static Mu::Result -view_msg_plain(const Message& message, const Options& opts) -{ - const auto color{!opts.nocolor}; - - print_field("From", to_string(message.from()), color); - print_field("To", to_string(message.to()), color); - print_field("Cc", to_string(message.cc()), color); - print_field("Bcc", to_string(message.bcc()), color); - print_field("Subject", message.subject(), color); - - if (auto&& date = message.date(); date != 0) - print_field("Date", time_to_string("%c", date), color); - - print_field("Tags", join(message.tags(), ", "), color); - - print_field("Attachments",get_attach_str(message, opts), color); - body_or_summary(message, opts); - - return Ok(); -} - -static Mu::Result -handle_msg(const Message& message, const Options& opts) -{ - using Format = Options::View::Format; - - switch (opts.view.format) { - case Format::Plain: - return view_msg_plain(message, opts); - case Format::Sexp: - return view_msg_sexp(message, opts); - default: - g_critical("bug: should not be reached"); - return Err(Error::Code::Internal, "error"); - } -} - -static Mu::Result -cmd_view(const Options& opts) -{ - for (auto&& file: opts.view.files) { - 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(); -} - -static Mu::Result -cmd_mkdir(const Options& opts) -{ - for (auto&& dir: opts.mkdir.dirs) { - if (auto&& res = - maildir_mkdir(dir, opts.mkdir.mode); !res) - return res; - } - - return Ok(); -} - -static Result -cmd_add(Mu::Store& store, const Options& opts) -{ - for (auto&& file: opts.add.files) { - const auto docid{store.add_message(file)}; - if (!docid) - return Err(docid.error()); - else - g_debug("added message @ %s, docid=%u", - file.c_str(), docid.value()); - } - - return Ok(); -} - -static Result -cmd_remove(Mu::Store& store, const Options& opts) -{ - for (auto&& file: opts.remove.files) { - const auto res = store.remove_message(file); - if (!res) - return Err(Error::Code::File, "failed to remove %s", file.c_str()); - else - g_debug("removed message @ %s", file.c_str()); - } - - return Ok(); -} - - -template -static void -key_val(const Mu::MaybeAnsi& col, const std::string& key, T val) -{ - using Color = Mu::MaybeAnsi::Color; - - std::cout << col.fg(Color::BrightBlue) << std::left << std::setw(18) << key << col.reset() - << ": "; - - std::cout << col.fg(Color::Green) << val << col.reset() << "\n"; -} - - -static void -print_signature(const Mu::MimeSignature& sig, const Options& opts) -{ - Mu::MaybeAnsi col{!opts.nocolor}; - - const auto created{sig.created()}; - key_val(col, "created", - created == 0 ? "unknown" : - time_to_string("%c", sig.created()).c_str()); - - const auto expires{sig.expires()}; - key_val(col, "expires", expires==0 ? "never" : - time_to_string("%c", sig.expires()).c_str()); - - const auto cert{sig.certificate()}; - key_val(col, "public-key algo", - to_string_view_opt(cert.pubkey_algo()).value_or("unknown")); - key_val(col, "digest algo", - to_string_view_opt(cert.digest_algo()).value_or("unknown")); - key_val(col, "id-validity", - to_string_view_opt(cert.id_validity()).value_or("unknown")); - key_val(col, "trust", - to_string_view_opt(cert.trust()).value_or("unknown")); - key_val(col, "issuer-serial", cert.issuer_serial().value_or("unknown")); - key_val(col, "issuer-name", cert.issuer_name().value_or("unknown")); - key_val(col, "finger-print", cert.fingerprint().value_or("unknown")); - key_val(col, "key-id", cert.key_id().value_or("unknown")); - key_val(col, "name", cert.name().value_or("unknown")); - key_val(col, "user-id", cert.user_id().value_or("unknown")); -} - - -static bool -verify(const MimeMultipartSigned& sigpart, const Options& opts) -{ - using VFlags = MimeMultipartSigned::VerifyFlags; - const auto vflags{opts.verify.auto_retrieve ? - VFlags::EnableKeyserverLookups: VFlags::None}; - - auto ctx{MimeCryptoContext::make_gpg()}; - if (!ctx) - return false; - - const auto sigs{sigpart.verify(*ctx, vflags)}; - Mu::MaybeAnsi col{!opts.nocolor}; - - if (!sigs || sigs->empty()) { - - if (!opts.quiet) - g_print("cannot find signatures in part\n"); - - return true; - } - - bool valid{true}; - for (auto&& sig: *sigs) { - - const auto status{sig.status()}; - - if (!opts.quiet) - key_val(col, "status", to_string(status)); - - if (opts.verbose) - print_signature(sig, opts); - - if (none_of(sig.status() & MimeSignature::Status::Green)) - valid = false; - } - - 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) -{ - bool all_ok{true}; - const auto mopts = message_options(opts.verify); - - for (auto&& file: opts.verify.files) { - - auto message{Message::make_from_path(file, mopts)}; - if (!message) - return Err(message.error()); - - if (!opts.quiet && opts.verify.files.size() > 1) - g_print("verifying %s\n", file.c_str()); - - if (!verify_message(*message, opts, file)) - all_ok = false; - } - - // 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()); - - all_ok = verify_message(*message, opts, ""); - } - - if (all_ok) - return Ok(); - else - return Err(Error::Code::UnverifiedSignature, - "failed to verify one or more signatures"); -} - -static Result -cmd_info(const Mu::Store& store, const Options& opts) -{ - using namespace tabulate; - - if (!locale_workaround()) - return Err(Error::Code::User, "failed to find a working locale"); - - auto colorify = [](Table& table) { - for (auto&& row: table) { - - if (row.cells().size() < 2) - continue; - - row.cells().at(0)->format().font_style({FontStyle::bold}) - .font_color(Color::green); - row.cells().at(1)->format().font_color(Color::blue); - } - }; - - auto tstamp = [](::time_t t)->std::string { - if (t == 0) - return "never"; - else - return time_to_string("%c", t); - - }; - - Table info; - info.add_row({"maildir", store.properties().root_maildir}); - info.add_row({"database-path", store.properties().database_path}); - info.add_row({"schema-version", store.properties().schema_version}); - info.add_row({"max-message-size", format("%zu", store.properties().max_message_size)}); - info.add_row({"batch-size", format("%zu", store.properties().batch_size)}); - info.add_row({"created", tstamp(store.properties().created)}); - for (auto&& c : store.properties().personal_addresses) - info.add_row({"personal-address", c}); - - info.add_row({"messages in store", format("%zu", store.size())}); - info.add_row({"last-change", tstamp(store.statistics().last_change)}); - info.add_row({"last-index", tstamp(store.statistics().last_index)}); - - if (!opts.nocolor) - colorify(info); - - std::cout << info << '\n'; - - return Ok(); -} - -static Result -cmd_init(const Options& opts) -{ - auto store = std::invoke([&]()->Result { - - /* - * reinit - */ - if (opts.init.reinit) - return Store::make(opts.runtime_path(RuntimePath::XapianDb), - Store::Options::ReInit|Store::Options::Writable); - /* - * full init - */ - - /* not provided, nor could we find a good default */ - if (opts.init.maildir.empty()) - return Err(Error::Code::InvalidArgument, - "missing --maildir parameter and could " - "not determine default"); - - Mu::Store::Config conf{}; - conf.max_message_size = opts.init.max_msg_size.value_or(0); - conf.batch_size = opts.init.batch_size.value_or(0); - - return Store::make_new(opts.runtime_path(RuntimePath::XapianDb), - opts.init.maildir, opts.init.my_addresses, conf); - }); - - if (!store) - return Err(store.error()); - - if (!opts.quiet) { - cmd_info(*store, opts); - std::cout << "database " - << (opts.init.reinit ? "reinitialized" : "created") - << "; use the 'index' command to fill/update it.\n"; - } - return Ok(); -} - static Result cmd_find(const Options& opts) { @@ -543,13 +103,13 @@ Mu::mu_cmd_execute(const Options& opts) try { case Options::SubCommand::Fields: return mu_cmd_fields(opts); case Options::SubCommand::Mkdir: - return cmd_mkdir(opts); + return mu_cmd_mkdir(opts); case Options::SubCommand::Script: return mu_cmd_script(opts); case Options::SubCommand::View: - return cmd_view(opts); + return mu_cmd_view(opts); case Options::SubCommand::Verify: - return cmd_verify(opts); + return mu_cmd_verify(opts); case Options::SubCommand::Extract: return mu_cmd_extract(opts); /* @@ -561,20 +121,20 @@ Mu::mu_cmd_execute(const Options& opts) try { case Options::SubCommand::Find: return cmd_find(opts); case Options::SubCommand::Info: - return with_readonly_store(cmd_info, opts); + return with_readonly_store(mu_cmd_info, opts); /* writable store */ case Options::SubCommand::Add: - return with_writable_store(cmd_add, opts); + return with_writable_store(mu_cmd_add, opts); case Options::SubCommand::Remove: - return with_writable_store(cmd_remove, opts); + return with_writable_store(mu_cmd_remove, opts); case Options::SubCommand::Index: return with_writable_store(mu_cmd_index, opts); /* commands instantiate store themselves */ case Options::SubCommand::Init: - return cmd_init(opts); + return mu_cmd_init(opts); case Options::SubCommand::Server: return mu_cmd_server(opts); diff --git a/mu/mu-cmd.hh b/mu/mu-cmd.hh index e98d3c71..9fcfa9ae 100644 --- a/mu/mu-cmd.hh +++ b/mu/mu-cmd.hh @@ -50,17 +50,25 @@ message_options(const CmdOpts& cmdopts) return mopts; } - - /** - * execute the 'find' command + * execute the 'add' command * * @param store store object to use * @param opts configuration options * * @return Ok() or some error */ -Result mu_cmd_find(const Store& store, const Options& opts); +Result mu_cmd_add(Store& store, const Options& opts); + +/** + * execute the 'cfind' command + * + * @param store store object to use + * @param opts configuration options + * + * @return Ok() or some error + */ +Result mu_cmd_cfind(const Store& store, const Options& opts); /** * execute the 'extract' command @@ -81,24 +89,14 @@ Result mu_cmd_extract(const Options& opts); Result mu_cmd_fields(const Options& opts); /** - * execute the 'script' command - * - * @param opts configuration options - * @param err receives error information, or NULL - * - * @return Ok() or some error - */ -Result mu_cmd_script(const Options& opts); - -/** - * execute the cfind command + * execute the 'find' command * * @param store store object to use * @param opts configuration options * * @return Ok() or some error */ -Result mu_cmd_cfind(const Store& store, const Options& opts); +Result mu_cmd_find(const Store& store, const Options& opts); /** * execute the 'index' command @@ -110,6 +108,55 @@ Result mu_cmd_cfind(const Store& store, const Options& opts); */ Result mu_cmd_index(Store& store, const Options& opt); +/** + * execute the 'info' command + * + * @param store message store object. + * @param opts configuration options + * + * @return Ok() or some error + */ +Result mu_cmd_info(const Mu::Store& store, const Options& opts); + +/** + * execute the 'init' command + * + * @param opts configuration options + * + * @return Ok() or some error + */ +Result mu_cmd_init(const Options& opts); + +/** + * execute the 'mkdir' command + * + * @param opts configuration options + * + * @return Ok() or some error + */ +Result mu_cmd_mkdir(const Options& opts); + +/** + * execute the 'remove' command + * + * @param store store object to use + * @param opts configuration options + * + * @return Ok() or some error + */ +Result mu_cmd_remove(Store& store, const Options& opt); + +/** + * execute the 'script' command + * + * @param opts configuration options + * @param err receives error information, or NULL + * + * @return Ok() or some error + */ +Result mu_cmd_script(const Options& opts); + + /** * execute the server command * @param opts configuration options @@ -119,6 +166,24 @@ Result mu_cmd_index(Store& store, const Options& opt); */ Result mu_cmd_server(const Options& opts); +/** + * execute the 'verify' command + * + * @param opts configuration options + * + * @return Ok() or some error + */ +Mu::Result mu_cmd_verify(const Options& opts); + +/** + * execute the 'view' command + * + * @param opts configuration options + * + * @return Ok() or some error + */ +Mu::Result mu_cmd_view(const Options& opts); + /** * execute some mu command, based on 'opts' *