/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ /* ** Copyright (C) 2011-2012 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*/ #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-str.h" #include "mu-cmd.h" #include "mu-maildir.h" #include "mu-query.h" #include "mu-index.h" #include "mu-msg-part.h" static gboolean MU_CAUGHT_SIGNAL; static void sig_handler (int sig) { MU_CAUGHT_SIGNAL = TRUE; } static void install_sig_handler (void) { struct sigaction action; int i, sigs[] = { SIGINT, SIGHUP, SIGTERM }; MU_CAUGHT_SIGNAL = 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));; } /* BOX - beginning-of-expression */ #define BOX "\376" static void send_expr (const char* frm, ...) G_GNUC_PRINTF(1, 2); static void send_expr (const char* frm, ...) { char *expr; va_list ap; char hdr[16]; size_t exprlen, hdrlen; va_start (ap, frm); expr = NULL; exprlen = g_vasprintf (&expr, frm, ap); hdrlen = snprintf (hdr, sizeof(hdr), BOX "%u" BOX, (unsigned)exprlen); if (write (fileno(stdout), hdr, hdrlen) < 0) MU_WRITE_LOG ("error writing output: %s", strerror(errno)); if (write (fileno(stdout), expr, exprlen) < 0) MU_WRITE_LOG ("error writing output: %s", strerror(errno)); g_free (expr); va_end (ap); } static MuError server_error (GError **err, MuError merr, const char* frm, ...) G_GNUC_PRINTF(3, 4); static MuError server_error (GError **err, MuError merr, const char* frm, ...) { gboolean has_err; char *errmsg; va_list ap; va_start (ap, frm); errmsg = g_strdup_vprintf (frm, ap); has_err = err && *err; send_expr ("(:error %u :error-message \"%s\")\n", has_err ? (unsigned)(*err)->code : merr, has_err ? (*err)->message : errmsg); g_free (errmsg); va_end (ap); return has_err ? (unsigned)(*err)->code : merr; } #define MU_PROMPT ";; mu> " static gchar* my_readline (const char *prompt) { GString *gstr; int kar; gstr = g_string_sized_new (512); fputs (prompt, stdout); do { kar = fgetc (stdin); if (kar == '\n' || kar == EOF) break; else gstr = g_string_append_c (gstr, (char)kar); } while (1); return g_string_free (gstr, FALSE); } enum _Cmd { CMD_ADD, CMD_COMPOSE, CMD_FIND, CMD_FLAG, CMD_HELP, CMD_INDEX, CMD_MKDIR, CMD_MOVE, CMD_OPEN, CMD_QUIT, CMD_REMOVE, CMD_SAVE, CMD_PING, CMD_VIEW, CMD_IGNORE }; typedef enum _Cmd Cmd; static const Cmd CMD_INVALID = (Cmd)-1; static Cmd cmd_from_string (const char *str) { unsigned u; struct { Cmd cmd; const char *name; } commands[] = { { CMD_ADD, "add"}, { CMD_COMPOSE, "compose"}, { CMD_FIND, "find" }, { CMD_FLAG, "flag"}, { CMD_INDEX, "index"}, { CMD_MKDIR, "mkdir"}, { CMD_MOVE, "move"}, { CMD_OPEN, "open" }, { CMD_QUIT, "quit"}, { CMD_REMOVE, "remove" }, { CMD_SAVE, "save"}, { CMD_PING, "ping"}, { CMD_VIEW, "view"} }; for (u = 0; u != G_N_ELEMENTS(commands); ++u) if (g_strcmp0 (str, commands[u].name) == 0) return commands[u].cmd; /* handle all-blank strings */ /* if (g_regex_match_simple ("^ *$", str, 0, 0)) */ /* return CMD_IGNORE; */ return CMD_INVALID; } static Cmd parse_line (const gchar *line, GSList **args) { Cmd cmd; GSList *lst; *args = NULL; if (!line) return CMD_IGNORE; lst = mu_str_esc_to_list (line); if (!lst) return CMD_INVALID; cmd = cmd_from_string ((const char*)lst->data); if (cmd == CMD_INVALID) { mu_str_free_list (lst); *args = NULL; } else { *args = g_slist_next (lst); lst->next = NULL; mu_str_free_list (lst); } return cmd; } /* * 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)); mu_msg_unref (msg); return path; } static gboolean check_param_num (GSList *args, unsigned min, unsigned max) { unsigned len; len = g_slist_length (args); return len >= min && len <= max; } #define return_if_fail_param_num(ARGS,MN,MX,USAGE) \ do { \ if (!check_param_num((ARGS),(MN),(MX))) \ return server_error(NULL,MU_ERROR_IN_PARAMETERS, \ "%s",(USAGE)); \ } while (0) /* -> ping * <- (:pong mu :version :doccount ) */ static MuError cmd_ping (MuStore *store, GSList *args, GError **err) { return_if_fail_param_num (args, 0, 0, "usage: version"); send_expr ("(:pong \"" PACKAGE_NAME "\" " ":version \"" VERSION "\" " ":doccount %u" ")\n", mu_store_count (store, err)); return MU_OK; } static unsigned output_found_sexps (MuMsgIter *iter, int maxnum) { unsigned u, max; u = 0; if (maxnum > 0) max = (unsigned) maxnum; else max = G_MAXUINT32; while (!mu_msg_iter_is_done (iter) && u <= max && !MU_CAUGHT_SIGNAL) { MuMsg *msg; msg = mu_msg_iter_get_msg_floating (iter); if (mu_msg_is_readable (msg)) { char *sexp; sexp = mu_msg_to_sexp (msg, mu_msg_iter_get_docid (iter), mu_msg_iter_get_thread_info (iter), TRUE); send_expr ("%s", sexp); g_free (sexp); ++u; } mu_msg_iter_next (iter); } return u; } /* * find * => list of s-expression, each describing a message * => (:found ) */ static MuError cmd_find (MuStore *store, MuQuery *query, GSList *args, GError **err) { MuMsgIter *iter; int maxnum; const char* usage; usage = "usage: find "; return_if_fail_param_num (args, 2, 2, usage); if ((maxnum = atoi((const char*)args->next->data)) == 0) return server_error (NULL, MU_ERROR_IN_PARAMETERS, "%s", usage); /* TODO: ask for *all* results, then, get the newest * ones; it seems we cannot get a sorted list of a subset of * the result --> needs investigation, this is a * work-around */ iter = mu_query_run (query, (const char*)args->data, TRUE, MU_MSG_FIELD_ID_DATE, TRUE, -1, err); if (!iter) return server_error (err, MU_ERROR_INTERNAL, "couldn't get iterator"); /* return results + the number of results found */ send_expr ("(:found %u)\n", output_found_sexps (iter, maxnum)); mu_msg_iter_destroy (iter); return MU_OK; } static MuError cmd_mkdir (GSList *args, GError **err) { const char *path; return_if_fail_param_num (args, 1, 1, "usage: mkdir "); path = (const char*)args->data; if (!mu_maildir_mkdir (path, 0755, FALSE, err)) return server_error (err, MU_G_ERROR_CODE (err), "failed to create maildir '%s'", path); send_expr ("(:info mkdir :message \"%s has been created\")", path); return MU_OK; } static unsigned get_docid_from_msgid (MuQuery *query, const char *str, GError **err) { gchar *querystr; unsigned docid; MuMsgIter *iter; querystr = g_strdup_printf ("msgid:%s", str); iter = mu_query_run (query, querystr, FALSE, MU_MSG_FIELD_ID_NONE, FALSE, 1, err); g_free (querystr); docid = MU_STORE_INVALID_DOCID; if (!iter || mu_msg_iter_is_done (iter)) if (err && *err == NULL) g_set_error (err, 0, MU_ERROR_NO_MATCHES, "could not find message %s", str); else return docid; else { MuMsg *msg; msg = mu_msg_iter_get_msg_floating (iter); if (!mu_msg_is_readable(msg)) { g_set_error (err, 0, MU_ERROR_FILE_CANNOT_READ, "'%s' is not readable", mu_msg_get_path(msg)); } else docid = mu_msg_iter_get_docid (iter); mu_msg_iter_destroy (iter); } return docid; } /* 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 get_docid (MuQuery *query, const char *str, GError **err) { unsigned docid; char *endptr; docid = strtol (str, &endptr, 10); if (*endptr != '\0') { if (!query) { g_set_error (err, 0, MU_ERROR_IN_PARAMETERS, "invalid docid '%s'", str); return MU_STORE_INVALID_DOCID; } else return get_docid_from_msgid (query, str, err); } return docid; } 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); } } static MuError do_move (MuStore *store, unsigned docid, MuMsg *msg, const char *maildir, MuFlags flags, gboolean is_move, GError **err) { gchar *sexp; unsigned rv; if (!mu_msg_move_to_maildir (msg, maildir, flags, TRUE, err)) return MU_G_ERROR_CODE (err); /* note, 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) return server_error (err, MU_ERROR_XAPIAN, "%s", "failed to update message"); sexp = mu_msg_to_sexp (msg, docid, NULL, TRUE); send_expr ("(:update %s :move %s)", sexp, is_move ? "t" : "nil"); g_free (sexp); return MU_OK; } static MuError move_or_flag (MuStore *store, MuQuery *query, GSList *args, gboolean is_move, GError **err) { MuError merr; unsigned docid; MuMsg *msg; MuFlags flags; GSList *flagitem; const char *mdir; if ((docid = get_docid (query, (const char*)args->data, err)) == 0) return server_error (err, MU_ERROR_IN_PARAMETERS, "invalid docid '%s'", (char*)args->data); if (!(msg = mu_store_get_msg (store, docid, err))) return server_error (err, MU_ERROR, "failed to get message"); if (is_move) { mdir = (const char*)g_slist_nth (args, 1)->data; flagitem = g_slist_nth (args, 2); } else { /* flag */ mdir = mu_msg_get_maildir (msg); flagitem = g_slist_nth (args, 1); } flags = get_flags (mu_msg_get_path(msg), flagitem ? (gchar*)flagitem->data : NULL); if (flags == MU_FLAG_INVALID) { mu_msg_unref (msg); return server_error (err, MU_ERROR_IN_PARAMETERS, "invalid flags"); } merr = do_move (store, docid, msg, mdir, flags, is_move, err); mu_msg_unref (msg); return (merr == MU_OK) ? MU_OK : server_error (err, merr, "error moving/flagging file"); } static MuError cmd_move (MuStore *store, GSList *args, GError **err) { return_if_fail_param_num (args, 2, 3, "usage: move []"); return move_or_flag (store, NULL, args, TRUE, err); } static MuError cmd_flag (MuStore *store, MuQuery *query, GSList *args, GError **err) { return_if_fail_param_num (args, 2, 2, "usage: flag | "); return move_or_flag (store, query, args, FALSE, err); } static MuError cmd_remove (MuStore *store, GSList *args, GError **err) { unsigned docid; const char *path; return_if_fail_param_num (args, 1, 1, "usage: remove "); docid = get_docid (NULL, (const char*)args->data, err); if (docid == MU_STORE_INVALID_DOCID) return server_error (err, MU_ERROR_IN_PARAMETERS, "invalid docid"); path = get_path_from_docid (store, docid, err); if (!path) return server_error (err, MU_ERROR_IN_PARAMETERS, "no valid doc for docid"); if (unlink (path) != 0) return server_error (err, MU_ERROR_FILE_CANNOT_UNLINK, "%s", strerror (errno)); if (!mu_store_remove_path (store, path)) return server_error (NULL, MU_ERROR_XAPIAN_REMOVE_FAILED, "failed to remove from database"); send_expr ("(:remove %u)", docid); return MU_OK; } static MuError save_part (MuMsg *msg, const char* targetpath, unsigned partindex, GError **err) { gboolean rv; gchar *path; rv = mu_msg_part_save (msg, targetpath, partindex, TRUE/*overwrite*/, FALSE/*use cache*/, err); if (!rv) return server_error (err, MU_ERROR_FILE, "failed to save to target path"); path = mu_str_escape_c_literal (targetpath, FALSE); send_expr ("(:info save :message \"%s has been saved\")", path); g_free (path); return MU_OK; } static MuError open_part (MuMsg *msg, unsigned partindex, GError **err) { char *targetpath; gboolean rv; targetpath = mu_msg_part_filepath_cache (msg, partindex); rv = mu_msg_part_save (msg, targetpath, partindex, FALSE/*overwrite*/, TRUE/*use cache*/, err); if (!rv) { g_free (targetpath); return server_error (err, MU_ERROR_FILE, "failed to save"); } rv = mu_util_play (targetpath, TRUE/*allow local*/, FALSE/*allow remote*/); if (!rv) { g_free (targetpath); return server_error (err, MU_ERROR_FILE, "failed to open"); } else { gchar *path; path = mu_str_escape_c_literal (targetpath, FALSE); send_expr ("(:info open :message \"%s has been opened\")", path); g_free (path); } g_free (targetpath); return MU_OK; } static MuError save_or_open (MuStore *store, GSList *args, gboolean is_save, GError **err) { MuMsg *msg; unsigned docid, partindex; gboolean rv; docid = get_docid (NULL, (const char*)args->data, err); if (docid == MU_STORE_INVALID_DOCID) return server_error (err, MU_ERROR_IN_PARAMETERS, "invalid docid"); msg = mu_store_get_msg (store, docid, err); if (!msg) return server_error (err, MU_ERROR, "failed to get message"); partindex = atoi((const char*)g_slist_nth (args, 1)->data); if (is_save) { const char *targetpath; targetpath = ((const char*)g_slist_nth (args, 2)->data); rv = save_part (msg, targetpath, partindex, err); } else rv = open_part (msg, partindex, err); mu_msg_unref (msg); return rv; } static MuError cmd_save (MuStore *store, GSList *args, GError **err) { return_if_fail_param_num (args, 3, 3, "save "); return save_or_open (store, args, TRUE, err); } static MuError cmd_open (MuStore *store, GSList *args, GError **err) { return_if_fail_param_num (args, 2, 2, "open "); return save_or_open (store, args, FALSE, err); } static MuError cmd_view (MuStore *store, MuQuery *query, GSList *args, GError **err) { MuMsg *msg; unsigned docid; char *sexp; return_if_fail_param_num (args, 1, 1, "view |"); docid = get_docid (query, (const char*)args->data, err); if (docid == MU_STORE_INVALID_DOCID) return server_error (err, MU_ERROR_IN_PARAMETERS, "invalid docid"); msg = mu_store_get_msg (store, docid, err); if (!msg) return server_error (err, MU_ERROR, "failed to get message"); sexp = mu_msg_to_sexp (msg, docid, NULL, FALSE); mu_msg_unref (msg); send_expr ("(:view %s)\n", sexp); g_free (sexp); return MU_OK; } static void each_part (MuMsg *msg, MuMsgPart *part, GSList **attlist) { char *att, *cachefile; GError *err; /* exclude things that don't look like proper attachments */ if (!mu_msg_part_looks_like_attachment(part, TRUE)) return; err = NULL; cachefile = mu_msg_part_save_temp (msg, part->index, &err); if (!cachefile) { server_error (&err, MU_ERROR_FILE, "could not save %s", cachefile); goto leave; } att = g_strdup_printf ( "(:file-name \"%s\" :mime-type \"%s/%s\" :disposition \"%s\")", cachefile, part->type, part->subtype, part->disposition ? part->disposition : "attachment"); *attlist = g_slist_append (*attlist, att); leave: g_clear_error (&err); 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) { GSList *attlist, *cur; GString *gstr; attlist = NULL; mu_msg_part_foreach (msg, FALSE, (MuMsgPartForeachFunc)each_part, &attlist); gstr = g_string_sized_new (512); gstr = g_string_append_c (gstr, '('); for (cur = attlist; cur; cur = g_slist_next (cur)) g_string_append (gstr, (gchar*)cur->data); gstr = g_string_append_c (gstr, ')'); mu_str_free_list (attlist); return g_string_free (gstr, FALSE); } static MuError cmd_compose (MuStore *store, GSList *args, GError **err) { MuMsg *msg; unsigned docid; char *sexp, *atts; const char* ctype; return_if_fail_param_num (args, 2, 2, "compose "); ctype = (const char*)args->data; if (strcmp (ctype, "reply") != 0 && strcmp(ctype, "forward") != 0 && strcmp (ctype, "edit") != 0) return server_error (NULL, MU_ERROR_IN_PARAMETERS, "compose "); docid = get_docid (NULL, (const char*)g_slist_nth(args, 1)->data, err); if (docid == MU_STORE_INVALID_DOCID) return server_error (err, MU_ERROR_IN_PARAMETERS, "invalid docid"); msg = mu_store_get_msg (store, docid, err); if (!msg) return server_error (err, MU_ERROR, "failed to get message"); sexp = mu_msg_to_sexp (msg, docid, NULL, FALSE); atts = (!strcmp(ctype, "forward")) ? include_attachments (msg) : NULL; mu_msg_unref (msg); send_expr ("(:compose-type %s :original %s :include %s)", ctype, sexp, atts ? atts : "()"); g_free (sexp); g_free (atts); return MU_OK; } static MuError index_msg_cb (MuIndexStats *stats, void *user_data) { if (MU_CAUGHT_SIGNAL) return MU_STOP; if (stats->_processed % 500) return MU_OK; send_expr ("(:info index :status running " ":processed %u :updated %u)", stats->_processed, stats->_updated); return MU_OK; } static MuError cmd_add (MuStore *store, GSList *args, GError **err) { unsigned docid; const char *path, *maildir; gchar *escpath; return_if_fail_param_num (args, 2, 2, "usage: add "); path = (const char*)args->data; maildir = (const char*)g_slist_nth (args, 1)->data; docid = mu_store_add_path (store, path, maildir, err); if (docid == MU_STORE_INVALID_DOCID) return server_error (err, MU_ERROR_XAPIAN, "failed to add path '%s'", path); escpath = mu_str_escape_c_literal (path, TRUE); send_expr ("(:info add :path %s :docid %u)", escpath, docid); g_free (escpath); return MU_OK; } static MuError cmd_index (MuStore *store, GSList *args, GError **err) { MuIndex *index; const char *maildir; MuIndexStats stats, stats2; MuError rv; return_if_fail_param_num (args, 1, 1, "usage: index "); index = mu_index_new (store, err); if (!index) return server_error (err, MU_ERROR, "failed to create index object"); mu_index_stats_clear (&stats); maildir = (const char*)args->data; rv = mu_index_run (index, maildir, FALSE, &stats, index_msg_cb, NULL, NULL); if (rv != MU_OK && rv != MU_STOP) { mu_index_destroy (index); return server_error (NULL, rv, "indexing failed"); } mu_index_stats_clear (&stats2); rv = mu_index_cleanup (index, &stats2, NULL, NULL, err); mu_index_destroy (index); if (rv != MU_OK && rv != MU_STOP) return server_error (err, rv, "cleanup failed"); mu_store_flush (store); send_expr ("(:info index :status complete " ":processed %u :updated %u :cleaned-up %u)", stats._processed, stats._updated, stats2._cleaned_up); return MU_OK; } static MuError cmd_quit (GSList *args, GError **err) { return_if_fail_param_num (args, 0, 0, "usage: quit"); send_expr (";; quiting\n"); return MU_OK; } static gboolean handle_command (Cmd cmd, MuStore *store, MuQuery *query, GSList *args, GError **err) { MuError rv; switch (cmd) { case CMD_ADD: rv = cmd_add (store, args, err); break; case CMD_COMPOSE: rv = cmd_compose (store, args, err); break; case CMD_FIND: rv = cmd_find (store, query, args, err); break; case CMD_FLAG: rv = cmd_flag (store, query, args, err); break; case CMD_INDEX: rv = cmd_index (store, args, err); break; case CMD_MKDIR: rv = cmd_mkdir (args, err); break; case CMD_MOVE: rv = cmd_move (store, args, err); break; case CMD_OPEN: rv = cmd_open (store, args, err); break; case CMD_QUIT: rv = cmd_quit (args, err); break; case CMD_REMOVE: rv = cmd_remove (store, args, err); break; case CMD_SAVE: rv = cmd_save (store, args, err); break; case CMD_PING: rv = cmd_ping (store, args, err); break; case CMD_VIEW: rv = cmd_view (store, query, args, err); break; case CMD_IGNORE: return TRUE; default: g_set_error (err, 0, MU_ERROR_IN_PARAMETERS, "unknown command"); return FALSE; } return rv == MU_OK ? TRUE : FALSE; } MuError mu_cmd_server (MuStore *store, MuConfig *opts, GError **err) { MuQuery *query; g_return_val_if_fail (store, MU_ERROR_INTERNAL); install_sig_handler (); fputs (";; welcome to mu\n", stdout); fputs (";; type your commands, and press Enter to execute them\n", stdout); query = mu_query_new (store, err); if (!query) return MU_G_ERROR_CODE (err); while (!MU_CAUGHT_SIGNAL) { char *line; Cmd cmd; GSList *args; GError *my_err; line = my_readline (MU_PROMPT); cmd = parse_line (line, &args); g_free (line); my_err = NULL; if (!handle_command (cmd, store, query, args, &my_err)) g_clear_error (&my_err); mu_str_free_list (args); if (cmd == CMD_QUIT) { mu_store_flush (store); break; } } mu_query_destroy (query); return MU_OK; }