diff --git a/Makefile.am b/Makefile.am index 695fd42d..f62c2e7a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -42,42 +42,6 @@ cleanupnote: tags: gtags -# this warns about function that have a cyclomatic complexity of > 10, -# which is a sign that it needs some refactoring. requires the pmccabe -# tool. If all is fine, it outputs nothing -cc10: - @$(PMCCABE) `find . -name '*.c' -o -name '*.cc'` \ - | grep -v mu-str-normalize.c \ - | grep -v mu_str_subject_normalize \ - | grep -v tests \ - | sort -nr | awk '($$1 > 10)' - -# this warns about functions that are over 35 non-comment lines long, which is a -# sign that they need some refactoring. requires the pmccabe tool. if -# all is fine, it outputs nothing -# note, some functions are exempted from this rule. -line35: - @$(PMCCABE) -c `find . -name '*.c' -o -name '*.cc'` \ - | grep -v mu-str-normalize.c \ - | grep -v mu_str_subject_normalize \ - | grep -v config_options_group_find \ - | grep -v SCM_DEFINE \ - | grep -v tests \ - | awk '($$5 > 35)' - -# get all todo/fixme messages -fixme: - @grep -i 'FIXME\|TODO' `find src -type f` - -# check whether we can run make distcheck from the repo version -gitcheck: - cd `mktemp -d`; \ - git clone git://github.com/djcb/mu.git ; \ - cd mu; \ - autoreconf -i ; \ - ./configure ; \ - make distcheck - EXTRA_DIST= \ TODO \ HACKING \ diff --git a/mu/Makefile.am b/mu/Makefile.am index bdbceb9d..b0f8d8ae 100644 --- a/mu/Makefile.am +++ b/mu/Makefile.am @@ -53,7 +53,7 @@ mu_SOURCES= \ mu-cmd-extract.c \ mu-cmd-find.c \ mu-cmd-index.c \ - mu-cmd-server.c \ + mu-cmd-server.cc \ mu-cmd-script.c \ mu-cmd.c \ mu-cmd.h @@ -66,7 +66,9 @@ mu-help-strings.h: mu-help-strings.txt mu-help-strings.awk mu_LDADD= \ ${top_builddir}/lib/libmu.la \ + ${top_builddir}/lib/utils/libmu-utils.la \ $(GLIB_LIBS) \ + $(READLINE_LIBS) \ $(CODE_COVERAGE_LIBS) EXTRA_DIST= \ diff --git a/mu/mu-cmd-server.c b/mu/mu-cmd-server.c deleted file mode 100644 index a1279c1c..00000000 --- a/mu/mu-cmd-server.c +++ /dev/null @@ -1,1640 +0,0 @@ -/* -** Copyright (C) 2011-2019 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. -** -*/ - -#if HAVE_CONFIG_H -#include "config.h" -#endif /*HAVE_CONFIG_H*/ - -#define _GNU_SOURCE - -#include -#include -#include -#include -#include -#include -#include - -#include - -/* hopefully, the should get us a sane PATH_MAX */ -#include -/* not all systems provide PATH_MAX in limits.h */ -#ifndef PATH_MAX -#include -#ifndef PATH_MAX -#define PATH_MAX MAXPATHLEN -#endif /*!PATH_MAX */ -#endif /*PATH_MAX */ - -#include "mu-runtime.h" -#include "mu-cmd.h" -#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" - -/* signal handling *****************************************************/ -/* - * when we receive SIGINT, SIGHUP, SIGTERM, set MU_CAUGHT_SIGNAL to - * TRUE - * */ -static gboolean MU_TERMINATE; - -static void -sig_handler (int sig) -{ - MU_TERMINATE = TRUE; -} - -static void -install_sig_handler (void) -{ - struct sigaction action; - int i, sigs[] = { SIGINT, SIGHUP, SIGTERM, SIGPIPE }; - - MU_TERMINATE = 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], 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 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__, strerror(errno)); - /* terminate ourselves */ - raise (SIGTERM); - } -} - - - -G_GNUC_PRINTF(2,3) static MuError -print_error (MuError errcode, const char* frm, ...) -{ - char *msg, *str; - va_list ap; - - va_start (ap, frm); - g_vasprintf (&msg, frm, ap); - va_end (ap); - - str = mu_str_escape_c_literal (msg, TRUE); - print_expr ("(:error %u :message %s)", errcode, str); - - g_free (msg); - g_free (str); - - return errcode; -} - -static MuError -print_and_clear_g_error (GError **err) -{ - MuError rv; - - if (err && *err) - rv = print_error ((*err)->code, "%s", (*err)->message); - else - rv = print_error (MU_ERROR_INTERNAL, "unknown error"); - - g_clear_error (err); - - return rv; -} - - -static GHashTable* -read_command_line (GError **err) -{ - char *line; - GHashTable *hash; - GString *gstr; - - line = NULL; - gstr = g_string_sized_new (512); - - fputs (";; mu> ", stdout); - - do { - int kar; - - kar = fgetc (stdin); - if (kar == '\n' || kar == EOF) - break; - else - gstr = g_string_append_c (gstr, (char)kar); - - } while (1); - - line = g_string_free (gstr, FALSE); - - if (!mu_str_is_empty (line)) - hash = mu_str_parse_arglist (line, err); - else - hash = NULL; - - g_free (line); - - return hash; -} - - -static const char* -get_string_from_args (GHashTable *args, const char *param, gboolean optional, - GError **err) -{ - const char *str; - - str = g_hash_table_lookup (args, param); - if (!str && !optional) - mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, - "parameter '%s' not found", param); - return str; -} - -static gboolean -get_bool_from_args (GHashTable *args, const char *param, gboolean optional, - GError **err) -{ - const char *val; - - val = get_string_from_args (args, param, optional, err); - if (err && (*err != NULL)) - return FALSE; - - if (g_strcmp0 (val, "true") == 0) - return TRUE; - - if (!val || g_strcmp0 (val, "false") == 0) - return FALSE; - - mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, - "invalid value for parameter '%s'", param); - return FALSE; - -} - - -#define GET_STRING_OR_ERROR_RETURN(ARGS,PARAM,SP,E) \ - do { \ - *(SP) = get_string_from_args ((ARGS),(PARAM),FALSE,(E)); \ - if (!(*(SP))) \ - return MU_G_ERROR_CODE((E)); \ - } while (0); - - -/* get a *list* of all messages with the given message id */ -static GSList* -get_docids_from_msgids (MuQuery *query, const char *msgid, - int max, GError **err) -{ - char xprefix; - char *rawq, *tmp; - MuMsgIter *iter; - GSList *lst; - - if (!msgid || strlen(msgid) > MU_STORE_MAX_TERM_LENGTH - 1) { - mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, - "invalid message-id '%s' (length=%zu)", - msgid, strlen(msgid)); - return NULL; - } - - xprefix = mu_msg_field_xapian_prefix(MU_MSG_FIELD_ID_MSGID); - /*XXX this is a bit dodgy */ - tmp = g_ascii_strdown(msgid, -1); - rawq = g_strdup_printf("%c%s", xprefix, tmp); - g_free(tmp); - iter = mu_query_run (query, rawq, MU_MSG_FIELD_ID_NONE, - max, MU_QUERY_FLAG_RAW, err); - g_free (rawq); - - if (!iter || mu_msg_iter_is_done (iter)) { - mu_util_g_set_error (err, MU_ERROR_NO_MATCHES, - "could not find message(s) for msgid %s", - msgid); - return NULL; - } - - lst = NULL; - do { - lst = g_slist_prepend - (lst, GSIZE_TO_POINTER(mu_msg_iter_get_docid (iter))); - } while (mu_msg_iter_next (iter)); - mu_msg_iter_destroy (iter); - - return lst; -} - - -/* the string contains either a number (docid) or a message-id if it - * doesn't look like a number, and the query param is non-nil, try to - * locale the message with message-id in the database, and return its - * docid */ -static unsigned -determine_docid (MuQuery *query, GHashTable *args, GError **err) -{ - GSList *docids; - unsigned docid; - const char* docidstr, *msgidstr; - - docidstr = get_string_from_args (args, "docid", TRUE, err); - if (docidstr) - return atoi (docidstr); - - /* no docid: param; use msgid: instead */ - msgidstr = get_string_from_args (args, "msgid", TRUE, err); - if (!msgidstr) { - mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, - "neither docid nor msgid specified"); - return MU_STORE_INVALID_DOCID; - } - - docids = get_docids_from_msgids (query, msgidstr, 1, err); - if (!docids) - return MU_STORE_INVALID_DOCID; - - docid = GPOINTER_TO_UINT(docids->data); - g_slist_free (docids); - - return docid; -} - - -#define DOCID_VALID_OR_ERROR_RETURN(DOCID,E) \ - if ((DOCID)==MU_STORE_INVALID_DOCID) { \ - mu_util_g_set_error((E),MU_ERROR_IN_PARAMETERS, \ - "invalid docid"); \ - return MU_G_ERROR_CODE((E)); \ - } - - -#define EQSTR(S1,S2) (g_strcmp0((S1),(S2))==0) - - -struct _ServerContext { - MuStore *store; - MuQuery *query; -}; -typedef struct _ServerContext ServerContext; - -/*************************************************************************/ -/* implementation for the commands -- for each command , there is a - * dedicated function cmd_. These function all are of the type CmdFunc - * - * these functions return errors only if they don't handle them themselves, - * where 'handling' is defined as 'report it using print_and_clear_g_error' - * - * if function return non-MU_OK, the repl will print the error instead - */ - -typedef MuError (*CmdFunc) (ServerContext*,GHashTable*,GError**); - -/* '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 MuError -cmd_add (ServerContext *ctx, GHashTable *args, GError **err) -{ - unsigned docid; - const char *maildir, *path; - MuMsg *msg; - gchar *sexp; - - GET_STRING_OR_ERROR_RETURN (args, "path", &path, err); - GET_STRING_OR_ERROR_RETURN (args, "maildir", &maildir, err); - - docid = mu_store_add_path (ctx->store, path, maildir, err); - if (docid == MU_STORE_INVALID_DOCID) - print_and_clear_g_error (err); - else { - gchar *escpath; - escpath = mu_str_escape_c_literal (path, TRUE); - print_expr ("(:info add :path %s :docid %u)", escpath, docid); - - msg = mu_store_get_msg (ctx->store, docid, err); - if (msg) { - sexp = mu_msg_to_sexp (msg, docid, NULL, - MU_MSG_OPTION_VERIFY); - print_expr ("(:update %s :move nil)", sexp); - - mu_msg_unref(msg); - g_free (sexp); - } - g_free (escpath); - } - - return MU_OK; -} - - -struct _PartInfo { - GSList *attlist; - MuMsgOptions opts; -}; -typedef struct _PartInfo PartInfo; - -static void -each_part (MuMsg *msg, MuMsgPart *part, PartInfo *pinfo) -{ - char *att, *cachefile, *encfile; - GError *err; - - /* exclude things that don't look like proper attachments, - * unless they're images */ - if (!mu_msg_part_maybe_attachment(part)) - return; - - err = NULL; - cachefile = mu_msg_part_save_temp (msg, - pinfo->opts|MU_MSG_OPTION_OVERWRITE, - part->index, &err); - if (!cachefile) { - print_and_clear_g_error (&err); - return; - } - - encfile = mu_str_escape_c_literal(cachefile, TRUE); - g_free (cachefile); - - att = g_strdup_printf ( - "(:file-name %s :mime-type \"%s/%s\")", - encfile, part->type, part->subtype); - pinfo->attlist = g_slist_append (pinfo->attlist, att); - g_free (encfile); -} - - -/* take the attachments of msg, save them as tmp files, and return - * as sexp (as a string) describing them - * - * ((:name :mime-type :disposition - * ) ... ) - * - */ -static gchar* -include_attachments (MuMsg *msg, MuMsgOptions opts) -{ - GSList *cur; - GString *gstr; - PartInfo pinfo; - - pinfo.attlist = NULL; - pinfo.opts = opts; - mu_msg_part_foreach (msg, opts, - (MuMsgPartForeachFunc)each_part, - &pinfo); - - gstr = g_string_sized_new (512); - gstr = g_string_append_c (gstr, '('); - for (cur = pinfo.attlist; cur; cur = g_slist_next (cur)) - g_string_append (gstr, (gchar*)cur->data); - gstr = g_string_append_c (gstr, ')'); - - mu_str_free_list (pinfo.attlist); - - return g_string_free (gstr, FALSE); -} - -static MuMsgOptions -get_encrypted_msg_opts (GHashTable *args) -{ - MuMsgOptions opts; - - opts = MU_MSG_OPTION_NONE; - - if (get_bool_from_args (args, "use-agent", FALSE, NULL)) - opts |= MU_MSG_OPTION_USE_AGENT; - if (get_bool_from_args (args, "extract-encrypted", FALSE, NULL)) - opts |= MU_MSG_OPTION_DECRYPT; - - return opts; -} - -enum { NEW, REPLY, FORWARD, EDIT, RESEND, INVALID_TYPE }; -static unsigned -compose_type (const char *typestr) -{ - if (EQSTR (typestr, "reply")) - return REPLY; - else if (EQSTR (typestr, "forward")) - return FORWARD; - else if (EQSTR (typestr, "edit")) - return EDIT; - else if (EQSTR (typestr, "resend")) - return RESEND; - else if (EQSTR (typestr, "new")) - return NEW; - else - return INVALID_TYPE; -} - -/* '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 MuError -cmd_compose (ServerContext *ctx, GHashTable *args, GError **err) -{ - const gchar *typestr; - char *sexp, *atts; - unsigned ctype; - MuMsgOptions opts; - - opts = get_encrypted_msg_opts (args); - - GET_STRING_OR_ERROR_RETURN (args, "type", &typestr, err); - - ctype = compose_type (typestr); - if (ctype == INVALID_TYPE) { - print_error (MU_ERROR_IN_PARAMETERS, "invalid type to compose"); - return MU_OK; - } - - if (ctype == REPLY || ctype == FORWARD || - ctype == EDIT || ctype == RESEND) { - MuMsg *msg; - const char *docidstr; - GET_STRING_OR_ERROR_RETURN (args, "docid", &docidstr, err); - msg = mu_store_get_msg (ctx->store, atoi(docidstr), err); - if (!msg) { - print_and_clear_g_error (err); - return MU_OK; - } - sexp = mu_msg_to_sexp (msg, atoi(docidstr), NULL, opts); - atts = (ctype == FORWARD) ? - include_attachments (msg, opts) : NULL; - mu_msg_unref (msg); - } else - atts = sexp = NULL; - - print_expr ("(:compose %s :original %s :include %s)", - typestr, sexp ? sexp : "nil", atts ? atts : "nil"); - - g_free (sexp); - g_free (atts); - - return MU_OK; -} - - -struct _SexpData { - GString *gstr; - gboolean personal; - time_t last_seen; - gint64 tstamp; - size_t rank; -}; -typedef struct _SexpData SexpData; - -static void -each_contact_sexp (const char* full_address, - const char *email, const char *name, gboolean personal, - time_t last_seen, unsigned freq, - gint64 tstamp, SexpData *sdata) -{ - char *escaddr; - - sdata->rank++; - - /* since the last time we got some contacts */ - if (sdata->tstamp > tstamp) - return; - - /* (maybe) only include 'personal' contacts */ - if (sdata->personal && !personal) - return; - - /* only include newer-than-x contacts */ - if (sdata->last_seen > last_seen) - return; - - /* only include *real* e-mail addresses (ignore local - * addresses... there's little to complete there anyway...) */ - if (!email || !strstr (email, "@")) - return; - - escaddr = mu_str_escape_c_literal (full_address, TRUE); - - g_string_append_printf (sdata->gstr, "(%s . %zu)\n", - escaddr, sdata->rank); - - g_free (escaddr); -} - - -/** - * get all contacts as an s-expression - * - * @param self contacts object - * @param personal_only whether to restrict the list to 'personal' email - * addresses - * - * @return the sexp - */ -static char* -contacts_to_sexp (const MuContacts *contacts, gboolean personal, - time_t last_seen, gint64 tstamp) -{ - SexpData sdata; - gint64 cutoff; - - g_return_val_if_fail (contacts, NULL); - - memset (&sdata, 0, sizeof(SexpData)); - - sdata.personal = personal; - sdata.last_seen = last_seen; - sdata.tstamp = tstamp; - sdata.rank = 0; - - /* make a guess for the initial size */ - sdata.gstr = g_string_sized_new (mu_contacts_count(contacts) * 128); - g_string_append (sdata.gstr, "(:contacts ("); - - cutoff = g_get_monotonic_time(); - mu_contacts_foreach (contacts, - (MuContactsForeachFunc)each_contact_sexp, &sdata); - /* pass a string, elisp doesn't like 64-bit nums */ - g_string_append_printf (sdata.gstr, - ") :tstamp \"%" G_GINT64_FORMAT "\")", cutoff); - - return g_string_free (sdata.gstr, FALSE); -} - - -static MuError -cmd_contacts (ServerContext *ctx, GHashTable *args, GError **err) -{ - const MuContacts *contacts; - char *sexp; - gboolean personal; - time_t after; - const char *str; - gint64 tstamp; - - personal = get_bool_from_args (args, "personal", TRUE, NULL); - str = get_string_from_args (args, "after", TRUE, NULL); - after = str ? (time_t)atoi(str) : 0; - - // only get contacts updated since tstamp. - str = get_string_from_args (args, "tstamp", TRUE, NULL); - tstamp = str ? g_ascii_strtoll (str, NULL, 10) : 0; - - contacts = mu_store_contacts(ctx->store); - if (!contacts) { - print_error (MU_ERROR_INTERNAL, - "failed to get contacts cache"); - return MU_OK; - } - - /* dump the contacts cache as a giant sexp */ - sexp = contacts_to_sexp (contacts, personal, after, tstamp); - print_expr ("%s\n", sexp); - g_free (sexp); - - return MU_OK; -} - - - -static unsigned -print_sexps (MuMsgIter *iter, unsigned maxnum) -{ - unsigned u; - u = 0; - - while (!mu_msg_iter_is_done (iter) && u < maxnum && !MU_TERMINATE) { - - 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; -} - - -static MuError -save_part (MuMsg *msg, unsigned docid, unsigned index, - MuMsgOptions opts, GHashTable *args, GError **err) -{ - gboolean rv; - const gchar *path; - gchar *escpath; - - GET_STRING_OR_ERROR_RETURN (args, "path", &path, err); - - rv = mu_msg_part_save (msg, opts | MU_MSG_OPTION_OVERWRITE, - path, index, err); - if (!rv) { - print_and_clear_g_error (err); - return MU_OK; - } - - escpath = mu_str_escape_c_literal (path, FALSE); - print_expr ("(:info save :message \"%s has been saved\")", - escpath); - - g_free (escpath); - return MU_OK; -} - - -static MuError -open_part (MuMsg *msg, unsigned docid, unsigned index, - MuMsgOptions opts, GError **err) -{ - char *targetpath; - gboolean rv; - - targetpath = mu_msg_part_get_cache_path (msg, opts, index, err); - if (!targetpath) - return print_and_clear_g_error (err); - - rv = mu_msg_part_save (msg, opts | MU_MSG_OPTION_USE_EXISTING, - targetpath, index, err); - if (!rv) { - print_and_clear_g_error (err); - goto leave; - } - - rv = mu_util_play (targetpath, TRUE,/*allow local*/ - FALSE/*allow remote*/, err); - if (!rv) - print_and_clear_g_error (err); - else { - gchar *path; - path = mu_str_escape_c_literal (targetpath, FALSE); - print_expr ("(:info open :message \"%s has been opened\")", - path); - g_free (path); - } -leave: - g_free (targetpath); - return MU_OK; -} - - -static MuError -temp_part (MuMsg *msg, unsigned docid, unsigned index, - MuMsgOptions opts, GHashTable *args, GError **err) -{ - const char *what, *param; - char *path; - - GET_STRING_OR_ERROR_RETURN (args, "what", &what, err); - param = get_string_from_args (args, "param", TRUE, NULL); - - path = mu_msg_part_get_cache_path (msg, opts, index, err); - if (!path) - print_and_clear_g_error (err); - else if (!mu_msg_part_save (msg, opts | MU_MSG_OPTION_USE_EXISTING, - path, index, err)) - print_and_clear_g_error (err); - else { - gchar *escpath; - escpath = mu_str_escape_c_literal (path, FALSE); - - if (param) { - char *escparam; - escparam = mu_str_escape_c_literal (param, FALSE); - print_expr ("(:temp \"%s\"" - " :what \"%s\"" - " :docid %u" - " :param \"%s\""")", - escpath, what, docid, escparam); - g_free (escparam); - } else - print_expr ("(:temp \"%s\" :what \"%s\" :docid %u)", - escpath, what, docid); - - g_free (escpath); - - } - - g_free (path); - return MU_OK; -} - -enum { SAVE, OPEN, TEMP, INVALID_ACTION }; -static int -action_type (const char *actionstr) -{ - if (EQSTR (actionstr, "save")) - return SAVE; - else if (EQSTR (actionstr, "open")) - return OPEN; - else if (EQSTR (actionstr, "temp")) - return TEMP; - else - return INVALID_ACTION; -} - -/* 'extract' extracts some mime part from a message */ -static MuError -cmd_extract (ServerContext *ctx, GHashTable *args, GError **err) -{ - MuMsg *msg; - int docid, index, action; - MuError rv; - MuMsgOptions opts; - const char* actionstr, *indexstr; - - opts = get_encrypted_msg_opts (args); - rv = MU_ERROR; - - /* read parameters */ - GET_STRING_OR_ERROR_RETURN (args, "action", &actionstr, err); - GET_STRING_OR_ERROR_RETURN (args, "index", &indexstr, err); - index = atoi (indexstr); - docid = determine_docid (ctx->query, args, err); - if (docid == MU_STORE_INVALID_DOCID) { - print_and_clear_g_error (err); - return MU_OK; - } - - if ((action = action_type (actionstr)) == INVALID_ACTION) { - print_error (MU_ERROR_IN_PARAMETERS, "invalid action"); - return MU_OK; - } - msg = mu_store_get_msg (ctx->store, docid, err); - if (!msg) { - print_error (MU_ERROR, "failed to get message"); - return MU_OK; - } - - switch (action) { - case SAVE: rv = save_part (msg, docid, index, opts, args, err); break; - case OPEN: rv = open_part (msg, docid, index, opts, err); break; - case TEMP: rv = temp_part (msg, docid, index, opts, args, err); break; - default: print_error (MU_ERROR_INTERNAL, "unknown action"); - } - - if (rv != MU_OK) - print_and_clear_g_error (err); - - mu_msg_unref (msg); - return MU_OK; -} - -#define MAX_NUM_DEFAULT 500 - -/* parse the find parameters, and return the values as out params */ -static MuError -get_find_params (GHashTable *args, MuMsgFieldId *sortfield, - int *maxnum, MuQueryFlags *qflags, GError **err) -{ - const char *maxnumstr, *sortfieldstr; - - /* defaults */ - *maxnum = MAX_NUM_DEFAULT; - *qflags = MU_QUERY_FLAG_NONE; - *sortfield = MU_MSG_FIELD_ID_NONE; - - /* field to sort by */ - sortfieldstr = get_string_from_args (args, "sortfield", TRUE, NULL); - if (sortfieldstr) { - *sortfield = mu_msg_field_id_from_name (sortfieldstr, FALSE); - /* note: shortcuts are not allowed here */ - if (*sortfield == MU_MSG_FIELD_ID_NONE) { - mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, - "not a valid sort field: '%s'", - sortfield); - return MU_G_ERROR_CODE(err); - } - } else - *sortfield = MU_MSG_FIELD_ID_DATE; - - - /* maximum number of results */ - maxnumstr = get_string_from_args (args, "maxnum", TRUE, NULL); - if (maxnumstr) - *maxnum = atoi (maxnumstr); - - if (get_bool_from_args (args, "reverse", TRUE, NULL)) - *qflags |= MU_QUERY_FLAG_DESCENDING; - if (get_bool_from_args (args, "skip-dups", TRUE, NULL)) - *qflags |= MU_QUERY_FLAG_SKIP_DUPS; - if (get_bool_from_args (args, "include-related", TRUE, NULL)) - *qflags |= MU_QUERY_FLAG_INCLUDE_RELATED; - if (get_bool_from_args (args, "threads", TRUE, NULL)) - *qflags |= MU_QUERY_FLAG_THREADS; - - return MU_OK; -} - - -/* - * 'find' finds a list of messages matching some query, and takes a - * parameter 'query' with the search query, and (optionally) a - * parameter 'maxnum' with the maximum number of messages to return. - * - * returns: - * => list of s-expressions, each describing a message => - * (:found ) - */ -static MuError -cmd_find (ServerContext *ctx, GHashTable *args, GError **err) -{ - MuMsgIter *iter; - unsigned foundnum; - int maxnum; - MuMsgFieldId sortfield; - const char *querystr; - MuQueryFlags qflags; - char *query; - - GET_STRING_OR_ERROR_RETURN (args, "query", &querystr, err); - if (get_find_params (args, &sortfield, &maxnum, &qflags, err) - != MU_OK) { - print_and_clear_g_error (err); - return MU_OK; - } - - { - char *s; - gsize len; - s = (char*)g_base64_decode (querystr, &len); - query = g_strndup (s, len); - g_free (s); - } - - /* note: when we're threading, we get *all* matching messages, - * and then only return maxnum; this is so that we maximimize - * the change of all messages in a thread showing up */ - - iter = mu_query_run (ctx->query, query, sortfield, - maxnum, qflags, err); - g_free (query); - if (!iter) { - print_and_clear_g_error (err); - return MU_OK; - } - - /* 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. */ - print_expr ("(:erase t)"); - foundnum = print_sexps (iter, maxnum); - print_expr ("(:found %u)", foundnum); - mu_msg_iter_destroy (iter); - - return MU_OK; -} - -static MuError -index_msg_cb (MuIndexStats *stats, void *user_data) -{ - if (MU_TERMINATE) - return MU_STOP; - - if (stats->_processed % 1000) - return MU_OK; - - print_expr ("(:info index :status running " - ":processed %u :updated %u)", - stats->_processed, stats->_updated); - - return MU_OK; -} - - -static void -set_my_addresses (MuStore *store, const char *addrstr) -{ - char **my_addresses; - - if (!addrstr) - return; - - my_addresses = g_strsplit (addrstr, ",", -1); - mu_store_set_personal_addresses (store, (const char**)my_addresses); - - g_strfreev (my_addresses); -} - - -static char* -get_checked_path (const char *path) -{ - char *cpath; - - cpath = mu_util_dir_expand(path); - if (!cpath || - !mu_util_check_dir (cpath, TRUE, FALSE)) { - print_error (MU_ERROR_IN_PARAMETERS, - "not a readable dir: '%s'", cpath); - g_free (cpath); - return NULL; - } - - return cpath; -} - - -static MuError -index_and_maybe_cleanup (MuIndex *index, const char *path, - gboolean cleanup, gboolean lazy_check, GError **err) -{ - MuError rv; - MuIndexStats stats, stats2; - - mu_index_stats_clear (&stats); - rv = mu_index_run (index, path, FALSE, lazy_check, &stats, - index_msg_cb, NULL, NULL); - - if (rv != MU_OK && rv != MU_STOP) { - mu_util_g_set_error (err, MU_ERROR_INTERNAL, "indexing failed"); - return rv; - } - - mu_index_stats_clear (&stats2); - if (cleanup) { - rv = mu_index_cleanup (index, &stats2, NULL, NULL, err); - if (rv != MU_OK && rv != MU_STOP) { - mu_util_g_set_error (err, MU_ERROR_INTERNAL, - "cleanup failed"); - return rv; - } - } - - print_expr ("(:info index :status complete " - ":processed %u :updated %u :cleaned-up %u)", - stats._processed, stats._updated, stats2._cleaned_up); - - return rv; -} - -/* - * 'index' (re)indexs maildir at path:, and responds with (:info - * index ... ) messages while doing so (see the code) - */ -static MuError -cmd_index (ServerContext *ctx, GHashTable *args, GError **err) -{ - MuIndex *index; - const char *argpath; - char *path; - gboolean cleanup, lazy_check; - - index = NULL; - - GET_STRING_OR_ERROR_RETURN (args, "path", &argpath, err); - if (!(path = get_checked_path (argpath))) - goto leave; - - set_my_addresses (ctx->store, get_string_from_args - (args, "my-addresses", TRUE, NULL)); - - if (!(index = mu_index_new (ctx->store, err))) - goto leave; - - cleanup = get_bool_from_args (args, "cleanup", TRUE, NULL); - lazy_check = get_bool_from_args (args, "lazy-check", TRUE, NULL); - lazy_check = get_bool_from_args (args, "contacts", TRUE, NULL); - - index_and_maybe_cleanup (index, path, - cleanup, lazy_check, - err); -leave: - g_free (path); - - if (err && *err) - print_and_clear_g_error (err); - - mu_index_destroy (index); - - return MU_OK; -} - - - -/* 'mkdir' attempts to create a maildir directory at 'path:'; sends an - * (:info mkdir ...) message when it succeeds */ -static MuError -cmd_mkdir (ServerContext *ctx, GHashTable *args, GError **err) -{ - const char *path; - - GET_STRING_OR_ERROR_RETURN (args, "path", &path, err); - - if (!mu_maildir_mkdir (path, 0755, FALSE, err)) - print_and_clear_g_error (err); - else - print_expr ("(:info mkdir :message \"%s has been created\")", - path); - - return MU_OK; -} - - - -static MuFlags -get_flags (const char *path, const char *flagstr) -{ - if (!flagstr) - 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, "+") || strstr (flagstr, "-")) { - MuFlags oldflags; - oldflags = mu_maildir_get_flags_from_path (path); - return mu_flags_from_str_delta (flagstr, oldflags, - MU_FLAG_TYPE_ANY); - } else - return mu_flags_from_str (flagstr, MU_FLAG_TYPE_ANY, - TRUE /*ignore invalid*/); - } -} - - -static MuError -do_move (MuStore *store, unsigned docid, MuMsg *msg, const char *maildir, - MuFlags flags, gboolean new_name, gboolean no_view, GError **err) -{ - unsigned rv; - gchar *sexp; - gboolean different_mdir; - - if (!maildir) { - maildir = mu_msg_get_maildir (msg); - different_mdir = FALSE; - } else - /* are we moving to a different mdir, or is it just flags? */ - different_mdir = - (g_strcmp0 (maildir, mu_msg_get_maildir(msg)) != 0); - - if (!mu_msg_move_to_maildir (msg, maildir, flags, TRUE, - new_name, err)) - return MU_G_ERROR_CODE (err); - - /* after mu_msg_move_to_maildir, path will be the *new* path, - * and flags and maildir fields will be updated as wel */ - rv = mu_store_update_msg (store, docid, msg, err); - if (rv == MU_STORE_INVALID_DOCID) { - mu_util_g_set_error (err, MU_ERROR_XAPIAN, - "failed to store updated message"); - print_and_clear_g_error (err); - } - - sexp = mu_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 */ - print_expr ("(:update %s :move %s :maybe-view %s)", sexp, - different_mdir ? "t" : "nil", - no_view ? "nil" : "t"); - g_free (sexp); - - return MU_OK; -} - -static MuError -move_docid (MuStore *store, unsigned docid, const char* flagstr, - gboolean new_name, gboolean no_view, GError **err) -{ - MuMsg *msg; - MuError rv; - MuFlags flags; - - rv = MU_ERROR; - msg = mu_store_get_msg (store, docid, err); - - if (!msg) - goto leave; - - flags = flagstr ? get_flags (mu_msg_get_path(msg), flagstr) : - mu_msg_get_flags (msg); - - if (flags == MU_FLAG_INVALID) { - mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, - "invalid flags"); - goto leave; - } - - rv = do_move (store, docid, msg, NULL, flags, - new_name, no_view, err); - -leave: - if (msg) - mu_msg_unref (msg); - if (rv != MU_OK) - print_and_clear_g_error (err); - - return rv; -} - - -/* when called with a msgid, we need to take care of possibly multiple - * messages with this message id. this is a common case when sending - * messages to ourselves (maybe through a mailing list), where there - * would a message in the inbox and sent folders with the same id. we - * set the flag on both */ -static gboolean -move_msgid_maybe (ServerContext *ctx, GHashTable *args, GError **err) -{ - GSList *docids, *cur; - const char* maildir, *msgid, *flagstr; - gboolean new_name, no_view; - - maildir = get_string_from_args (args, "maildir", TRUE, err); - msgid = get_string_from_args (args, "msgid", TRUE, err); - flagstr = get_string_from_args (args, "flags", TRUE, err); - new_name = get_bool_from_args (args, "newname", TRUE, err); - no_view = get_bool_from_args (args, "noupdate", TRUE, err); - - /* you cannot use 'maildir' for multiple messages at once */ - if (!msgid || !flagstr || maildir) - return FALSE; - - if (!(docids = get_docids_from_msgids (ctx->query, msgid, - -1/*unlimited*/, err))) { - print_and_clear_g_error (err); - return TRUE; - } - - for (cur = docids; cur; cur = g_slist_next(cur)) - if (move_docid (ctx->store, GPOINTER_TO_SIZE(cur->data), - flagstr, new_name, no_view, err) != MU_OK) - break; - - g_slist_free (docids); - - return TRUE; -} - - - -/* - * '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 ) - * - */ -static MuError -cmd_move (ServerContext *ctx, GHashTable *args, GError **err) -{ - unsigned docid; - MuMsg *msg; - MuFlags flags; - const char *maildir, *flagstr; - gboolean new_name, no_view; - - /* check if the move is based on the message id; if so, handle - * it in move_msgid_maybe */ - if (move_msgid_maybe (ctx, args, err)) - return MU_OK; - - maildir = get_string_from_args (args, "maildir", TRUE, err); - flagstr = get_string_from_args (args, "flags", TRUE, err); - new_name = get_bool_from_args (args, "newname", TRUE, err); - no_view = get_bool_from_args (args, "noupdate", TRUE, err); - - docid = determine_docid (ctx->query, args, err); - if (docid == MU_STORE_INVALID_DOCID || - !(msg = mu_store_get_msg (ctx->store, docid, err))) { - print_and_clear_g_error (err); - return MU_OK; - } - - /* if maildir was not specified, take the current one */ - if (!maildir) - 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. */ - if (flagstr) - flags = get_flags (mu_msg_get_path(msg), flagstr); - else - flags = mu_msg_get_flags (msg); - - if (flags == MU_FLAG_INVALID) { - print_error (MU_ERROR_IN_PARAMETERS, "invalid flags"); - goto leave; - } - - if ((do_move (ctx->store, docid, msg, maildir, flags, - new_name, no_view, err) - != MU_OK)) - print_and_clear_g_error (err); - -leave: - mu_msg_unref (msg); - return MU_OK; -} - - - -/* 'ping' takes no parameters, and provides information about this mu - * server using a (:pong ...) message (details: see code below) - */ -static MuError -cmd_ping (ServerContext *ctx, GHashTable *args, GError **err) -{ - unsigned doccount; - doccount = mu_store_count (ctx->store, err); - - if (doccount == (unsigned)-1) - return print_and_clear_g_error (err); - - print_expr ("(:pong \"" PACKAGE_NAME "\" " - " :props (:crypto %s :guile %s " - " :version \"" VERSION "\" " - " :doccount %u))", - mu_util_supports (MU_FEATURE_CRYPTO) ? "t" : "nil", - mu_util_supports (MU_FEATURE_GUILE|MU_FEATURE_GNUPLOT) - ? "t" : "nil", - doccount); - - return MU_OK; -} - - -/* 'quit' takes no parameters, terminates this mu server */ -static MuError -cmd_quit (ServerContext *ctx, GHashTable *args , GError **err) -{ - print_expr (";; quitting"); - - return MU_STOP; -} - - -/* - * 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 - * - * NOTE: not re-entrant. - */ -static const char* -get_path_from_docid (MuStore *store, unsigned docid, GError **err) -{ - MuMsg *msg; - const char* msgpath; - static char path[PATH_MAX + 1]; - - msg = mu_store_get_msg (store, docid, err); - if (!msg) - return NULL; - - msgpath = mu_msg_get_path (msg); - if (!msgpath) { - mu_msg_unref (msg); - return NULL; - } - - strncpy (path, msgpath, sizeof(path) - 1); - path[sizeof(path)-1] = '\0'; - - mu_msg_unref (msg); - return path; -} - - - -/* 'remove' removes the message with either docid: or msgid:, sends a - * (:remove ...) message when it succeeds - */ -static MuError -cmd_remove (ServerContext *ctx, GHashTable *args, GError **err) -{ - unsigned docid; - const char *path; - - docid = determine_docid (ctx->query, args, err); - if (docid == MU_STORE_INVALID_DOCID) { - print_and_clear_g_error (err); - return MU_OK; - } - - path = get_path_from_docid (ctx->store, docid, err); - if (!path) { - print_and_clear_g_error (err); - return MU_OK; - } - - if (unlink (path) != 0) { - mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_UNLINK, - "%s", strerror (errno)); - print_and_clear_g_error (err); - return MU_OK; - } - - if (!mu_store_remove_path (ctx->store, path)) { - print_error (MU_ERROR_XAPIAN_REMOVE_FAILED, - "failed to remove from database"); - return MU_OK; - } - - print_expr ("(:remove %u)", docid); - return MU_OK; -} - -/* '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 MuError -cmd_sent (ServerContext *ctx, GHashTable *args, GError **err) -{ - unsigned docid; - const char *maildir, *path; - - GET_STRING_OR_ERROR_RETURN (args, "path", &path, err); - GET_STRING_OR_ERROR_RETURN (args, "maildir", &maildir, err); - - docid = mu_store_add_path (ctx->store, path, maildir, err); - if (docid == MU_STORE_INVALID_DOCID) - print_and_clear_g_error (err); - else { - gchar *escpath; - escpath = mu_str_escape_c_literal (path, TRUE); - print_expr ("(:sent t :path %s :docid %u)", - escpath, docid); - g_free (escpath); - } - - return MU_OK; -} - -static MuMsgOptions -get_view_msg_opts (GHashTable *args) -{ - MuMsgOptions opts; - - opts = MU_MSG_OPTION_VERIFY; - - if (get_bool_from_args (args, "extract-images", FALSE, NULL)) - opts |= MU_MSG_OPTION_EXTRACT_IMAGES; - if (get_bool_from_args (args, "use-agent", FALSE, NULL)) - opts |= MU_MSG_OPTION_USE_AGENT; - if (get_bool_from_args (args, "auto-retrieve-key", FALSE, NULL)) - opts |= MU_MSG_OPTION_AUTO_RETRIEVE; - if (get_bool_from_args (args, "extract-encrypted", FALSE, NULL)) - opts |= MU_MSG_OPTION_DECRYPT; - - return opts; -} - -/* 'view' gets a full (including body etc.) sexp for some message, - * identified by either docid: or msgid:; return a (:view ) - */ -static MuError -cmd_view (ServerContext *ctx, GHashTable *args, GError **err) -{ - MuMsg *msg; - const gchar *path; - char *sexp; - MuMsgOptions opts; - unsigned docid; - - opts = get_view_msg_opts (args); - - /* when 'path' is specified, get the message at path */ - path = get_string_from_args (args, "path", FALSE, NULL); - - if (path) { - docid = 0; - msg = mu_msg_new_from_file (path, NULL, err); - } else { - docid = determine_docid (ctx->query, args, err); - if (docid == MU_STORE_INVALID_DOCID) { - print_and_clear_g_error (err); - return MU_OK; - } - msg = mu_store_get_msg (ctx->store, docid, err); - } - - if (!msg) { - print_and_clear_g_error (err); - return MU_OK; - } - - sexp = mu_msg_to_sexp (msg, docid, NULL, opts); - mu_msg_unref (msg); - - print_expr ("(:view %s)\n", sexp); - g_free (sexp); - - return MU_OK; -} - -/*************************************************************************/ - -static MuError -handle_args (ServerContext *ctx, GHashTable *args, GError **err) -{ - unsigned u; - const char *cmd; - struct { - const char *cmd; - CmdFunc func; - } cmd_map[] = { - { "add", cmd_add }, - { "compose", cmd_compose }, - { "contacts", cmd_contacts }, - { "extract", cmd_extract }, - { "find", cmd_find }, - { "index", cmd_index }, - { "mkdir", cmd_mkdir }, - { "move", cmd_move }, - { "ping", cmd_ping }, - { "quit", cmd_quit }, - { "remove", cmd_remove }, - { "sent", cmd_sent }, - { "view", cmd_view } - }; - - cmd = g_hash_table_lookup (args, "cmd"); - - /* ignore empty */ - if (mu_str_is_empty (cmd)) - return MU_OK; - - for (u = 0; u != G_N_ELEMENTS (cmd_map); ++u) - if (g_strcmp0(cmd, cmd_map[u].cmd) == 0) - return cmd_map[u].func (ctx, args, err); - - mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, - "unknown command '%s'", cmd ? cmd : ""); - - return MU_G_ERROR_CODE (err); -} - - -MuError -mu_cmd_server (MuStore *store, MuConfig *opts/*unused*/, GError **err) -{ - ServerContext ctx; - gboolean do_quit; - - g_return_val_if_fail (store, MU_ERROR_INTERNAL); - - ctx.store = store; - ctx.query = mu_query_new (store, err); - if (!ctx.query) - return MU_G_ERROR_CODE (err); - - srand (time(NULL)*getpid()); - - install_sig_handler (); - - g_print (";; welcome to " PACKAGE_STRING "\n"); - - /* the main REPL */ - do_quit = FALSE; - while (!MU_TERMINATE && !do_quit) { - - GHashTable *args; - GError *my_err; - - /* args will receive a the command as a list of - * strings. returning NULL indicates an error */ - my_err = NULL; - args = read_command_line (&my_err); - if (!args) { - if (feof(stdin)) - break; - if (my_err) - print_and_clear_g_error (&my_err); - continue; - } - - switch (handle_args (&ctx, args, &my_err)) { - case MU_OK: - break; - case MU_STOP: - do_quit = TRUE; - break; - default: /* some error occurred */ - print_and_clear_g_error (&my_err); - } - - g_hash_table_destroy (args); - } - - mu_store_flush (ctx.store); - mu_query_destroy (ctx.query); - - return MU_OK; -} diff --git a/mu/mu-cmd-server.cc b/mu/mu-cmd-server.cc new file mode 100644 index 00000000..8ad957b3 --- /dev/null +++ b/mu/mu-cmd-server.cc @@ -0,0 +1,1280 @@ +/* +** 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-cmd.h" + + +#include +#include +#include + +#include +#include + +#ifdef HAVE_LIBREADLINE +#if defined(HAVE_READLINE_READLINE_H) +#include +#elif defined(HAVE_READLINE_H) +#include +# endif /* !defined(HAVE_READLINE_H) */ +#endif /* HAVE_LIBREADLINE */ + +#ifdef HAVE_READLINE_HISTORY +#if defined(HAVE_READLINE_HISTORY_H) +#include +#elif defined(HAVE_HISTORY_H) +#include +#endif +#endif + +#include "mu-runtime.h" +#include "mu-cmd.h" +#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" + +using namespace Mu; +using namespace Command; +using namespace Sexp; + +using DocId = unsigned; + +struct Context { + Context() {} + Context (MuStore *storearg): store{storearg} { + if (!store) + return; + + GError *gerr{}; + query = mu_query_new (store, &gerr); + if (!query) + throw Error(Error::Code::Store, &gerr, "failed to create query"); + } + + ~Context() { + if (query) + mu_query_destroy(query); + if (store) + mu_store_flush(store); + } + + Context(const Context&) = delete; + + MuStore *store{}; + MuQuery *query{}; + bool do_quit{}; + + CommandMap command_map; +}; + + +/* + * 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 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__, strerror(errno)); + /* terminate ourselves */ + raise (SIGTERM); + } +} + + +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); + + print_expr ("(:error %u :message %s)", errcode, quoted(msg).c_str()); + 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; +} + + +static MuMsgOptions +message_options (const Parameters& params) +{ + const auto extract_images{get_bool_or(params, "extract-images", false)}; + const auto extract_encrypted{get_bool_or(params, "extract-encrypted", false)}; + + int opts{MU_MSG_OPTION_VERIFY | MU_MSG_OPTION_USE_AGENT}; + if (extract_images) + opts |= MU_MSG_OPTION_EXTRACT_IMAGES; + if (extract_encrypted) + opts |= MU_MSG_OPTION_DECRYPT; + + 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) +{ + const auto path{get_string_or(params, "path")}; + const auto maildir{get_string_or(params, "maildir")}; + + GError *gerr{}; + const auto docid{mu_store_add_path (context.store, path.c_str(), maildir.c_str(), &gerr)}; + if (docid == MU_STORE_INVALID_DOCID) + throw Error(Error::Code::Store, &gerr, "failed to add message at %s", + path.c_str()); + + print_expr ("(:info add :path %s :docid %u)", quoted(path).c_str(), docid); + + auto msg{mu_store_get_msg(context.store, docid, &gerr)}; + if (!msg) + throw Error(Error::Code::Store, &gerr, "failed to get message at %s", + path.c_str()); + + auto sexp{mu_msg_to_sexp (msg, docid, NULL, MU_MSG_OPTION_VERIFY)}; + print_expr ("(:update %s :move nil)", sexp); + mu_msg_unref(msg); + g_free (sexp); +} + + +struct _PartInfo { + GSList *attlist; + MuMsgOptions opts; +}; +typedef struct _PartInfo PartInfo; + +static void +each_part (MuMsg *msg, MuMsgPart *part, PartInfo *pinfo) +{ + char *att, *cachefile; + + /* exclude things that don't look like proper attachments, + * unless they're images */ + if (!mu_msg_part_maybe_attachment(part)) + return; + + GError *gerr{}; + 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"); + + att = g_strdup_printf ("(:file-name %s :mime-type \"%s/%s\")", + quoted(cachefile).c_str(), part->type, part->subtype); + pinfo->attlist = g_slist_append (pinfo->attlist, att); + + g_free (cachefile); + +} + + +/* take the attachments of msg, save them as tmp files, and return + * as sexp (as a string) describing them + * + * ((:name :mime-type :disposition + * ) ... ) + * + */ +static gchar* +include_attachments (MuMsg *msg, MuMsgOptions opts) +{ + GSList *cur; + GString *gstr; + PartInfo pinfo; + + pinfo.attlist = NULL; + pinfo.opts = opts; + mu_msg_part_foreach (msg, opts, + (MuMsgPartForeachFunc)each_part, + &pinfo); + + gstr = g_string_sized_new (512); + gstr = g_string_append_c (gstr, '('); + for (cur = pinfo.attlist; cur; cur = g_slist_next (cur)) + g_string_append (gstr, (gchar*)cur->data); + gstr = g_string_append_c (gstr, ')'); + + mu_str_free_list (pinfo.attlist); + + return g_string_free (gstr, FALSE); +} + +enum { NEW, REPLY, FORWARD, EDIT, RESEND, INVALID_TYPE }; +static unsigned +compose_type (const char *typestr) +{ + if (g_str_equal (typestr, "reply")) + return REPLY; + else if (g_str_equal (typestr, "forward")) + return FORWARD; + else if (g_str_equal (typestr, "edit")) + return EDIT; + else if (g_str_equal (typestr, "resend")) + return RESEND; + else if (g_str_equal (typestr, "new")) + return NEW; + else + return INVALID_TYPE; +} + +/* '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 void +compose_handler (Context& context, const Parameters& params) +{ + const auto typestr{get_symbol_or(params, "type")}; + const auto ctype{compose_type(typestr.c_str())}; + if (ctype == INVALID_TYPE) + throw Error(Error::Code::InvalidArgument, "invalid compose type"); + + // message optioss below checks extract-images / extract-encrypted + + char *sexp{}, *atts{}; + if (ctype == REPLY || ctype == FORWARD || ctype == EDIT || ctype == RESEND) { + + GError *gerr{}; + const unsigned docid{(unsigned)get_int_or(params, "docid")}; + auto msg{mu_store_get_msg (context.store, docid, &gerr)}; + if (!msg) + throw Error{Error::Code::Store, &gerr, "faile to get message %u", docid}; + + const auto opts{message_options(params)}; + sexp = mu_msg_to_sexp (msg, docid, NULL, opts); + atts = (ctype == FORWARD) ? include_attachments (msg, opts) : NULL; + mu_msg_unref (msg); + } + print_expr ("(:compose %s :original %s :include %s)", + typestr.c_str(), sexp ? sexp : "nil", atts ? atts : "nil"); + + g_free (sexp); + g_free (atts); +} + + +struct SexpData { + GString *gstr; + gboolean personal; + time_t last_seen; + gint64 tstamp; + size_t rank; +}; + + +static void +each_contact_sexp (const char* full_address, + const char *email, const char *name, gboolean personal, + time_t last_seen, unsigned freq, + gint64 tstamp, SexpData *sdata) +{ + sdata->rank++; + + /* since the last time we got some contacts */ + if (sdata->tstamp > tstamp) + return; + + /* (maybe) only include 'personal' contacts */ + if (sdata->personal && !personal) + return; + + /* only include newer-than-x contacts */ + if (sdata->last_seen > last_seen) + return; + + /* only include *real* e-mail addresses (ignore local + * addresses... there's little to complete there anyway...) */ + if (!email || !strstr (email, "@")) + return; + + g_string_append_printf (sdata->gstr, "(%s . %zu)\n", + quoted(full_address).c_str(), sdata->rank); +} + +/** + * get all contacts as an s-expression + * + * @param self contacts object + * @param personal_only whether to restrict the list to 'personal' email + * addresses + * + * @return the sexp + */ +static char* +contacts_to_sexp (const MuContacts *contacts, bool personal, + time_t last_seen, gint64 tstamp) +{ + + g_return_val_if_fail (contacts, NULL); + + SexpData sdata{}; + sdata.personal = personal; + sdata.last_seen = last_seen; + sdata.tstamp = tstamp; + sdata.rank = 0; + + /* make a guess for the initial size */ + sdata.gstr = g_string_sized_new (mu_contacts_count(contacts) * 128); + g_string_append (sdata.gstr, "(:contacts ("); + + const auto cutoff{g_get_monotonic_time()}; + mu_contacts_foreach (contacts, (MuContactsForeachFunc)each_contact_sexp, &sdata); + /* pass a string, elisp doesn't like 64-bit nums */ + g_string_append_printf (sdata.gstr, + ") :tstamp \"%" G_GINT64_FORMAT "\")", cutoff); + + return g_string_free (sdata.gstr, FALSE); +} + + +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 = (time_t)g_ascii_strtoll (afterstr.c_str(), NULL, 10); + const auto tstamp = g_ascii_strtoll (tstampstr.c_str(), NULL, 10); + + const auto contacts{mu_store_contacts(context.store)}; + if (!contacts) + throw Error{Error::Code::Internal, "failed to get contacts"}; + + /* dump the contacts cache as a giant sexp */ + auto sexp = contacts_to_sexp (contacts, personal, after, tstamp); + print_expr ("%s\n", sexp); + g_free (sexp); +} + +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"}; + + print_expr ("(:info save :message %s)", quoted(path + " has been saved").c_str()); +} + + +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"}; + } + + print_expr ("(:info open :message %s)", + quoted(std::string{targetpath} + " has been opened").c_str()); + g_free (targetpath); +} + +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"}; + } + + const auto qpath{quoted(path)}; + g_free(path); + + if (!param.empty()) + print_expr ("(:temp %s" + " :what \"%s\"" + " :docid %u" + " :param %s" + ")", + qpath.c_str(), what.c_str(), docid, quoted(param).c_str()); + else + print_expr ("(:temp %s :what \"%s\" :docid %u)", + qpath.c_str(), what.c_str(), docid); +} + + + +/* '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{mu_store_get_msg (context.store, docid, &gerr)}; + 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 +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 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 (MuStore *store, unsigned docid) +{ + GError *gerr{}; + auto msg{mu_store_get_msg (store, docid, &gerr)}; + if (!msg) + throw Error(Error::Code::Store, &gerr, "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 (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_string_or(params, "sortfield")}; + const auto reverse{get_bool_or(params, "reverse", false)}; + const auto maxnum{get_int_or(params, "maxnum", 500)}; + 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(), FALSE); + if (sort_field == MU_MSG_FIELD_ID_NONE) { + g_warning ("invalid sort field %s", sortfieldstr.c_str()); // XXXX + return; + } + } + + int qflags{MU_QUERY_FLAG_NONE}; + if (reverse) + 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. */ + print_expr ("(:erase t)"); + const auto foundnum{print_sexps (miter, maxnum)}; + 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" + << ";; ( :param1 val1 :param2 val2 ...)\n" + << ";; For instance:\n;; (help :command quit)\n" + << ";; to get information about the 'quit' commmand\n;;\n"; + std::cout << ";; The following commands are available:\n"; + } + + std::vector 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; + + std::cout << ";; " << format("%-10s -- %s\n", name.c_str(), info.docstring.c_str()); + + if (!full) + continue; + + for (auto&& arg: info.args) { + std::cout << ";; " + << format("%-17s : %-20s ", 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 (stats->_processed % 1000) + return MU_OK; + + print_expr ("(:info index :status running " + ":processed %u :updated %u)", + stats->_processed, stats->_updated); + + return MU_OK; +} + +static void +set_my_addresses (MuStore *store, const std::vector& addrs) +{ + if (addrs.empty()) + return; + + const char **addresses = g_new0 (const char*, addrs.size() + 1); + for (auto i = 0U; i != addrs.size(); ++i) + addresses[i] = addrs[i].c_str(); + + mu_store_set_personal_addresses (store, (const char**)addresses); + g_free (addresses); // shallow +} + + +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"}; + } + + print_expr ("(:info index :status complete " + ":processed %u :updated %u :cleaned-up %u)", + stats._processed, stats._updated, stats2._cleaned_up); + + return rv; +} + + +static void +index_handler (Context& context, const Parameters& params) +{ + const auto cleanup{get_bool_or(params, "cleanup")}; + const auto lazy_check{get_bool_or(params, "lazy-check")}; + const auto my_addresses{get_string_vec(params, "my-addresses")}; + + set_my_addresses(context.store, my_addresses); + + GError *gerr{}; + auto index{mu_index_new (context.store, &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); +} + +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"}; + + print_expr ("(:info mkdir :message \"%s has been created\")", path.c_str()); +} + + +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 (MuStore *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 */ + auto rv = mu_store_update_msg (store, docid, msg, &gerr); + if (rv == MU_STORE_INVALID_DOCID) + throw Error{Error::Code::Store, &gerr, "failed to store updated message"}; + + char *sexp = mu_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 */ + print_expr ("(:update %s :move %s :maybe-view %s)", sexp, + different_mdir ? "t" : "nil", + no_view ? "nil" : "t"); + g_free (sexp); +} + +static void +move_docid (MuStore *store, DocId docid, const std::string& flagstr, + bool new_name, bool no_view) +{ + GError *gerr{}; + auto msg{mu_store_get_msg (store, docid, &gerr)}; + if (!msg) + throw Error{Error::Code::Store, &gerr, "failed to get message from store"}; + if (docid == MU_STORE_INVALID_DOCID) + throw Error{Error::Code::InvalidArgument, "invalid docid"}; + + const auto flags = flagstr.empty() ? mu_msg_get_flags (msg) : + get_flags (mu_msg_get_path(msg), flagstr); + if (flags == MU_FLAG_INVALID) { + mu_msg_unref(msg); + throw Error{Error::Code::InvalidArgument, "invalid flags '%s'", flagstr.c_str()}; + } + + try { + do_move (store, docid, msg, "", flags, new_name, no_view); + } catch (...) { + 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 ) + * + */ +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 new_name{get_bool_or (params, "newname")}; + 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, new_name, no_view); + return; + } + auto docid{docids.at(0)}; + + GError *gerr{}; + auto msg{mu_store_get_msg(context.store, docid, &gerr)}; + 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 flagse"}; + } + + try { + do_move (context.store, docid, msg, maildir, flags, + new_name, no_view); + } catch (...) { + mu_msg_unref(msg); + throw; + } + + mu_msg_unref(msg); +} + + + +static void +ping_handler (Context& context, const Parameters& params) +{ + GError *gerr{}; + const auto count = mu_store_count(context.store, &gerr); + if (count == (unsigned)-1) + throw Error{Error::Code::Store, &gerr, "failed to read store"}; + + print_expr ("(:pong \"" PACKAGE_NAME "\" " + ":props (" + ":version \"" VERSION "\" " + ":doccount %u))", count); +} + +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 (!mu_store_remove_path (context.store, path.c_str())) + throw Error(Error::Code::Store, + "failed to remove message @ %s (%d) from store", + path, docid); + + print_expr ("(:remove %u)", docid); +} + + +static void +sent_handler (Context& context, const Parameters& params) +{ + GError *gerr{}; + const auto path{get_string_or(params, "path")}; + const auto docid{mu_store_add_path(context.store, path.c_str(), + get_string_or(params, "maildir").c_str(), + &gerr)}; + if (docid == MU_STORE_INVALID_DOCID) + throw Error{Error::Code::Store, &gerr, "failed to add path"}; + + print_expr ("(:sent t :path %s :docid %u)", quoted(path).c_str(), docid); +} + + +static void +view_handler (Context& context, const Parameters& params) +{ + DocId docid{}; + const auto path{get_string_or(params, "path")}; + + 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 = mu_store_get_msg (context.store, docid, &gerr); + } + + if (!msg) + throw Error{Error::Code::Store, &gerr, "failed to find message for view"}; + + auto sexp{mu_msg_to_sexp(msg, docid, {}, message_options(params))}; + mu_msg_unref(msg); + + print_expr ("(:view %s)\n", sexp); + g_free (sexp); +} + + +static CommandMap +make_command_map (Context& context) +{ + CommandMap cmap; + + cmap.emplace("add", + CommandInfo{ + ArgMap{ {"path", ArgInfo{Type::String, true, "message-path" }}, + {"maildir", ArgInfo{Type::String, true, "maildir" }}}, + "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::Integer, false,"document id of parent-message, if any"}}, + {"extract-images", ArgInfo{Type::Symbol, false, + "whether to extract imagas for this messages (if any)"}}, + {"extract-encrypted", 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::Integer, true, "document for the message" }}, + {"index", ArgInfo{Type::Integer, true, "index for the part to operate on" }}, + {"action", ArgInfo{Type::Symbol, true, "what to do with the part" }}, + {"extract-encrypted", 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" }}, + {"sortdir", ArgInfo{Type::Symbol, false, + "whether to sort in descending order" }}, + {"maxnum", ArgInfo{Type::Integer, false, + "maximum number of result (hint)" }}, + {"skip-dups", ArgInfo{Type::Symbol, false, + "whether to skip messages with duplicate message-idsr" }}, + {"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::Integer, false, "document-id"}}, + {"msgid", ArgInfo{Type::String, false, "message-id"}}, + + {"maildir", ArgInfo{Type::String, false, "the target maildir" }}, + {"flags", ArgInfo{Type::String, false, "new flags for the message"}}, + {"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{{}, + "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::Integer, true, + "document-id for the message to remove" }}}, + "remove a message based 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" }}, + {"maildir", ArgInfo{Type::String, true, + "maildir the messages resides under" }} + }, + "tell mu about a message that was sent", + [&](const auto& params){sent_handler(context, params);}}); + + cmap.emplace("view", + CommandInfo{ + ArgMap{ + {"docid", ArgInfo{Type::Integer, false, "document-id"}}, + {"msgid", ArgInfo{Type::String, false, "message-id"}}, + {"path", ArgInfo{Type::String, false, "message filesystem path"}}, + + {"extract-images", ArgInfo{Type::Symbol, false, + "whether to extract imagas for this messages (if any)"}}, + {"extract-encrypted", ArgInfo{Type::Symbol, false, + "whether to decrypt encrypted parts (if any)" }}}, + "view a message. exactly one of docid/msgid/path must be specified", + [&](const auto& params){view_handler(context, params);}}); + return cmap; +} + + +static std::string +read_line (Context& context) +{ +// if we don't have readline, use the simple version. +#ifndef HAVE_LIBREADLINE + std::cout << ";; mu> "; + std::string line; + if (!std::getline(std::cin, line)) + context.do_quit = true; + return line; +#else /*!HAVE_LIBREADLINE*/ + + auto buf = readline(";; mu% "); + if (!buf) { + context.do_quit = true; + return {}; + } +#ifdef HAVE_READLINE_HISTORY + else if (buf[0]) + add_history(buf); +#endif /*HAVE_LIBREADLINE_HISTORY*/ + + std::string line{buf}; + ::free (buf); + + return line; +#endif /*HAVE_LIBREADLINE*/ +} + + +MuError +mu_cmd_server (MuStore *store, MuConfig *opts/*unused*/, GError **err) try +{ + if (!store && !opts->commands) + throw Error{Error::Code::Internal, "missing store"}; + + Context context{store}; + context.command_map = make_command_map (context); + + if (opts->commands) { + invoke(context.command_map, Sexp::parse("(help :full t)")); + return MU_OK; + } + + std::cout << ";; Welcome to the " << PACKAGE_STRING << " command-server\n" + << ";; Use (help) to get a list of commands, (quit) to quit.\n"; + + while (!context.do_quit) { + + try { + const auto line{read_line(context)}; + if (line.find_first_not_of(" \t") == std::string::npos) + continue; // skip whitespace-only lines + + invoke(context.command_map, Sexp::parse(line)); + + } catch (const Error& er) { + std::cerr << ";; error: " << er.what() << "\n"; + print_error ((MuError)er.code(), "%s", er.what()); + } + } + + return MU_OK; +} catch (const Error& er) { + g_set_error(err, MU_ERROR_DOMAIN, MU_ERROR, "%s", er.what()); + return MU_ERROR; +} catch (...) { + g_set_error(err, MU_ERROR_DOMAIN, MU_ERROR, "%s", "caught exception"); + return MU_ERROR; +} diff --git a/mu/mu-cmd.h b/mu/mu-cmd.h index 699898e1..e61e8315 100644 --- a/mu/mu-cmd.h +++ b/mu/mu-cmd.h @@ -173,6 +173,7 @@ MuError mu_cmd_tickle (MuStore *store, MuConfig *opts, GError **err); * some error code otherwise */ MuError mu_cmd_server (MuStore *store, MuConfig *opts, GError**/*unused*/); +MuError mu_cmd_server2 (MuStore *store, MuConfig *opts, GError**/*unused*/); /** * execute the verify command (to verify signatures) diff --git a/mu/mu-config.c b/mu/mu-config.c index 696366bd..e5244387 100644 --- a/mu/mu-config.c +++ b/mu/mu-config.c @@ -441,6 +441,8 @@ config_options_group_server (void) GOptionEntry entries[] = { {"maildir", 'm', 0, G_OPTION_ARG_FILENAME, &MU_CONFIG.maildir, "top of the maildir", ""}, + {"list-commands", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.list_commands, + "overwrite existing files (false)", NULL}, {NULL, 0, 0, 0, NULL, NULL, NULL} }; diff --git a/mu/mu-config.h b/mu/mu-config.h index 26fcf8da..3d4f2814 100644 --- a/mu/mu-config.h +++ b/mu/mu-config.h @@ -1,7 +1,5 @@ -/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ - /* -** Copyright (C) 2008-2017 Dirk-Jan C. Binnema +** Copyright (C) 2008-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 @@ -180,6 +178,10 @@ struct _MuConfig { gboolean overwrite; /* should we overwrite same-named files */ gboolean play; /* after saving, try to 'play' * (open) the attmnt using xdgopen */ + /* for server */ + gboolean list_commands; /* dump documentations for server + * commands */ + /* options for mu-script */ gchar *script; /* script to run */ const char **script_params; /* parameters for scripts */