/* ** Copyright (C) 2020 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-msg-fields.h" #include "mu-server.hh" #include #include #include #include #include #include #include #include #include #include #include "mu-runtime.hh" #include "mu-maildir.hh" #include "mu-query.hh" #include "index/mu-indexer.hh" #include "mu-store.hh" #include "mu-msg-part.hh" #include "mu-contacts.hh" #include "utils/mu-str.h" #include "utils/mu-utils.hh" #include "utils/mu-command-parser.hh" #include "utils/mu-readline.hh" using namespace Mu; using namespace Command; /// @brief object to manage the server-context for all commands. struct Server::Private { Private(Store& store, Output output): store_{store}, output_{output}, command_map_{make_command_map()}, query_{store_}, keep_going_{true} {} // // construction helpers // CommandMap make_command_map(); // // acccessors Store& store() { return store_; } const Store& store() const { return store_; } Indexer& indexer() { return store().indexer(); } const CommandMap& command_map() const { return command_map_; } const Query& query() const { return query_; } // // invoke // bool invoke (const std::string& expr) noexcept; // // output // void output_sexp(Sexp&& sexp) const { if (output_) output_(std::move(sexp)); } void output_sexp(Sexp::List&& lst) const { output_sexp(Sexp::make_list(std::move(lst))); } size_t output_sexp (const QueryResults& qres); // // handlers for various commands. // void add_handler (const Parameters& params); void compose_handler (const Parameters& params); void contacts_handler (const Parameters& params); void extract_handler (const Parameters& params); void find_handler (const Parameters& params); void help_handler (const Parameters& params); void index_handler (const Parameters& params); void move_handler (const Parameters& params); void mkdir_handler (const Parameters& params); void ping_handler (const Parameters& params); void quit_handler (const Parameters& params); void remove_handler (const Parameters& params); void sent_handler (const Parameters& params); void view_handler (const Parameters& params); private: // helpers Sexp build_message_sexp(MuMsg *msg, unsigned docid, const Option qm, MuMsgOptions opts); Sexp::List move_docid (Store::Id docid, const std::string& flagstr, bool new_name, bool no_view); Sexp::List perform_move (Store::Id docid, MuMsg *msg, const std::string& maildirarg, MuFlags flags, bool new_name, bool no_view); Store& store_; Server::Output output_; const CommandMap command_map_; const Query query_; std::atomic keep_going_{}; }; static void add_thread_info (Sexp::List& items, const QueryMatch& qmatch) { Sexp::List info; auto symbol_t = []{return Sexp::make_symbol("t");}; info.add_prop(":path", Sexp::make_string(qmatch.thread_path)); info.add_prop(":level", Sexp::make_number(qmatch.thread_level)); info.add_prop(":date", Sexp::make_string(qmatch.thread_date)); Sexp::List dlist; const auto td {::atoi(qmatch.thread_date.c_str())}; dlist.add(Sexp::make_number((unsigned)(td >> 16))); dlist.add(Sexp::make_number((unsigned)(td & 0xffff))); dlist.add(Sexp::make_number(0)); info.add_prop(":date-tstamp", Sexp::make_list(std::move(dlist))); if (qmatch.has_flag(QueryMatch::Flags::Root)) info.add_prop(":root", symbol_t()); if (qmatch.has_flag(QueryMatch::Flags::Related)) info.add_prop(":related", symbol_t()); if (qmatch.has_flag(QueryMatch::Flags::First)) info.add_prop(":first-child", symbol_t()); if (qmatch.has_flag(QueryMatch::Flags::Last)) info.add_prop(":last-child", symbol_t()); if (qmatch.has_flag(QueryMatch::Flags::Orphan)) info.add_prop(":orphan", symbol_t()); if (qmatch.has_flag(QueryMatch::Flags::Duplicate)) info.add_prop(":duplicate", symbol_t()); if (qmatch.has_flag(QueryMatch::Flags::HasChild)) info.add_prop(":has-child", symbol_t()); if (qmatch.has_flag(QueryMatch::Flags::ThreadSubject)) info.add_prop(":thread-subject", symbol_t()); items.add_prop(":thread", Sexp::make_list(std::move(info))); } Sexp Server::Private::build_message_sexp (MuMsg *msg, unsigned docid, const Option qm, MuMsgOptions opts) { auto lst{Mu::msg_to_sexp_list(msg, docid, opts)}; if (qm) add_thread_info(lst, *qm); return Sexp::make_list(std::move(lst)); } CommandMap Server::Private::make_command_map () { CommandMap cmap; using Type = Sexp::Type; cmap.emplace("add", CommandInfo{ ArgMap{ {":path", ArgInfo{Type::String, true, "file system path to the message" }}}, "add a message to the store", [&](const auto& params){add_handler(params);}}); cmap.emplace("compose", CommandInfo{ ArgMap{{":type", ArgInfo{Type::Symbol, true, "type of composition: reply/forward/edit/resend/new"}}, {":docid", ArgInfo{Type::Number, false, "document id of parent-message, if any"}}, {":decrypt", ArgInfo{Type::Symbol, false, "whether to decrypt encrypted parts (if any)" }}}, "compose a new message", [&](const auto& params){compose_handler(params);}}); cmap.emplace("contacts", CommandInfo{ ArgMap{ {":personal", ArgInfo{Type::Symbol, false, "only personal contacts" }}, {":after", ArgInfo{Type::String, false, "only contacts seen after time_t string" }}, {":tstamp", ArgInfo{Type::String, false, "return changes since tstamp" }}}, "get contact information", [&](const auto& params){contacts_handler(params);}}); cmap.emplace("extract", CommandInfo{ ArgMap{{":docid", ArgInfo{Type::Number, true, "document for the message" }}, {":index", ArgInfo{Type::Number, true, "index for the part to operate on" }}, {":action", ArgInfo{Type::Symbol, true, "what to do with the part" }}, {":decrypt", ArgInfo{Type::Symbol, false, "whether to decrypt encrypted parts (if any)" }}, {":path", ArgInfo{Type::String, false, "part for saving (for action: save)" }}, {":what", ArgInfo{Type::Symbol, false, "what to do with the part (feedback)" }}, {":param", ArgInfo{Type::String, false, "parameter for 'what'" }}}, "extract mime-parts from a message", [&](const auto& params){extract_handler(params);}}); cmap.emplace("find", CommandInfo{ ArgMap{ {":query", ArgInfo{Type::String, true, "search expression" }}, {":threads", ArgInfo{Type::Symbol, false, "whether to include threading information" }}, {":sortfield", ArgInfo{Type::Symbol, false, "the field to sort results by" }}, {":descending", ArgInfo{Type::Symbol, false, "whether to sort in descending order" }}, {":maxnum", ArgInfo{Type::Number, false, "maximum number of result (hint)" }}, {":skip-dups", ArgInfo{Type::Symbol, false, "whether to skip messages with duplicate message-ids" }}, {":include-related", ArgInfo{Type::Symbol, false, "whether to include other message related to matching ones" }}}, "query the database for messages", [&](const auto& params){find_handler(params);}}); cmap.emplace("help", CommandInfo{ ArgMap{ {":command", ArgInfo{Type::Symbol, false, "command to get information for" }}, {":full", ArgInfo{Type::Symbol, false, "show full descriptions" }}}, "get information about one or all commands", [&](const auto& params){help_handler(params);}}); cmap.emplace("index", CommandInfo{ ArgMap{ {":my-addresses", ArgInfo{Type::List, false, "list of 'my' addresses"}}, {":cleanup", ArgInfo{Type::Symbol, false, "whether to remove stale messages from the store"}}, {":lazy-check", ArgInfo{Type::Symbol, false, "whether to avoid indexing up-to-date directories"}}}, "scan maildir for new/updated/removed messages", [&](const auto& params){index_handler(params);}}); cmap.emplace("move", CommandInfo{ ArgMap{ {":docid", ArgInfo{Type::Number, false, "document-id"}}, {":msgid", ArgInfo{Type::String, false, "message-id"}}, {":flags", ArgInfo{Type::String, false, "new flags for the message"}}, {":maildir", ArgInfo{Type::String, false, "the target maildir" }}, {":rename", ArgInfo{Type::Symbol, false, "change filename when moving" }}, {":no-view", ArgInfo{Type::Symbol, false, "if set, do not hint at updating the view"}},}, "move messages and/or change their flags", [&](const auto& params){move_handler(params);}}); cmap.emplace("mkdir", CommandInfo{ ArgMap{ {":path", ArgInfo{Type::String, true, "location for the new maildir" }}}, "create a new maildir", [&](const auto& params){mkdir_handler(params);}}); cmap.emplace("ping", CommandInfo{ ArgMap{ {":queries", ArgInfo{Type::List, false, "queries for which to get read/unread numbers"}}, {":skip-dups", ArgInfo{Type::Symbol, false, "whether to exclude messages with duplicate message-ids"}},}, "ping the mu-server and get information in response", [&](const auto& params){ping_handler(params);}}); cmap.emplace("quit", CommandInfo{{}, "quit the mu server", [&](const auto& params){quit_handler(params);}}); cmap.emplace("remove", CommandInfo{ ArgMap{ {":docid", ArgInfo{Type::Number, true, "document-id for the message to remove" }}}, "remove a message from filesystem and database", [&](const auto& params){remove_handler(params);}}); cmap.emplace("sent", CommandInfo{ ArgMap{ {":path", ArgInfo{Type::String, true, "path to the message file" }} }, "tell mu about a message that was sent", [&](const auto& params){sent_handler(params);}}); cmap.emplace("view", CommandInfo{ ArgMap{{":docid", ArgInfo{Type::Number, false, "document-id"}}, {":msgid", ArgInfo{Type::String, false, "message-id"}}, {":path", ArgInfo{Type::String, false, "message filesystem path"}}, {":mark-as-read", ArgInfo{Type::Symbol, false, "mark message as read (if not already)"}}, {":extract-images", ArgInfo{Type::Symbol, false, "whether to extract images for this messages (if any)"}}, {":decrypt", ArgInfo{Type::Symbol, false, "whether to decrypt encrypted parts (if any)" }}, {":verify", ArgInfo{Type::Symbol, false, "whether to verify signatures (if any)" }} }, "view a message. exactly one of docid/msgid/path must be specified", [&](const auto& params){view_handler(params);}}); return cmap; } G_GNUC_PRINTF(2,3) static Sexp make_error (Error::Code errcode, const char* frm, ...) { char *msg{}; va_list ap; va_start (ap, frm); g_vasprintf (&msg, frm, ap); va_end (ap); Sexp::List err; err.add_prop(":error", Sexp::make_number(static_cast(errcode))); err.add_prop(":message", Sexp::make_string(msg)); g_free (msg); return Sexp::make_list(std::move(err)); } bool Server::Private::invoke (const std::string& expr) noexcept { if (!keep_going_) return false; try { auto call{Sexp::Sexp::make_parse(expr)}; Command::invoke(command_map(), call); } catch (const Mu::Error& me) { output_sexp(make_error(me.code(), "%s", me.what())); } catch (const std::runtime_error& re) { output_sexp(make_error(Error::Code::Internal, "caught exception: %s", re.what())); keep_going_ = false; } catch (...) { output_sexp(make_error(Error::Code::Internal, "something went wrong: quiting")); keep_going_ = false; } return keep_going_; } static MuMsgOptions message_options (const Parameters& params) { const auto extract_images{get_bool_or(params, ":extract-images", false)}; const auto decrypt{get_bool_or(params, ":decrypt", false)}; const auto verify{get_bool_or(params, ":verify", false)}; int opts{MU_MSG_OPTION_NONE}; if (extract_images) opts |= MU_MSG_OPTION_EXTRACT_IMAGES; if (verify) opts |= MU_MSG_OPTION_VERIFY | MU_MSG_OPTION_USE_AGENT; if (decrypt) opts |= MU_MSG_OPTION_DECRYPT | MU_MSG_OPTION_USE_AGENT; return (MuMsgOptions)opts; } /* 'add' adds a message to the database, and takes two parameters: 'path', which * is the full path to the message, and 'maildir', which is the maildir this * message lives in (e.g. "/inbox"). response with an (:info ...) message with * information about the newly added message (details: see code below) */ void Server::Private::add_handler (const Parameters& params) { auto path{get_string_or(params, ":path")}; const auto docid{store().add_message(path)}; Sexp::List expr; expr.add_prop(":info", Sexp::make_symbol("add")); expr.add_prop(":path", Sexp::make_string(path)); expr.add_prop(":docid", Sexp::make_number(docid)); output_sexp(Sexp::make_list(std::move(expr))); auto msg{store().find_message(docid)}; if (!msg) throw Error(Error::Code::Store, "failed to get message at %s (docid=%u)", path.c_str(), docid); Sexp::List update; update.add_prop(":update", build_message_sexp(msg, docid, {}, MU_MSG_OPTION_VERIFY)); output_sexp(Sexp::make_list(std::move(update))); mu_msg_unref(msg); } struct PartInfo { Sexp::List attseq; MuMsgOptions opts; }; static void each_part (MuMsg *msg, MuMsgPart *part, PartInfo *pinfo) { /* exclude things that don't look like proper attachments, unless they're images */ if (!mu_msg_part_maybe_attachment(part)) return; GError *gerr{}; char *cachefile = mu_msg_part_save_temp ( msg, (MuMsgOptions)(pinfo->opts|MU_MSG_OPTION_OVERWRITE), part->index, &gerr); if (!cachefile) throw Error (Error::Code::File, &gerr, "failed to save part"); Sexp::List pi; pi.add_prop(":file-name", Sexp::make_string(cachefile)); pi.add_prop(":mime-type", Sexp::make_string(format("%s/%s", part->type, part->subtype))); pinfo->attseq.add(Sexp::make_list(std::move(pi))); g_free (cachefile); } /* 'compose' produces the un-changed *original* message sexp (ie., the message * to reply to, forward or edit) for a new message to compose). It takes two * parameters: 'type' with the compose type (either reply, forward or * edit/resend), and 'docid' for the message to reply to. Note, type:new does * not have an original message, and therefore does not need a docid * * In returns a (:compose [:original ] [:include] ) * message (detals: see code below) * * Note ':include' t or nil determines whether to include attachments */ void Server::Private::compose_handler (const Parameters& params) { const auto ctype{get_symbol_or(params, ":type")}; Sexp::List comp_lst; comp_lst.add_prop(":compose", Sexp::make_symbol(std::string(ctype))); if (ctype == "reply" || ctype == "forward" || ctype == "edit" || ctype == "resend") { GError *gerr{}; const unsigned docid{(unsigned)get_int_or(params, ":docid")}; auto msg{store().find_message(docid)}; if (!msg) throw Error{Error::Code::Store, &gerr, "failed to get message %u", docid}; const auto opts{message_options(params)}; comp_lst.add_prop(":original", build_message_sexp(msg, docid, {}, opts)); if (ctype == "forward") { PartInfo pinfo{}; pinfo.opts = opts; mu_msg_part_foreach (msg, opts, (MuMsgPartForeachFunc)each_part, &pinfo); if (!pinfo.attseq.empty()) comp_lst.add_prop (":include", Sexp::make_list(std::move(pinfo.attseq))); } mu_msg_unref (msg); } else if (ctype != "new") throw Error(Error::Code::InvalidArgument, "invalid compose type '%s'", ctype.c_str()); output_sexp (std::move(comp_lst)); } void Server::Private::contacts_handler (const Parameters& params) { const auto personal = get_bool_or(params, ":personal"); const auto afterstr = get_string_or(params, ":after"); const auto tstampstr = get_string_or(params, ":tstamp"); const auto after{afterstr.empty() ? 0 : g_ascii_strtoll(date_to_time_t_string(afterstr, true).c_str(), {}, 10)}; const auto tstamp = g_ascii_strtoll (tstampstr.c_str(), NULL, 10); auto rank{0}; Sexp::List contacts; store().contacts().for_each([&](const ContactInfo& ci) { rank++; /* since the last time we got some contacts */ if (tstamp > ci.tstamp) return; /* (maybe) only include 'personal' contacts */ if (personal && !ci.personal) return; /* only include newer-than-x contacts */ if (after > ci.last_seen) return; Sexp::List contact; contact.add_prop(":address", Sexp::make_string(ci.full_address)); contact.add_prop(":rank", Sexp::make_number(rank)); contacts.add(Sexp::make_list(std::move(contact))); }); Sexp::List seq; seq.add_prop(":contacts", Sexp::make_list(std::move(contacts))); seq.add_prop(":tstamp", Sexp::make_string(format("%" G_GINT64_FORMAT, g_get_monotonic_time()))); /* dump the contacts cache as a giant sexp */ output_sexp(std::move(seq)); } static Sexp::List save_part (MuMsg *msg, unsigned docid, unsigned index, MuMsgOptions opts, const Parameters& params) { const auto path{get_string_or(params, ":path")}; if (path.empty()) throw Error{Error::Code::Command, "missing path"}; GError *gerr{}; if (!mu_msg_part_save (msg, (MuMsgOptions)(opts | (int)MU_MSG_OPTION_OVERWRITE), path.c_str(), index, &gerr)) throw Error{Error::Code::File, &gerr, "failed to save part"}; Sexp::List seq; seq.add_prop(":info", Sexp::make_symbol("save")); seq.add_prop(":message", Sexp::make_string(format("%s has been saved", path.c_str()))); return seq; } static Sexp::List open_part (MuMsg *msg, unsigned docid, unsigned index, MuMsgOptions opts) { GError *gerr{}; char *targetpath{mu_msg_part_get_cache_path (msg, opts, index, &gerr)}; if (!targetpath) throw Error{Error::Code::File, &gerr, "failed to get cache-path"}; if (!mu_msg_part_save (msg, (MuMsgOptions)(opts | MU_MSG_OPTION_USE_EXISTING), targetpath, index, &gerr)) { g_free(targetpath); throw Error{Error::Code::File, &gerr, "failed to save to cache-path"}; } if (!mu_util_play (targetpath, TRUE,/*allow local*/ FALSE/*allow remote*/, &gerr)) { g_free(targetpath); throw Error{Error::Code::File, &gerr, "failed to play"}; } Sexp::List seq; seq.add_prop(":info", Sexp::make_symbol("open")); seq.add_prop(":message", Sexp::make_string(format("%s has been opened", targetpath))); g_free (targetpath); return seq; } static Sexp::List temp_part (MuMsg *msg, unsigned docid, unsigned index, MuMsgOptions opts, const Parameters& params) { const auto what{get_symbol_or(params, ":what")}; if (what.empty()) throw Error{Error::Code::Command, "missing 'what'"}; const auto param{get_string_or(params, ":param")}; GError *gerr{}; char *path{mu_msg_part_get_cache_path (msg, opts, index, &gerr)}; if (!path) throw Error{Error::Code::File, &gerr, "could not get cache path"}; if (!mu_msg_part_save (msg, (MuMsgOptions)(opts | MU_MSG_OPTION_USE_EXISTING), path, index, &gerr)) { g_free(path); throw Error{Error::Code::File, &gerr, "saving failed"}; } Sexp::List lst; lst.add_prop(":temp", Sexp::make_string(path)); lst.add_prop(":what", Sexp::make_string(what)); lst.add_prop(":docid", Sexp::make_number(docid)); if (!param.empty()) lst.add_prop(":param", Sexp::make_string(param)); g_free(path); return lst; } /* 'extract' extracts some mime part from a message */ void Server::Private::extract_handler (const Parameters& params) { const auto docid{get_int_or(params, ":docid")}; const auto index{get_int_or(params, ":index")}; const auto opts{message_options(params)}; auto msg{store().find_message(docid)}; if (!msg) throw Error{Error::Code::Store, "failed to get message"}; try { const auto action{get_symbol_or(params, ":action")}; if (action == "save") output_sexp(save_part (msg, docid, index, opts, params)); else if (action == "open") output_sexp(open_part (msg, docid, index, opts)); else if (action == "temp") output_sexp(temp_part (msg, docid, index, opts, params)); else { throw Error{Error::Code::InvalidArgument, "unknown action '%s'", action.c_str()}; } } catch (...) { mu_msg_unref (msg); throw; } } /* get a *list* of all messages with the given message id */ static std::vector docids_for_msgid (const Query& q, const std::string& msgid, size_t max=100) { if (msgid.size() > Store::MaxTermLength) { throw Error(Error::Code::InvalidArgument, "invalid message-id '%s'", msgid.c_str()); } const auto xprefix{mu_msg_field_shortcut(MU_MSG_FIELD_ID_MSGID)}; /*XXX this is a bit dodgy */ auto tmp{g_ascii_strdown(msgid.c_str(), -1)}; auto expr{g_strdup_printf("%c:%s", xprefix, tmp)}; g_free(tmp); GError *gerr{}; const auto res{q.run(expr, MU_MSG_FIELD_ID_NONE, QueryFlags::None, max)}; g_free (expr); if (!res) throw Error(Error::Code::Store, &gerr, "failed to run msgid-query"); else if (res->empty()) throw Error(Error::Code::NotFound, "could not find message(s) for msgid %s", msgid.c_str()); std::vector docids{}; for(auto&& mi: *res) docids.emplace_back(mi.doc_id()); return docids; } /* * creating a message object just to get a path seems a bit excessive maybe * mu_store_get_path could be added if this turns out to be a problem */ static std::string path_from_docid (const Store& store, unsigned docid) { auto msg{store.find_message(docid)}; if (!msg) throw Error(Error::Code::Store, "could not get message from store"); auto p{mu_msg_get_path(msg)}; if (!p) { mu_msg_unref(msg); throw Error(Error::Code::Store, "could not get path for message %u", docid); } std::string msgpath{p}; mu_msg_unref (msg); return msgpath; } static std::vector determine_docids (const Query& q, const Parameters& params) { auto docid{get_int_or(params, ":docid", 0)}; const auto msgid{get_string_or(params, ":msgid")}; if ((docid == 0) == msgid.empty()) throw Error(Error::Code::InvalidArgument, "precisely one of docid and msgid must be specified"); if (docid != 0) return { (unsigned)docid }; else return docids_for_msgid (q, msgid.c_str()); } size_t Server::Private::output_sexp (const QueryResults& qres) { size_t n{}; for (auto&& mi: qres) { ++n; auto msg{mi.floating_msg()}; if (!msg) continue; auto qm{mi.query_match()}; output_sexp(build_message_sexp(msg, mi.doc_id(), qm, MU_MSG_OPTION_HEADERS_ONLY)); } return n; } void Server::Private::find_handler (const Parameters& params) { const auto q{get_string_or(params, ":query")}; const auto threads{get_bool_or(params, ":threads", false)}; const auto sortfieldstr{get_symbol_or(params, ":sortfield", "")}; const auto descending{get_bool_or(params, ":descending", false)}; const auto maxnum{get_int_or(params, ":maxnum", -1/*unlimited*/)}; const auto skip_dups{get_bool_or(params, ":skip-dups", false)}; const auto include_related{get_bool_or(params, ":include-related", false)}; MuMsgFieldId sort_field{MU_MSG_FIELD_ID_NONE}; if (!sortfieldstr.empty()) { sort_field = mu_msg_field_id_from_name ( sortfieldstr.c_str() + 1, FALSE); // skip ':' if (sort_field == MU_MSG_FIELD_ID_NONE) throw Error{Error::Code::InvalidArgument, "invalid sort field %s", sortfieldstr.c_str()}; } auto qflags{QueryFlags::None}; if (descending) qflags |= QueryFlags::Descending; if (skip_dups) qflags |= QueryFlags::SkipDuplicates; if (include_related) qflags |= QueryFlags::IncludeRelated; if (threads) qflags |= QueryFlags::Threading; auto qres{query().run(q, sort_field, qflags, maxnum)}; if (!qres) throw Error(Error::Code::Query, "failed to run query"); /* before sending new results, send an 'erase' message, so the frontend * knows it should erase the headers buffer. this will ensure that the * output of two finds will not be mixed. */ { Sexp::List lst; lst.add_prop(":erase", Sexp::make_symbol("t")); output_sexp(std::move(lst)); } { const auto foundnum{output_sexp (*qres)}; Sexp::List lst; lst.add_prop(":found", Sexp::make_number(foundnum)); output_sexp(std::move(lst)); } } void Server::Private::help_handler (const Parameters& params) { const auto command{get_symbol_or(params, ":command", "")}; const auto full{get_bool_or(params, ":full", !command.empty())}; if (command.empty()) { std::cout << ";; Commands are s-expressions of the form\n" << ";; ( :param1 val1 :param2 val2 ...)\n" << ";; For instance:\n;; (help :command quit)\n" << ";; to get detailed information about the 'quit'\n;;\n"; std::cout << ";; The following commands are available:\n\n"; } std::vector names; for (auto&& name_cmd: command_map()) names.emplace_back(name_cmd.first); std::sort(names.begin(), names.end()); for (auto&& name: names) { const auto& info{command_map().find(name)->second}; if (!command.empty() && name != command) continue; if (!command.empty()) std::cout << ";; " << format("%-10s -- %s\n", name.c_str(), info.docstring.c_str()); else std::cout << ";; " << name.c_str() << " -- " << info.docstring.c_str() << '\n'; if (!full) continue; for (auto&& argname: info.sorted_argnames()) { const auto& arg{info.args.find(argname)}; std::cout << ";; " << format("%-17s : %-24s ", arg->first.c_str(), to_string(arg->second).c_str()); std::cout << " " << arg->second.docstring << "\n"; } std::cout << ";;\n"; } } static Sexp::List get_stats (const Indexer::Progress& stats, const std::string& state) { Sexp::List lst; lst.add_prop(":info", Sexp::make_symbol("index")); lst.add_prop(":status", Sexp::make_symbol(std::string{state})); lst.add_prop(":processed", Sexp::make_number(stats.processed)); lst.add_prop(":updated", Sexp::make_number(stats.updated)); lst.add_prop(":cleaned-up", Sexp::make_number(stats.removed)); return lst; } void Server::Private::index_handler (const Parameters& params) { Mu::Indexer::Config conf{}; conf.cleanup = get_bool_or(params, ":cleanup"); conf.lazy_check = get_bool_or(params, ":lazy-check"); indexer().stop(); indexer().start(conf); while (indexer().is_running()) { std::this_thread::sleep_for(std::chrono::milliseconds(1000)); output_sexp(get_stats(indexer().progress(), "running")); } output_sexp(get_stats(indexer().progress(), "complete")); } void Server::Private::mkdir_handler (const Parameters& params) { const auto path{get_string_or(params, ":path")}; GError *gerr{}; if (!mu_maildir_mkdir(path.c_str(), 0755, FALSE, &gerr)) throw Error{Error::Code::File, &gerr, "failed to create maildir"}; Sexp::List lst; lst.add_prop(":info", Sexp::make_string("mkdir")); lst.add_prop(":message", Sexp::make_string(format("%s has been created", path.c_str()))); output_sexp(std::move(lst)); } static MuFlags get_flags (const std::string& path, const std::string& flagstr) { if (flagstr.empty()) return MU_FLAG_NONE; /* ie., ignore flags */ else { /* if there's a '+' or '-' sign in the string, it must * be a flag-delta */ if (strstr (flagstr.c_str(), "+") || strstr (flagstr.c_str(), "-")) { auto oldflags = mu_maildir_get_flags_from_path (path.c_str()); return mu_flags_from_str_delta (flagstr.c_str(), oldflags, MU_FLAG_TYPE_ANY); } else return mu_flags_from_str (flagstr.c_str(), MU_FLAG_TYPE_ANY, TRUE /*ignore invalid*/); } } Sexp::List Server::Private::perform_move (Store::Id docid, MuMsg *msg, const std::string& maildirarg, MuFlags flags, bool new_name, bool no_view) { bool different_mdir{}; auto maildir{maildirarg}; if (maildir.empty()) { maildir = mu_msg_get_maildir (msg); different_mdir = false; } else /* are we moving to a different mdir, or is it just flags? */ different_mdir = maildir != mu_msg_get_maildir(msg); GError* gerr{}; if (!mu_msg_move_to_maildir (msg, maildir.c_str(), flags, TRUE, new_name, &gerr)) throw Error{Error::Code::File, &gerr, "failed to move message"}; /* after mu_msg_move_to_maildir, path will be the *new* path, and flags and maildir fields * will be updated as wel */ if (!store_.update_message (msg, docid)) throw Error{Error::Code::Store, "failed to store updated message"}; Sexp::List seq; seq.add_prop(":update", build_message_sexp (msg, docid, {}, MU_MSG_OPTION_VERIFY)); /* note, the :move t thing is a hint to the frontend that it * could remove the particular header */ if (different_mdir) seq.add_prop(":move", Sexp::make_symbol("t")); if (!no_view) seq.add_prop(":maybe-view", Sexp::make_symbol("t")); return seq; } Sexp::List Server::Private::move_docid (Store::Id docid, const std::string& flagstr, bool new_name, bool no_view) { if (docid == Store::InvalidId) throw Error{Error::Code::InvalidArgument, "invalid docid"}; auto msg{store_.find_message(docid)}; try { if (!msg) throw Error{Error::Code::Store, "failed to get message from store"}; const auto flags = flagstr.empty() ? mu_msg_get_flags (msg) : get_flags (mu_msg_get_path(msg), flagstr); if (flags == MU_FLAG_INVALID) throw Error{Error::Code::InvalidArgument, "invalid flags '%s'", flagstr.c_str()}; auto lst = perform_move(docid, msg, "", flags, new_name, no_view); mu_msg_unref (msg); return lst; } catch (...) { if (msg) mu_msg_unref (msg); throw; } } /* * 'move' moves a message to a different maildir and/or changes its * flags. parameters are *either* a 'docid:' or 'msgid:' pointing to * the message, a 'maildir:' for the target maildir, and a 'flags:' * parameter for the new flags. * * returns an (:update ) * */ void Server::Private::move_handler (const Parameters& params) { auto maildir{get_string_or(params, ":maildir")}; const auto flagstr{get_string_or(params, ":flags")}; const auto rename{get_bool_or (params, ":rename")}; const auto no_view{get_bool_or (params, ":noupdate")}; const auto docids{determine_docids (query(), params)}; if (docids.size() > 1) { if (!maildir.empty()) // ie. duplicate message-ids. throw Mu::Error{Error::Code::Store, "can't move multiple messages at the same time"}; // multi. for (auto&& docid: docids) output_sexp(move_docid(docid, flagstr, rename, no_view)); return; } auto docid{docids.at(0)}; GError *gerr{}; auto msg{store().find_message(docid)}; if (!msg) throw Error{Error::Code::InvalidArgument, &gerr, "could not create message"}; /* if maildir was not specified, take the current one */ if (maildir.empty()) maildir = mu_msg_get_maildir (msg); /* determine the real target flags, which come from the flags-parameter * we received (ie., flagstr), if any, plus the existing message * flags. */ MuFlags flags{}; if (!flagstr.empty()) flags = get_flags (mu_msg_get_path(msg), flagstr.c_str()); else flags = mu_msg_get_flags (msg); if (flags == MU_FLAG_INVALID) { mu_msg_unref(msg); throw Error{Error::Code::InvalidArgument, "invalid flags"}; } try { output_sexp(perform_move(docid, msg, maildir, flags, rename, no_view)); } catch (...) { mu_msg_unref(msg); throw; } mu_msg_unref(msg); } void Server::Private::ping_handler (const Parameters& params) { const auto storecount{store().size()}; if (storecount == (unsigned)-1) throw Error{Error::Code::Store, "failed to read store"}; const auto queries = get_string_vec (params, ":queries"); Sexp::List qresults; for (auto&& q: queries) { const auto count{query().count(q)}; const auto unreadq{format("flag:unread AND (%s)", q.c_str())}; const auto unread{query().count(unreadq)}; Sexp::List lst; lst.add_prop(":query", Sexp::make_string(q)); lst.add_prop(":count", Sexp::make_number(count)); lst.add_prop(":unread", Sexp::make_number(unread)); qresults.add(Sexp::make_list(std::move(lst))); } Sexp::List addrs; for (auto&& addr: store().metadata().personal_addresses) addrs.add(Sexp::make_string(addr)); Sexp::List lst; lst.add_prop(":pong", Sexp::make_string("mu")); Sexp::List proplst; proplst.add_prop(":version", Sexp::make_string(VERSION)); proplst.add_prop(":personal-addresses", Sexp::make_list(std::move(addrs))); proplst.add_prop(":database-path", Sexp::make_string(store().metadata().database_path)); proplst.add_prop(":root-maildir", Sexp::make_string(store().metadata().root_maildir)); proplst.add_prop(":doccount", Sexp::make_number(storecount)); proplst.add_prop(":queries", Sexp::make_list(std::move(qresults))); lst.add_prop(":props", Sexp::make_list(std::move(proplst))); output_sexp(std::move(lst)); } void Server::Private::quit_handler (const Parameters& params) { keep_going_ = false; } void Server::Private::remove_handler (const Parameters& params) { const auto docid{get_int_or(params, ":docid")}; const auto path{path_from_docid (store(), docid)}; if (::unlink (path.c_str()) != 0 && errno != ENOENT) throw Error(Error::Code::File, "could not delete %s: %s", path.c_str(), g_strerror (errno)); if (!store().remove_message (path)) g_warning("failed to remove message @ %s (%d) from store", path.c_str(), docid); // act as if it worked. Sexp::List lst; lst.add_prop(":remove", Sexp::make_number(docid)); output_sexp(std::move(lst)); } void Server::Private::sent_handler (const Parameters& params) { const auto path{get_string_or(params, ":path")}; const auto docid{store().add_message(path)}; if (docid == Store::InvalidId) throw Error{Error::Code::Store, "failed to add path"}; Sexp::List lst; lst.add_prop (":sent", Sexp::make_symbol("t")); lst.add_prop (":path", Sexp::make_string(path)); lst.add_prop (":docid", Sexp::make_number(docid)); output_sexp (std::move(lst)); } static bool maybe_mark_as_read (Mu::Store& store, MuMsg *msg, Store::Id docid) { if (!msg) throw Error{Error::Code::Store, "missing message"}; if (docid == Store::InvalidId) throw Error{Error::Code::Store, "invalid docid"}; const auto oldflags{mu_msg_get_flags (msg)}; const auto newflags{get_flags (mu_msg_get_path(msg), "+S-u-N")}; if (oldflags == newflags) return false; // nothing to do. GError* gerr{}; if (!mu_msg_move_to_maildir (msg, mu_msg_get_maildir (msg), newflags, TRUE, FALSE,/*new_name,*/ &gerr)) throw Error{Error::Code::File, &gerr, "failed to move message"}; /* after mu_msg_move_to_maildir, path will be the *new* path, and flags * and maildir fields will be updated as wel */ if (!store.update_message (msg, docid)) throw Error{Error::Code::Store, "failed to store updated message"}; g_debug ("marked message %d as read => %s", docid, mu_msg_get_path(msg)); return true; } void Server::Private::view_handler (const Parameters& params) { Store::Id docid{Store::InvalidId}; const auto path{get_string_or(params, ":path")}; const auto mark_as_read{get_bool_or(params, ":mark-as-read")}; GError *gerr{}; MuMsg *msg{}; if (!path.empty()) msg = mu_msg_new_from_file (path.c_str(), NULL, &gerr); else { docid = determine_docids(query(), params).at(0); msg = store().find_message(docid); } if (!msg) throw Error{Error::Code::Store, &gerr, "failed to find message for view"}; if (mark_as_read) maybe_mark_as_read (store(), msg, docid); Sexp::List seq; seq.add_prop(":view", build_message_sexp(msg, docid, {}, message_options(params))); mu_msg_unref(msg); output_sexp (std::move(seq)); } Server::Server(Store& store, Server::Output output): priv_{std::make_unique(store, output)} {} Server::~Server() = default; bool Server::invoke (const std::string& expr) noexcept { return priv_->invoke(expr); }