/* ** Copyright (C) 2020-2022 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 "message/mu-message.hh" #include "mu-server.hh" #include #include #include #include #include #include #include #include #include #include #include "mu-maildir.hh" #include "mu-query.hh" #include "index/mu-indexer.hh" #include "mu-store.hh" #include "utils/mu-utils.hh" #include "utils/mu-option.hh" #include "utils/mu-command-handler.hh" #include "utils/mu-readline.hh" using namespace Mu; /// @brief object to manage the server-context for all commands. struct Server::Private { Private(Store& store, Output output) : store_{store}, output_{output}, command_handler_{make_command_map()}, keep_going_{true} {} ~Private() { indexer().stop(); if (index_thread_.joinable()) index_thread_.join(); } // // construction helpers // CommandHandler::CommandInfoMap make_command_map(); // // acccessors Store& store() { return store_; } const Store& store() const { return store_; } Indexer& indexer() { return store().indexer(); } //CommandMap& command_map() const { return command_map_; } // // invoke // bool invoke(const std::string& expr) noexcept; // // output // void output_sexp(const Sexp& sexp, Server::OutputFlags flags = {}) const { if (output_) output_(sexp, flags); } size_t output_results(const QueryResults& qres, size_t batch_size) const; // // handlers for various commands. // void add_handler(const Command& cmd); void compose_handler(const Command& cmd); void contacts_handler(const Command& cmd); void find_handler(const Command& cmd); void help_handler(const Command& cmd); void index_handler(const Command& cmd); void move_handler(const Command& cmd); void mkdir_handler(const Command& cmd); void ping_handler(const Command& cmd); void queries_handler(const Command& cmd); void quit_handler(const Command& cmd); void remove_handler(const Command& cmd); void sent_handler(const Command& cmd); void view_handler(const Command& cmd); private: // helpers Sexp build_message_sexp(const Message& msg, Store::Id docid, const Option qm) const; void move_docid(Store::Id docid, Option flagstr, bool new_name, bool no_view); void perform_move(Store::Id docid, const Message& msg, const std::string& maildirarg, Flags flags, bool new_name, bool no_view); void view_mark_as_read(Store::Id docid, Message&& msg, bool rename); Store& store_; Server::Output output_; const CommandHandler command_handler_; std::atomic keep_going_{}; std::thread index_thread_; }; static Sexp build_metadata(const QueryMatch& qmatch) { const auto td{::atoi(qmatch.thread_date.c_str())}; auto mdata = Sexp().put_props(":path", qmatch.thread_path, ":level", qmatch.thread_level, ":date", qmatch.thread_date, ":data-tstamp", Sexp().add(static_cast(td >> 16), static_cast(td & 0xffff), 0)); if (qmatch.has_flag(QueryMatch::Flags::Root)) mdata.put_props(":root", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::Related)) mdata.put_props(":related", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::First)) mdata.put_props(":first-child", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::Last)) mdata.put_props(":last-child", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::Orphan)) mdata.put_props(":orphan", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::Duplicate)) mdata.put_props(":duplicate", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::HasChild)) mdata.put_props(":has-child", Sexp::t()); if (qmatch.has_flag(QueryMatch::Flags::ThreadSubject)) mdata.put_props(":thread-subject", Sexp::t()); return mdata; } /* * A message here consists of a message s-expression with optionally a :docid * and/or :meta expression added. */ Sexp Server::Private::build_message_sexp(const Message& msg, Store::Id docid, const Option qm) const { Sexp sexp{msg.sexp()}; // copy if (docid != 0) sexp.put_props(":docid", docid); if (qm) sexp.put_props(":meta", build_metadata(*qm)); return sexp; } CommandHandler::CommandInfoMap Server::Private::make_command_map() { CommandHandler::CommandInfoMap cmap; using CommandInfo = CommandHandler::CommandInfo; using ArgMap = CommandHandler::ArgMap; using ArgInfo = CommandHandler::ArgInfo; using Type = Sexp::Type; 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"}}, {":maxnum", ArgInfo{Type::Number, false, "max number of contacts to return"}}}, "get contact information", [&](const auto& params) { contacts_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"}}, {":batch-size", ArgInfo{Type::Number, false, "batch size for result"}}, {":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{}, "ping the mu-server and get server information in the response", [&](const auto& params) { ping_handler(params); }}); cmap.emplace( "queries", CommandInfo{ ArgMap{ {":queries", ArgInfo{Type::List, false, "queries for which to get read/unread numbers"}}, }, "get unread/totals information for a list of queries", [&](const auto& params) { queries_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)"}}, {":rename", ArgInfo{Type::Symbol, false, "change filename when moving"}}, }, "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); auto err = Sexp().put_props(":error", static_cast(errcode), ":message", msg); g_free(msg); return err; } bool Server::Private::invoke(const std::string& expr) noexcept { if (!keep_going_) return false; try { auto cmd{Command::make_parse(std::string{expr})}; if (!cmd) throw cmd.error(); auto res = command_handler_.invoke(*cmd); if (!res) throw res.error(); } catch (const Mu::Error& me) { output_sexp(make_error(me.code(), "%s", me.what())); keep_going_ = true; } catch (const Xapian::Error& xerr) { output_sexp(make_error(Error::Code::Internal, "xapian error: %s: %s", xerr.get_type(), xerr.get_description().c_str())); keep_going_ = false; } 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_; } /* '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 Command& cmd) { auto path{cmd.string_arg(":path")}; const auto docid_res{store().add_message(*path)}; if (!docid_res) throw docid_res.error(); const auto docid{docid_res.value()}; output_sexp(Sexp().put_props(":info", "add"_sym, ":path", *path, ":docid", docid)); auto msg_res{store().find_message(docid)}; if (!msg_res) throw Error(Error::Code::Store, "failed to get message at %s (docid=%u): %s", path->c_str(), docid); output_sexp(Sexp().put_props(":update", build_message_sexp(msg_res.value(), docid, {}))); } /* '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 */ static Option maybe_add_attachment(Message& message, const MessagePart& part, size_t index) { if (!part.is_attachment()) return Nothing; const auto cache_path{message.cache_path(index)}; if (!cache_path) throw cache_path.error(); const auto cooked_name{part.cooked_filename()}; const auto fname{format("%s/%s", cache_path->c_str(), cooked_name.value_or("part").c_str())}; const auto res = part.to_file(fname, true); if (!res) throw res.error(); Sexp pi; if (auto cdescr = part.content_description(); cdescr) pi.put_props(":description", *cdescr); else if (cooked_name) pi.put_props(":description", cooked_name.value()); pi.put_props(":file-name", fname, ":mime-type", part.mime_type().value_or("application/octet-stream")); return Some(std::move(pi)); } void Server::Private::compose_handler(const Command& cmd) { const auto ctype = cmd.symbol_arg(":type").value_or(""); auto comp_lst = Sexp().put_props(":compose", Sexp::Symbol(ctype)); if (ctype == "reply" || ctype == "forward" || ctype == "edit" || ctype == "resend") { const unsigned docid{static_cast(cmd.number_arg(":docid").value_or(0))}; auto msg{store().find_message(docid)}; if (!msg) throw Error{Error::Code::Store, "failed to get message %u", docid}; comp_lst.put_props(":original", build_message_sexp(msg.value(), docid, {})); if (ctype == "forward") { // when forwarding, attach any attachment in the orig size_t index{}; Sexp attseq; for (auto&& part: msg->parts()) { if (auto attsexp = maybe_add_attachment( *msg, part, index); attsexp) { attseq.add(std::move(*attsexp)); ++index; } } if (!attseq.empty()) { comp_lst.put_props(":include", std::move(attseq), ":cache-path", *msg->cache_path()); } } } else if (ctype != "new") throw Error(Error::Code::InvalidArgument, "invalid compose type '%s'", ctype.c_str()); output_sexp(comp_lst); } void Server::Private::contacts_handler(const Command& cmd) { const auto personal = cmd.boolean_arg(":personal"); const auto afterstr = cmd.string_arg(":after").value_or(""); const auto tstampstr = cmd.string_arg(":tstamp").value_or(""); const auto maxnum = cmd.number_arg(":maxnum").value_or(0 /*unlimited*/); const auto after{afterstr.empty() ? 0 : parse_date_time(afterstr, true).value_or(0)}; const auto tstamp = g_ascii_strtoll(tstampstr.c_str(), NULL, 10); g_debug("find %s contacts last seen >= %s (tstamp: %zu)", personal ? "personal" : "any", time_to_string("%c", after).c_str(), static_cast(tstamp)); auto n{0}; Sexp contacts; store().contacts_cache().for_each([&](const Contact& ci) { /* since the last time we got some contacts */ if (tstamp > ci.tstamp) return true; /* (maybe) only include 'personal' contacts */ if (personal && !ci.personal) return true; /* only include newer-than-x contacts */ if (after > ci.message_date) return true; n++; contacts.add(ci.display_name(true/*encode-if-needed*/)); return maxnum == 0 || n < maxnum; }); Sexp seq; seq.put_props(":contacts", contacts, ":tstamp", format("%" G_GINT64_FORMAT, g_get_monotonic_time())); /* dump the contacts cache as a giant sexp */ g_debug("sending %d of %zu contact(s)", n, store().contacts_cache().size()); output_sexp(seq, Server::OutputFlags::SplitList); } /* get a *list* of all messages with the given message id */ static std::vector docids_for_msgid(const Store& store, const std::string& msgid, size_t max = 100) { if (msgid.size() > MaxTermLength) { throw Error(Error::Code::InvalidArgument, "invalid message-id '%s'", msgid.c_str()); } else if (msgid.empty()) return {}; const auto xprefix{field_from_id(Field::Id::MessageId).shortcut}; /*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{}; std::lock_guard l{store.lock()}; const auto res{store.run_query(expr, {}, QueryFlags::None, max)}; g_free(expr); if (!res) throw Error(Error::Code::Store, &gerr, "failed to run message-id-query: %s", res.error().what()); 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, Store::Id docid) { auto msg{store.find_message(docid)}; if (!msg) throw Error(Error::Code::Store, "could not get message from store"); if (auto path{msg->path()}; path.empty()) throw Error(Error::Code::Store, "could not get path for message %u", docid); else return path; } static std::vector determine_docids(const Store& store, const Command& cmd) { auto docid{cmd.number_arg(":docid").value_or(0)}; const auto msgid{cmd.string_arg(":msgid").value_or("")}; if ((docid == 0) == msgid.empty()) throw Error(Error::Code::InvalidArgument, "precisely one of docid and msgid must be specified"); if (docid != 0) return {static_cast(docid)}; else return docids_for_msgid(store, msgid.c_str()); } size_t Server::Private::output_results(const QueryResults& qres, size_t batch_size) const { size_t n{}; Sexp headers; const auto output_batch = [&](Sexp&& hdrs) { Sexp batch; batch.put_props(":headers", std::move(hdrs)); output_sexp(batch); }; for (auto&& mi : qres) { auto msg{mi.message()}; if (!msg) continue; ++n; // construct sexp for a single header. auto qm{mi.query_match()}; auto msgsexp{build_message_sexp(*msg, mi.doc_id(), qm)}; headers.add(std::move(msgsexp)); // we output up-to-batch-size lists of messages. It's much // faster (on the emacs side) to handle such batches than single // headers. if (headers.size() % batch_size == 0) { output_batch(std::move(headers)); headers.clear(); }; } // remaining. if (!headers.empty()) output_batch(std::move(headers)); return n; } void Server::Private::find_handler(const Command& cmd) { const auto q{cmd.string_arg(":query").value_or("")}; const auto threads{cmd.boolean_arg(":threads")}; // perhaps let mu4e set this as frame-lines of the appropriate frame. const auto batch_size{cmd.number_arg(":batch-size").value_or(110)}; const auto sortfieldstr{cmd.symbol_arg(":sortfield").value_or("")}; const auto descending{cmd.boolean_arg(":descending")}; const auto maxnum{cmd.number_arg(":maxnum").value_or(-1) /*unlimited*/}; const auto skip_dups{cmd.boolean_arg(":skip-dups")}; const auto include_related{cmd.boolean_arg(":include-related")}; auto sort_field = std::invoke([&]()->Option{ if (sortfieldstr.size() < 2) return Nothing; else return field_from_name(sortfieldstr.substr(1)); }); if (!sort_field && !sortfieldstr.empty()) throw Error{Error::Code::InvalidArgument, "invalid sort field '%s'", sortfieldstr.c_str()}; if (batch_size < 1) throw Error{Error::Code::InvalidArgument, "invalid batch-size %d", batch_size}; auto qflags{QueryFlags::SkipUnreadable}; // don't show unreadables. if (descending) qflags |= QueryFlags::Descending; if (skip_dups) qflags |= QueryFlags::SkipDuplicates; if (include_related) qflags |= QueryFlags::IncludeRelated; if (threads) qflags |= QueryFlags::Threading; std::lock_guard l{store_.lock()}; auto qres{store_.run_query(q, sort_field->id, 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. */ output_sexp(Sexp().put_props(":erase", Sexp::t())); const auto foundnum{output_results(*qres, static_cast(batch_size))}; output_sexp(Sexp().put_props(":found", foundnum)); } void Server::Private::help_handler(const Command& cmd) { const auto command{cmd.symbol_arg(":command").value_or("")}; const auto full{cmd.bool_arg(":full").value_or(!command.empty())}; auto&& info_map{command_handler_.info_map()}; 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: info_map) names.emplace_back(name_cmd.first); std::sort(names.begin(), names.end()); for (auto&& name : names) { const auto& info{info_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 get_stats(const Indexer::Progress& stats, const std::string& state) { Sexp sexp; sexp.put_props( ":info", "index"_sym, ":status", Sexp::Symbol(state), ":checked", static_cast(stats.checked), ":updated", static_cast(stats.updated), ":cleaned-up", static_cast(stats.removed)); return sexp; } void Server::Private::index_handler(const Command& cmd) { Mu::Indexer::Config conf{}; conf.cleanup = cmd.boolean_arg(":cleanup"); conf.lazy_check = cmd.boolean_arg(":lazy-check"); // ignore .noupdate with an empty store. conf.ignore_noupdate = store().empty(); indexer().stop(); if (index_thread_.joinable()) index_thread_.join(); // start a background track. index_thread_ = std::thread([this, conf = std::move(conf)] { indexer().start(conf); while (indexer().is_running()) { std::this_thread::sleep_for(std::chrono::milliseconds(2000)); output_sexp(get_stats(indexer().progress(), "running"), Server::OutputFlags::Flush); } output_sexp(get_stats(indexer().progress(), "complete"), Server::OutputFlags::Flush); store().commit(); /* ensure on-disk database is updated, too */ }); } void Server::Private::mkdir_handler(const Command& cmd) { const auto path{cmd.string_arg(":path").value_or("")}; if (auto&& res = maildir_mkdir(path, 0755, false); !res) throw res.error(); output_sexp(Sexp().put_props(":info", "mkdir", ":message", format("%s has been created", path.c_str()))); } void Server::Private::perform_move(Store::Id docid, const Message& msg, const std::string& maildirarg, Flags flags, bool new_name, bool no_view) { bool different_mdir{}; auto maildir{maildirarg}; if (maildir.empty()) { maildir = msg.maildir(); different_mdir = false; } else /* are we moving to a different mdir, or is it just flags? */ different_mdir = maildir != msg.maildir(); Store::MoveOptions move_opts{Store::MoveOptions::DupFlags}; if (new_name) move_opts |= Store::MoveOptions::ChangeName; /* note: we get back _all_ the messages that changed; the first is the * primary mover; the rest (if present) are any dups affected */ const auto idmsgvec{store().move_message(docid, maildir, flags, move_opts)}; if (!idmsgvec) throw idmsgvec.error(); for (auto&&[id, msg]: *idmsgvec) { Sexp sexp{":update"_sym, build_message_sexp(idmsgvec->at(0).second, id, {})}; /* note, the :move t thing is a hint to the frontend that it * could remove the particular header */ if (different_mdir) sexp.put_props(":move", Sexp::t()); if (!no_view && id == docid) sexp.put_props(":maybe-view", Sexp::t()); output_sexp(std::move(sexp)); } } static Flags calculate_message_flags(const Message& msg, Option flagopt) { const auto flags = std::invoke([&]()->Option{ if (!flagopt) return msg.flags(); else return flags_from_expr(*flagopt, msg.flags()); }); if (!flags) throw Error{Error::Code::InvalidArgument, "invalid flags '%s'", flagopt.value_or("").c_str()}; else return flags.value(); } void Server::Private::move_docid(Store::Id docid, Option flagopt, bool new_name, bool no_view) { if (docid == Store::InvalidId) throw Error{Error::Code::InvalidArgument, "invalid docid"}; auto msg{store_.find_message(docid)}; if (!msg) throw Error{Error::Code::Store, "failed to get message from store"}; const auto flags = calculate_message_flags(msg.value(), flagopt); perform_move(docid, *msg, "", flags, new_name, no_view); } /* * '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. */ void Server::Private::move_handler(const Command& cmd) { auto maildir{cmd.string_arg(":maildir").value_or("")}; const auto flagopt{cmd.string_arg(":flags").value_or("")}; const auto rename{cmd.boolean_arg(":rename")}; const auto no_view{cmd.boolean_arg(":noupdate")}; const auto docids{determine_docids(store_, cmd)}; 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) move_docid(docid, flagopt, rename, no_view); return; } auto docid{docids.at(0)}; auto msg = store().find_message(docid) .or_else([]{throw Error{Error::Code::InvalidArgument, "could not create message"};}).value(); /* if maildir was not specified, take the current one */ if (maildir.empty()) maildir = msg.maildir(); /* determine the real target flags, which come from the flags-parameter * we received (ie., flagstr), if any, plus the existing message * flags. */ const auto flags = calculate_message_flags(msg, flagopt); perform_move(docid, msg, maildir, flags, rename, no_view); } void Server::Private::ping_handler(const Command& cmd) { const auto storecount{store().size()}; if (storecount == (unsigned)-1) throw Error{Error::Code::Store, "failed to read store"}; Sexp addrs; for (auto&& addr : store().properties().personal_addresses) addrs.add(addr); output_sexp(Sexp() .put_props(":pong", "mu") .put_props(":props", Sexp().put_props( ":version", VERSION, ":personal-addresses", std::move(addrs), ":database-path", store().properties().database_path, ":root-maildir", store().properties().root_maildir, ":doccount", storecount))); } void Server::Private::queries_handler(const Command& cmd) { const auto queries{cmd.string_vec_arg(":queries") .value_or(std::vector{})}; Sexp qresults; for (auto&& q : queries) { const auto count{store_.count_query(q)}; const auto unreadq{format("flag:unread AND (%s)", q.c_str())}; const auto unread{store_.count_query(unreadq)}; qresults.add(Sexp().put_props(":query", q, ":count", count, ":unread", unread)); } output_sexp(Sexp(":queries"_sym, std::move(qresults))); } void Server::Private::quit_handler(const Command& cmd) { keep_going_ = false; } void Server::Private::remove_handler(const Command& cmd) { const auto docid{cmd.number_arg(":docid").value_or(0)}; 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); output_sexp(Sexp().put_props(":remove", docid)); // act as if it worked. } void Server::Private::sent_handler(const Command& cmd) { const auto path{cmd.string_arg(":path").value_or("")}; const auto docid = store().add_message(path); if (!docid) throw Error{Error::Code::Store, "failed to add path: %s", docid.error().what()}; output_sexp(Sexp().put_props( ":sent", Sexp::t(), ":path", path, ":docid", docid.value())); } void Server::Private::view_mark_as_read(Store::Id docid, Message&& msg, bool rename) { auto move_res = std::invoke([&]()->Result { const auto newflags{flags_from_delta_expr("+S-u-N", msg.flags())}; if (!newflags || msg.flags() == *newflags) { /* case 1: message was already read; do nothing */ Store::IdMessageVec idmvec; idmvec.emplace_back(docid, std::move(msg)); return idmvec; } else { /* case 2: move message (and possibly dups) */ Store::MoveOptions move_opts{Store::MoveOptions::DupFlags}; if (rename) move_opts |= Store::MoveOptions::ChangeName; return store().move_message(docid, {}, newflags, move_opts); } }); if (!move_res) throw move_res.error(); for (auto&& [id, msg]: move_res.value()) output_sexp(Sexp{id == docid ? ":view"_sym : ":update"_sym, build_message_sexp(msg, id, {})}); } void Server::Private::view_handler(const Command& cmd) { const auto mark_as_read{cmd.boolean_arg(":mark-as-read")}; /* for now, do _not_ rename, as it seems to confuse mbsync */ const auto rename{false}; //const auto rename{get_bool_or(params, ":rename")}; const auto docids{determine_docids(store(), cmd)}; if (docids.empty()) throw Error{Error::Code::Store, "failed to find message for view"}; const auto docid{docids.at(0)}; auto msg = store().find_message(docid) .or_else([]{throw Error{Error::Code::Store, "failed to find message for view"};}).value(); /* if the message should not be marked-as-read, we're done. */ if (!mark_as_read) output_sexp(Sexp().put_props(":view", build_message_sexp(msg, docid, {}))); else view_mark_as_read(docid, std::move(msg), rename); /* otherwise, mark message and and possible dups as read */ } 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); }