Instead of a multi-step process to display an unread message (ie. get the original, notice it's unread, then update it, replace the message with update one etc.), we now handle that in the (view /./..) command on the server side. Simplifies things, and is faster (which could be noticeable, esp. if e.g. signature verification is part of the process)
1325 lines
49 KiB
C++
1325 lines
49 KiB
C++
/*
|
|
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
|
**
|
|
** 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 <iostream>
|
|
#include <string>
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cstring>
|
|
#include <glib.h>
|
|
#include <glib/gprintf.h>
|
|
|
|
#include "mu-msg.h"
|
|
#include "mu-runtime.h"
|
|
#include "mu-cmd.hh"
|
|
#include "mu-maildir.h"
|
|
#include "mu-query.h"
|
|
#include "mu-index.h"
|
|
#include "mu-store.hh"
|
|
#include "mu-msg-part.h"
|
|
#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;
|
|
using namespace Sexp;
|
|
|
|
using DocId = unsigned;
|
|
|
|
static std::atomic<bool> MuTerminate{false};
|
|
|
|
static void
|
|
sig_handler (int sig)
|
|
{
|
|
MuTerminate = true;
|
|
}
|
|
|
|
static void
|
|
install_sig_handler (void)
|
|
{
|
|
struct sigaction action;
|
|
int i, sigs[] = { SIGINT, SIGHUP, SIGTERM, SIGPIPE };
|
|
|
|
MuTerminate = false;
|
|
|
|
action.sa_handler = sig_handler;
|
|
sigemptyset(&action.sa_mask);
|
|
action.sa_flags = SA_RESETHAND;
|
|
|
|
for (i = 0; i != G_N_ELEMENTS(sigs); ++i)
|
|
if (sigaction (sigs[i], &action, NULL) != 0)
|
|
g_critical ("set sigaction for %d failed: %s",
|
|
sigs[i], g_strerror (errno));;
|
|
}
|
|
|
|
|
|
/*
|
|
* Markers for/after the length cookie that precedes the expression we write to
|
|
* output. We use octal 376, 377 (ie, 0xfe, 0xff) as they will never occur in
|
|
* utf8 */
|
|
|
|
|
|
#define COOKIE_PRE '\376'
|
|
#define COOKIE_POST '\377'
|
|
|
|
static void G_GNUC_PRINTF(1, 2)
|
|
print_expr (const char* frm, ...)
|
|
{
|
|
char *expr, *expr_orig;
|
|
va_list ap;
|
|
ssize_t rv;
|
|
size_t exprlen, lenlen;
|
|
char cookie[16];
|
|
static int outfd = 0;
|
|
|
|
#if defined(__CYGWIN__ )&& !defined (_WIN32)
|
|
const size_t writestep = 4096 * 16;
|
|
size_t bytestowrite = 0;
|
|
#endif
|
|
|
|
if (outfd == 0)
|
|
outfd = fileno (stdout);
|
|
|
|
expr = NULL;
|
|
|
|
va_start (ap, frm);
|
|
exprlen = g_vasprintf (&expr, frm, ap);
|
|
va_end (ap);
|
|
|
|
/* this cookie tells the frontend where to expect the next
|
|
* expression */
|
|
|
|
cookie[0] = COOKIE_PRE;
|
|
lenlen = sprintf(cookie + 1, "%x",
|
|
(unsigned)exprlen + 1); /* + 1 for \n */
|
|
cookie[lenlen + 1] = COOKIE_POST;
|
|
|
|
/* write the cookie, ie.
|
|
* COOKIE_PRE <len-of-following-sexp-in-hex> COOKIE_POST
|
|
*/
|
|
rv = write (outfd, cookie, lenlen + 2);
|
|
if (rv != -1) {
|
|
expr_orig = expr;
|
|
#if defined (__CYGWIN__) && !defined(_WIN32)
|
|
/* CYGWIN doesn't like big packets */
|
|
while (exprlen > 0) {
|
|
bytestowrite = exprlen > writestep ? writestep : exprlen;
|
|
rv = write(outfd, expr, bytestowrite);
|
|
expr += bytestowrite;
|
|
exprlen -= bytestowrite;
|
|
}
|
|
#else
|
|
rv = write (outfd, expr, exprlen);
|
|
#endif
|
|
g_free (expr_orig);
|
|
}
|
|
if (rv != -1)
|
|
rv = write (outfd, "\n", 1);
|
|
if (rv == -1) {
|
|
g_critical ("%s: write() failed: %s",
|
|
__func__, g_strerror(errno));
|
|
/* terminate ourselves */
|
|
raise (SIGTERM);
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
print_expr (const Node& sexp)
|
|
{
|
|
print_expr ("%s", sexp.to_string().c_str());
|
|
}
|
|
|
|
static void
|
|
print_expr (Node::Seq&& seq)
|
|
{
|
|
print_expr (Node::make_list(std::move(seq)));
|
|
}
|
|
|
|
|
|
|
|
G_GNUC_PRINTF(2,3) static MuError
|
|
print_error (MuError errcode, const char* frm, ...)
|
|
{
|
|
char *msg;
|
|
va_list ap;
|
|
|
|
va_start (ap, frm);
|
|
g_vasprintf (&msg, frm, ap);
|
|
va_end (ap);
|
|
|
|
Node::Seq err_sexp;
|
|
err_sexp.add_prop(":error", (int)errcode);
|
|
err_sexp.add_prop(":message", msg);
|
|
|
|
print_expr(Node::make_list(std::move(err_sexp)));
|
|
|
|
g_free (msg);
|
|
|
|
return errcode;
|
|
}
|
|
|
|
static unsigned
|
|
print_sexps (MuMsgIter *iter, unsigned maxnum)
|
|
{
|
|
unsigned u;
|
|
u = 0;
|
|
|
|
while (!mu_msg_iter_is_done (iter) && u < maxnum) {
|
|
|
|
MuMsg *msg;
|
|
msg = mu_msg_iter_get_msg_floating (iter);
|
|
|
|
if (mu_msg_is_readable (msg)) {
|
|
char *sexp;
|
|
const MuMsgIterThreadInfo* ti;
|
|
ti = mu_msg_iter_get_thread_info (iter);
|
|
sexp = mu_msg_to_sexp (msg,
|
|
mu_msg_iter_get_docid (iter),
|
|
ti, MU_MSG_OPTION_HEADERS_ONLY);
|
|
print_expr ("%s", sexp);
|
|
g_free (sexp);
|
|
++u;
|
|
}
|
|
mu_msg_iter_next (iter);
|
|
}
|
|
return u;
|
|
}
|
|
|
|
|
|
struct Context {
|
|
Context(){}
|
|
Context (const MuConfig *opts):
|
|
store_{std::make_unique<Store>(mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB), false/*writable*/)} {
|
|
|
|
// store = mu_store_new_writable (dbpath, NULL);
|
|
// if (!store) {
|
|
// const auto mu_init = format("mu init %s%s",
|
|
// opts->muhome ? "--muhome=" : "",
|
|
// opts->muhome ? opts->muhome : "");
|
|
|
|
// if (gerr) {
|
|
// if ((MuError)gerr->code == MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK)
|
|
// print_error(MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK,
|
|
// "mu database already locked; "
|
|
// "some other mu running?");
|
|
// else
|
|
// print_error((MuError)gerr->code,
|
|
// "cannot open database @ %s:%s; already running? "
|
|
// "if not, please try '%s", dbpath,
|
|
// gerr->message ? gerr->message : "something went wrong",
|
|
// mu_init.c_str());
|
|
// } else
|
|
// print_error(MU_ERROR,
|
|
// "cannot open database @ %s; already running? if not, please try '%s'",
|
|
// dbpath, mu_init.c_str());
|
|
|
|
// throw Mu::Error (Error::Code::Store, &gerr/*consumed*/,
|
|
// "failed to open database @ %s; already running? if not, please try '%s'",
|
|
// dbpath, mu_init.c_str());
|
|
// }
|
|
|
|
GError *gerr{};
|
|
query = mu_query_new (reinterpret_cast<MuStore*>(store_.get()), &gerr);
|
|
if (!query)
|
|
throw Error(Error::Code::Store, &gerr/*consumes*/, "failed to create query");
|
|
}
|
|
|
|
~Context() {
|
|
if (query)
|
|
mu_query_destroy(query);
|
|
}
|
|
|
|
Context(const Context&) = delete;
|
|
|
|
Store& store() {
|
|
if (!store_)
|
|
throw Mu::Error (Error::Code::Internal, "no store");
|
|
return *store_.get();
|
|
}
|
|
|
|
|
|
std::unique_ptr<Mu::Store> store_;
|
|
MuQuery *query{};
|
|
bool do_quit{};
|
|
|
|
CommandMap command_map;
|
|
};
|
|
|
|
|
|
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)
|
|
*/
|
|
static void
|
|
add_handler (Context& context, const Parameters& params)
|
|
{
|
|
auto path{get_string_or(params, "path")};
|
|
const auto docid{context.store().add_message(path)};
|
|
|
|
Node::Seq seq;
|
|
seq.add_prop(":info", Node::make_symbol("add"));
|
|
seq.add_prop(":path", path);
|
|
seq.add_prop(":docid", docid);
|
|
|
|
print_expr (std::move(seq));
|
|
|
|
auto msg{context.store().find_message(docid)};
|
|
if (!msg)
|
|
throw Error(Error::Code::Store,
|
|
"failed to get message at %s (docid=%u)",
|
|
path.c_str(), docid);
|
|
|
|
Node::Seq updateseq;
|
|
updateseq.add_prop(":update", Mu::msg_to_sexp(msg, docid, NULL,
|
|
MU_MSG_OPTION_VERIFY));
|
|
|
|
print_expr (std::move(updateseq));
|
|
mu_msg_unref(msg);
|
|
}
|
|
|
|
|
|
struct PartInfo {
|
|
Node::Seq 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");
|
|
|
|
Node::Seq seq;
|
|
seq.add_prop(":file-name", cachefile);
|
|
seq.add_prop(":mime-type", format("%s/%s", part->type, part->subtype));
|
|
pinfo->attseq.add(std::move(seq));
|
|
|
|
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 <type> [:original <original-msg>] [:include] )
|
|
* message (detals: see code below)
|
|
*
|
|
* Note ':include' t or nil determines whether to include attachments
|
|
*/
|
|
static void
|
|
compose_handler (Context& context, const Parameters& params)
|
|
{
|
|
auto ctype{get_symbol_or(params, "type")};
|
|
|
|
Node::Seq compose_seq;
|
|
compose_seq.add_prop(":compose", ctype);
|
|
|
|
// message optioss below checks extract-images / extract-encrypted
|
|
|
|
|
|
if (ctype == "reply" || ctype == "forward" || ctype == "edit" || ctype == "resend") {
|
|
|
|
GError *gerr{};
|
|
const unsigned docid{(unsigned)get_int_or(params, "docid")};
|
|
auto msg{context.store().find_message(docid)};
|
|
if (!msg)
|
|
throw Error{Error::Code::Store, &gerr, "failed to get message %u", docid};
|
|
|
|
const auto opts{message_options(params)};
|
|
compose_seq.add_prop(":original", Mu::msg_to_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())
|
|
compose_seq.add_prop (":include", std::move(pinfo.attseq));
|
|
}
|
|
mu_msg_unref (msg);
|
|
|
|
} else if (ctype != "new")
|
|
throw Error(Error::Code::InvalidArgument, "invalid compose type");
|
|
|
|
print_expr (std::move(compose_seq));
|
|
}
|
|
|
|
static void
|
|
contacts_handler (Context& context, 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};
|
|
Node::Seq contacts;
|
|
context.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;
|
|
|
|
Node::Seq contact;
|
|
contact.add_prop(":address", std::string{ci.full_address});
|
|
contact.add_prop(":rank", rank);
|
|
|
|
contacts.add(Node::make_list(std::move(contact)));
|
|
});
|
|
|
|
Node::Seq seq;
|
|
seq.add_prop(":contacts", std::move(contacts));
|
|
seq.add_prop(":tstamp", Node::make_string(format("%" G_GINT64_FORMAT,
|
|
g_get_monotonic_time())));
|
|
|
|
/* dump the contacts cache as a giant sexp */
|
|
print_expr(std::move(seq));
|
|
}
|
|
|
|
|
|
static void
|
|
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"};
|
|
|
|
Node::Seq seq;
|
|
seq.add_prop(":info", Node::make_symbol("save"));
|
|
seq.add_prop(":message", Node::make_string(format("%s has been saved", path.c_str())));
|
|
|
|
print_expr(std::move(seq));
|
|
}
|
|
|
|
|
|
static void
|
|
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"};
|
|
}
|
|
|
|
Node::Seq seq;
|
|
seq.add_prop(":info", Node::make_symbol("open"));
|
|
seq.add_prop(":message", Node::make_string(format("%s has been opened", targetpath)));
|
|
g_free (targetpath);
|
|
|
|
print_expr(std::move(seq));
|
|
}
|
|
|
|
static void
|
|
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"};
|
|
}
|
|
|
|
Node::Seq seq;
|
|
seq.add_prop(":temp", path);
|
|
seq.add_prop(":what", std::string(what));
|
|
seq.add_prop(":docid", docid);
|
|
|
|
if (!param.empty())
|
|
seq.add_prop(":param", std::string(param));
|
|
|
|
g_free(path);
|
|
print_expr(std::move(seq));
|
|
}
|
|
|
|
|
|
|
|
/* 'extract' extracts some mime part from a message */
|
|
static void
|
|
extract_handler (Context& context, 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)};
|
|
|
|
GError *gerr{};
|
|
auto msg{context.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")
|
|
save_part (msg, docid, index, opts, params);
|
|
else if (action == "open")
|
|
open_part (msg, docid, index, opts);
|
|
else if (action == "temp")
|
|
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<DocId>
|
|
docids_for_msgid (MuQuery *query, const std::string& msgid, size_t max=100)
|
|
{
|
|
if (msgid.size() > MU_STORE_MAX_TERM_LENGTH - 1) {
|
|
throw Error(Error::Code::InvalidArgument,
|
|
"invalid message-id '%s'", msgid.c_str());
|
|
}
|
|
|
|
const auto xprefix{mu_msg_field_xapian_prefix(MU_MSG_FIELD_ID_MSGID)};
|
|
/*XXX this is a bit dodgy */
|
|
auto tmp{g_ascii_strdown(msgid.c_str(), -1)};
|
|
auto rawq{g_strdup_printf("%c%s", xprefix, tmp)};
|
|
g_free(tmp);
|
|
|
|
GError *gerr{};
|
|
auto iter{mu_query_run (query, rawq, MU_MSG_FIELD_ID_NONE, max, MU_QUERY_FLAG_RAW, &gerr)};
|
|
g_free (rawq);
|
|
if (!iter)
|
|
throw Error(Error::Code::Store, &gerr, "failed to run msgid-query");
|
|
if (mu_msg_iter_is_done (iter))
|
|
throw Error(Error::Code::NotFound,
|
|
"could not find message(s) for msgid %s", msgid.c_str());
|
|
std::vector<DocId> docids;
|
|
do {
|
|
docids.emplace_back(mu_msg_iter_get_docid (iter));
|
|
} while (mu_msg_iter_next (iter));
|
|
mu_msg_iter_destroy (iter);
|
|
|
|
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<DocId>
|
|
determine_docids (MuQuery *query, 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 (query, msgid.c_str());
|
|
}
|
|
|
|
|
|
static void
|
|
find_handler (Context& context, const Parameters& params)
|
|
{
|
|
const auto query{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()};
|
|
}
|
|
|
|
int qflags{MU_QUERY_FLAG_NONE/*UNREADABLE*/};
|
|
if (descending)
|
|
qflags |= MU_QUERY_FLAG_DESCENDING;
|
|
if (skip_dups)
|
|
qflags |= MU_QUERY_FLAG_SKIP_DUPS;
|
|
if (include_related)
|
|
qflags |= MU_QUERY_FLAG_INCLUDE_RELATED;
|
|
if (threads)
|
|
qflags |= MU_QUERY_FLAG_THREADS;
|
|
|
|
GError *gerr{};
|
|
auto miter{mu_query_run(context.query, query.c_str(), sort_field, maxnum,
|
|
(MuQueryFlags)qflags, &gerr)};
|
|
if (!miter)
|
|
throw Error(Error::Code::Query, &gerr, "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. */
|
|
{
|
|
Node::Seq seq;
|
|
seq.add_prop(":erase", Node::make_symbol("t"));
|
|
print_expr(std::move(seq));
|
|
}
|
|
//print_expr ("(:erase t)");
|
|
{
|
|
const auto foundnum{print_sexps (miter, maxnum)};
|
|
Node::Seq seq;
|
|
seq.add_prop(":found", foundnum);
|
|
print_expr(std::move(seq));
|
|
}
|
|
//print_expr ("(:found %u)", foundnum);
|
|
mu_msg_iter_destroy (miter);
|
|
}
|
|
|
|
|
|
static void
|
|
help_handler (Context& context, const Parameters& params)
|
|
{
|
|
const auto command{get_symbol_or(params, "command", "")};
|
|
const auto full{get_bool_or(params, "full")};
|
|
|
|
if (command.empty()) {
|
|
std::cout << ";; Commands are s-expressions of the form\n"
|
|
<< ";; (<command-name> :param1 val1 :param2 val2 ...)\n"
|
|
<< ";; For instance:\n;; (help :command quit)\n"
|
|
<< ";; to get information about the 'quit' command\n;;\n";
|
|
std::cout << ";; The following commands are available:\n";
|
|
}
|
|
|
|
std::vector<std::string> names;
|
|
for (auto&& name_cmd: context.command_map)
|
|
names.emplace_back(name_cmd.first);
|
|
std::sort(names.begin(), names.end());
|
|
|
|
for (auto&& name: names) {
|
|
const auto& info{context.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 MuError
|
|
index_msg_cb (MuIndexStats *stats, void *user_data)
|
|
{
|
|
if (MuTerminate)
|
|
return MU_STOP;
|
|
|
|
if (stats->_processed % 1000)
|
|
return MU_OK;
|
|
|
|
Node::Seq seq;
|
|
seq.add_prop(":info", Node::make_symbol("index"));
|
|
seq.add_prop(":status", Node::make_symbol("running"));
|
|
seq.add_prop(":processed", stats->_processed);
|
|
seq.add_prop(":updated", stats->_updated);
|
|
|
|
print_expr(std::move(seq));
|
|
|
|
return MU_OK;
|
|
}
|
|
|
|
|
|
static MuError
|
|
index_and_maybe_cleanup (MuIndex *index, bool cleanup, bool lazy_check)
|
|
{
|
|
MuIndexStats stats{}, stats2{};
|
|
mu_index_stats_clear (&stats);
|
|
auto rv = mu_index_run (index, FALSE, lazy_check, &stats,
|
|
index_msg_cb, NULL, NULL);
|
|
if (rv != MU_OK && rv != MU_STOP)
|
|
throw Error{Error::Code::Store, "indexing failed"};
|
|
|
|
mu_index_stats_clear (&stats2);
|
|
if (cleanup) {
|
|
GError *gerr{};
|
|
rv = mu_index_cleanup (index, &stats2, NULL, NULL, &gerr);
|
|
if (rv != MU_OK && rv != MU_STOP)
|
|
throw Error{Error::Code::Store, &gerr, "cleanup failed"};
|
|
}
|
|
|
|
Node::Seq seq;
|
|
seq.add_prop(":info", Node::make_symbol("index"));
|
|
seq.add_prop(":status", Node::make_symbol("complete"));
|
|
seq.add_prop(":processed", stats._processed);
|
|
seq.add_prop(":updated", stats._updated);
|
|
seq.add_prop(":cleaned-up", stats2._cleaned_up);
|
|
|
|
print_expr(std::move(seq));
|
|
|
|
return MU_OK;
|
|
}
|
|
|
|
|
|
static void
|
|
index_handler (Context& context, const Parameters& params)
|
|
{
|
|
GError *gerr{};
|
|
const auto cleanup{get_bool_or(params, "cleanup")};
|
|
const auto lazy_check{get_bool_or(params, "lazy-check")};
|
|
|
|
auto store_ptr = reinterpret_cast<MuStore*>(&context.store());
|
|
|
|
auto index{mu_index_new (store_ptr, &gerr)};
|
|
if (!index)
|
|
throw Error(Error::Code::Index, &gerr, "failed to create index object");
|
|
|
|
try {
|
|
index_and_maybe_cleanup (index, cleanup, lazy_check);
|
|
} catch (...) {
|
|
mu_index_destroy(index);
|
|
throw;
|
|
}
|
|
mu_index_destroy(index);
|
|
mu_store_flush(store_ptr);
|
|
}
|
|
|
|
static void
|
|
mkdir_handler (Context& context, 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"};
|
|
|
|
Node::Seq seq;
|
|
seq.add_prop(":info", "mkdir");
|
|
seq.add_prop(":message", format("%s has been created", path.c_str()));
|
|
|
|
print_expr(std::move(seq));
|
|
}
|
|
|
|
|
|
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*/);
|
|
}
|
|
}
|
|
|
|
static void
|
|
do_move (Store& store, DocId 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"};
|
|
|
|
Node::Seq seq;
|
|
seq.add_prop(":update", msg_to_sexp (msg, docid, NULL, 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", Node::make_symbol("t"));
|
|
if (!no_view)
|
|
seq.add_prop(":maybe-view", Node::make_symbol("t"));
|
|
|
|
print_expr (std::move(seq));
|
|
}
|
|
|
|
static void
|
|
move_docid (Store& store, DocId docid, const std::string& flagstr,
|
|
bool new_name, bool no_view)
|
|
{
|
|
if (docid == MU_STORE_INVALID_DOCID)
|
|
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()};
|
|
|
|
do_move (store, docid, msg, "", flags, new_name, no_view);
|
|
|
|
} catch (...) {
|
|
if (msg)
|
|
mu_msg_unref (msg);
|
|
throw;
|
|
}
|
|
|
|
mu_msg_unref (msg);
|
|
}
|
|
|
|
/*
|
|
* '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 <new-msg-sexp>)
|
|
*
|
|
*/
|
|
static void
|
|
move_handler (Context& context, 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 (context.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)
|
|
move_docid(context.store(), docid, flagstr, rename, no_view);
|
|
return;
|
|
}
|
|
auto docid{docids.at(0)};
|
|
|
|
GError *gerr{};
|
|
auto msg{context.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 {
|
|
do_move (context.store(), docid, msg, maildir, flags,
|
|
rename, no_view);
|
|
} catch (...) {
|
|
mu_msg_unref(msg);
|
|
throw;
|
|
}
|
|
|
|
mu_msg_unref(msg);
|
|
}
|
|
|
|
static void
|
|
ping_handler (Context& context, const Parameters& params)
|
|
{
|
|
const auto storecount{context.store().size()};
|
|
if (storecount == (unsigned)-1)
|
|
throw Error{Error::Code::Store, "failed to read store"};
|
|
|
|
const auto queries = get_string_vec (params, "queries");
|
|
Node::Seq qresults;
|
|
for (auto&& q: queries) {
|
|
const auto count{mu_query_count_run (context.query, q.c_str())};
|
|
const auto unreadq{format("flag:unread AND (%s)", q.c_str())};
|
|
const auto unread{mu_query_count_run (context.query, unreadq.c_str())};
|
|
|
|
Node::Seq seq;
|
|
seq.add_prop(":query", std::string(q));
|
|
seq.add_prop(":count", count);
|
|
seq.add_prop(":unread", unread);
|
|
|
|
qresults.add(Node::make_list(std::move(seq)));
|
|
}
|
|
|
|
Node::Seq addrs;
|
|
for (auto&& addr: context.store().personal_addresses())
|
|
addrs.add(std::string(addr));
|
|
|
|
Node::Seq seq;
|
|
seq.add_prop(":pong", "mu");
|
|
|
|
Node::Seq propseq;
|
|
propseq.add_prop(":version", VERSION);
|
|
propseq.add_prop(":personal-addresses", std::move(addrs));
|
|
propseq.add_prop(":database-path", context.store().database_path());
|
|
propseq.add_prop(":root-maildir", context.store().root_maildir());
|
|
propseq.add_prop(":doccount", storecount);
|
|
propseq.add_prop(":queries", std::move(qresults));
|
|
|
|
seq.add_prop(":props", std::move(propseq));
|
|
|
|
print_expr(std::move(seq));
|
|
}
|
|
|
|
static void
|
|
quit_handler (Context& context, const Parameters& params)
|
|
{
|
|
context.do_quit = true;
|
|
}
|
|
|
|
|
|
static void
|
|
remove_handler (Context& context, const Parameters& params)
|
|
{
|
|
const auto docid{get_int_or(params, "docid")};
|
|
const auto path{path_from_docid (context.store(), docid)};
|
|
|
|
if (::unlink (path.c_str()) != 0 && errno != ENOENT)
|
|
throw Error(Error::Code::File, "could not delete %s: %s",
|
|
path.c_str(), strerror (errno));
|
|
|
|
if (!context.store().remove_message (path))
|
|
g_warning("failed to remove message @ %s (%d) from store",
|
|
path.c_str(), docid);
|
|
// act as if it worked.
|
|
|
|
Node::Seq seq;
|
|
seq.add_prop(":remove", docid);
|
|
|
|
print_expr(std::move(seq));
|
|
}
|
|
|
|
|
|
static void
|
|
sent_handler (Context& context, const Parameters& params)
|
|
{
|
|
const auto path{get_string_or(params, "path")};
|
|
const auto docid{context.store().add_message(path)};
|
|
if (docid == MU_STORE_INVALID_DOCID)
|
|
throw Error{Error::Code::Store, "failed to add path"};
|
|
|
|
Node::Seq seq;
|
|
seq.add_prop (":sent", Node::make_symbol("t"));
|
|
seq.add_prop (":path", std::string(path));
|
|
seq.add_prop (":docid", docid);
|
|
|
|
print_expr (std::move(seq));
|
|
}
|
|
|
|
static bool
|
|
maybe_mark_as_read (Mu::Store& store, MuMsg *msg, DocId docid)
|
|
{
|
|
if (!msg)
|
|
throw Error{Error::Code::Store, "missing message"};
|
|
if (docid == MU_STORE_INVALID_DOCID)
|
|
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; // 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"};
|
|
|
|
g_debug ("marked message %d as read => %s", docid, mu_msg_get_path(msg));
|
|
}
|
|
|
|
|
|
static void
|
|
view_handler (Context& context, const Parameters& params)
|
|
{
|
|
DocId docid{MU_STORE_INVALID_DOCID};
|
|
const auto path{get_string_or(params, "path")};
|
|
const auto mark_unread{get_bool_or(params, "mark-unread")};
|
|
|
|
GError *gerr{};
|
|
MuMsg *msg{};
|
|
|
|
if (!path.empty())
|
|
msg = mu_msg_new_from_file (path.c_str(), NULL, &gerr);
|
|
else {
|
|
docid = determine_docids(context.query, params).at(0);
|
|
msg = context.store().find_message(docid);
|
|
}
|
|
|
|
if (!msg)
|
|
throw Error{Error::Code::Store, &gerr,
|
|
"failed to find message for view"};
|
|
//if (mark_unread)
|
|
maybe_mark_as_unread (msg, docid);
|
|
|
|
Node::Seq seq;
|
|
seq.add_prop(":view", msg_to_sexp(msg, docid, {}, message_options(params)));
|
|
mu_msg_unref(msg);
|
|
|
|
print_expr (std::move(seq));
|
|
}
|
|
|
|
|
|
static CommandMap
|
|
make_command_map (Context& context)
|
|
{
|
|
CommandMap cmap;
|
|
|
|
using Type = Node::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(context, 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)" }}},
|
|
"get contact information",
|
|
[&](const auto& params){compose_handler(context, 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(context, 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(context, 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(context, params);}});
|
|
|
|
cmap.emplace("help",
|
|
CommandInfo{
|
|
ArgMap{ {"command", ArgInfo{Type::Symbol, false,
|
|
"command to get information for" }},
|
|
{"full", ArgInfo{Type::Symbol, false,
|
|
"whether to include information about parameters" }}},
|
|
"get information about one or all commands",
|
|
[&](const auto& params){help_handler(context, 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(context, 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(context, 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(context, 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(context, params);}});
|
|
|
|
cmap.emplace("quit",
|
|
CommandInfo{{},
|
|
"quit the mu server",
|
|
[&](const auto& params){quit_handler(context, 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(context, 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(context, 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(context, params);}});
|
|
return cmap;
|
|
}
|
|
|
|
MuError
|
|
mu_cmd_server (const MuConfig *opts, GError **err) try
|
|
{
|
|
if (opts->commands) {
|
|
Context ctx{};
|
|
auto cmap = make_command_map(ctx);
|
|
invoke(cmap, Sexp::Node::make("(help :full t)"));
|
|
return MU_OK;
|
|
}
|
|
|
|
Context context{opts};
|
|
context.command_map = make_command_map (context);
|
|
|
|
if (opts->eval) { // evaluate command-line command & exit
|
|
auto call{Sexp::Node::make(opts->eval)};
|
|
invoke(context.command_map, call);
|
|
return MU_OK;
|
|
}
|
|
|
|
const auto histpath{std::string{mu_runtime_path(MU_RUNTIME_PATH_CACHE)} + "/history"};
|
|
setup_readline(histpath, 50);
|
|
|
|
install_sig_handler();
|
|
std::cout << ";; Welcome to the " << PACKAGE_STRING << " command-server\n"
|
|
<< ";; Use (help) to get a list of commands, (quit) to quit.\n";
|
|
|
|
while (!MuTerminate && !context.do_quit) {
|
|
|
|
std::string line;
|
|
try {
|
|
line = read_line(context.do_quit);
|
|
if (line.find_first_not_of(" \t") == std::string::npos)
|
|
continue; // skip whitespace-only lines
|
|
|
|
auto call{Sexp::Node::make(line)};
|
|
invoke(context.command_map, call);
|
|
|
|
save_line(line);
|
|
|
|
} catch (const Error& er) {
|
|
std::cerr << ";; error: " << er.what() << "\n";
|
|
g_warning ("error in server: %s", er.what());
|
|
print_error ((MuError)er.code(), "%s (line was:'%s')",
|
|
er.what(), line.c_str());
|
|
}
|
|
}
|
|
shutdown_readline();
|
|
|
|
return MU_OK;
|
|
|
|
} catch (const Error& er) {
|
|
g_critical ("server caught exception: %s", er.what());
|
|
g_set_error(err, MU_ERROR_DOMAIN, MU_ERROR, "%s", er.what());
|
|
return MU_ERROR;
|
|
} catch (...) {
|
|
g_critical ("server caught exception");
|
|
g_set_error(err, MU_ERROR_DOMAIN, MU_ERROR, "%s", "caught exception");
|
|
return MU_ERROR;
|
|
}
|