diff options
-rw-r--r-- | mh/mhn.c | 825 |
1 files changed, 749 insertions, 76 deletions
@@ -18,6 +18,9 @@ /* MH mhn command */ #include <mh.h> +#define obstack_chunk_alloc xmalloc +#define obstack_chunk_free free +#include <obstack.h> const char *argp_program_version = "mhn (" PACKAGE_STRING ")"; static char doc[] = N_("GNU MH mhn\v" @@ -57,15 +60,15 @@ static struct argp_option options[] = { {"form", ARG_FORM, N_("FILE"), 0, N_("Read mhl format from FILE"), 22}, {"pause", ARG_PAUSE, N_("BOOL"), OPTION_ARG_OPTIONAL, - N_("* Pause prior to displaying content"), 22}, + N_("Pause prior to displaying content"), 22}, {"nopause", ARG_NOPAUSE, NULL, OPTION_HIDDEN, "", 22 }, {N_("Saving options"), 0, NULL, OPTION_DOC, NULL, 30}, {"store", ARG_STORE, N_("BOOL"), OPTION_ARG_OPTIONAL, - N_("* Store the contents of the messages on disk"), 31}, + N_("Store the contents of the messages on disk"), 31}, {"nostore", ARG_NOSTORE, NULL, OPTION_HIDDEN, "", 31 }, {"auto", ARG_AUTO, N_("BOOL"), OPTION_ARG_OPTIONAL, - N_("* Use filenames from the content headers"), 31}, + N_("Use filenames from the content headers"), 31}, {"noauto", ARG_NOAUTO, NULL, OPTION_HIDDEN, "", 31 }, {N_("Other options"), 0, NULL, OPTION_DOC, NULL, 40}, @@ -76,6 +79,8 @@ static struct argp_option options[] = { {"verbose", ARG_VERBOSE, N_("BOOL"), OPTION_ARG_OPTIONAL, N_("Print additional information"), 41 }, {"noverbose", ARG_NOVERBOSE, NULL, OPTION_HIDDEN, "", 41 }, + {"quiet", ARG_QUIET, 0, 0, + N_("Be quiet")}, {NULL} }; @@ -125,6 +130,7 @@ static enum mhn_mode mode = mode_compose; #define OPT_AUTO 010 #define OPT_SERIALONLY 020 #define OPT_VERBOSE 040 +#define OPT_QUIET 100 static int mode_options = OPT_HEADERS; static char *formfile; @@ -138,6 +144,12 @@ static mailbox_t mbox; static message_t message; static msg_part_t req_part; +/* Show flags */ +#define MHN_EXCLUSIVE_EXEC 001 +#define MHN_STDIN 002 +#define MHN_LISTING 004 +#define MHN_CONFIRM 010 + void sfree (char **ptr) { @@ -170,6 +182,60 @@ split_content (const char *content, char **type, char **subtype) } } +int +_get_hdr_value (header_t hdr, const char *name, char **value) +{ + int status = header_aget_value (hdr, name, value); + if (status == 0) + { + /* Remove the newlines. */ + char *nl; + while ((nl = strchr (*value, '\n')) != NULL) + *nl = ' '; + } + return status; +} + +int +_get_content_type (header_t hdr, char **value, char **rest) +{ + char *type = NULL; + _get_hdr_value (hdr, MU_HEADER_CONTENT_TYPE, &type); + if (type == NULL || *type == '\0') + { + if (type) + free (type); + type = strdup ("text/plain"); /* Default. */ + if (rest) + *rest = NULL; + } + else + { + char *p = strchr (type, ';'); + if (p) + *p++ = 0; + if (rest) + *rest = p; + } + *value = type; + return 0; +} + +static int +_get_content_encoding (header_t hdr, char **value) +{ + char *encoding = NULL; + _get_hdr_value (hdr, MU_HEADER_CONTENT_TRANSFER_ENCODING, &encoding); + if (encoding == NULL || *encoding == '\0') + { + if (encoding) + free (encoding); + encoding = strdup ("7bit"); /* Default. */ + } + *value = encoding; + return 0; +} + static int opt_handler (int key, char *arg, void *unused, struct argp_state *state) { @@ -316,7 +382,11 @@ opt_handler (int key, char *arg, void *unused, struct argp_state *state) case ARG_PART: req_part = msg_part_parse (arg); break; - + + case ARG_QUIET: + mode_options |= OPT_QUIET; + break; + default: return 1; } @@ -405,6 +475,50 @@ msg_part_print (msg_part_t p, int max_width) putchar (' '); } +char * +msg_part_format (msg_part_t p) +{ + int i; + int width = 0; + char *str, *s; + char buf[64]; + + for (i = 1; i <= p->level; i++) + { + if (i > 1) + width++; + width += snprintf (buf, sizeof buf, "%lu", (unsigned long) p->part[i]); + } + + str = s = xmalloc (width + 1); + for (i = 1; i <= p->level; i++) + { + if (i > 1) + *s++ = '.'; + s += sprintf (s, "%lu", (unsigned long) p->part[i]); + } + *s = 0; + return str; +} + +void +msg_part_format_stk (struct obstack *stk, msg_part_t p) +{ + int i; + char buf[64]; + + for (i = 1; i <= p->level; i++) + { + int len; + + if (i > 1) + obstack_1grow (stk, '.'); + + len = snprintf (buf, sizeof buf, "%lu", (unsigned long) p->part[i]); + obstack_grow (stk, buf, len); + } +} + msg_part_t msg_part_parse (char *str) { @@ -449,58 +563,239 @@ msg_part_subpart (msg_part_t p, int level) } -/* ************************** Message iterators *************************** */ +/* *********************** Context file accessors ************************* */ -int -_get_hdr_value (header_t hdr, const char *name, char **value) +char * +_mhn_profile_get (char *prefix, char *type, char *subtype, char *defval) { - int status = header_aget_value (hdr, name, value); - if (status == 0) + char *str, *name; + + if (subtype) { - /* Remove the newlines. */ - char *nl; - while ((nl = strchr (*value, '\n')) != NULL) - *nl = ' '; + asprintf (&name, "mhn-%s-%s/%s", prefix, type, subtype); + str = mh_global_profile_get (name, NULL); + free (name); + if (!str) + return _mhn_profile_get (prefix, type, NULL, defval); } - return status; + else + { + asprintf (&name, "mhn-%s-%s", prefix, type); + str = mh_global_profile_get (name, defval); + free (name); + } + return str; } -int -_get_content_type (header_t hdr, char **value) +char * +mhn_show_command (message_t msg, msg_part_t part, int *flags, char **tempfile) { - char *type = NULL; - _get_hdr_value (hdr, MU_HEADER_CONTENT_TYPE, &type); - if (type == NULL || *type == '\0') + char *p, *str, *tmp; + char *typestr, *type, *subtype, *typeargs; + struct obstack stk; + header_t hdr; + + message_get_header (msg, &hdr); + _get_content_type (hdr, &typestr, &typeargs); + split_content (typestr, &type, &subtype); + str = _mhn_profile_get ("show", type, subtype, NULL); + if (!str) /* FIXME */ + return NULL; + + /* Expand macro-notations: + %a additional arguments + %e exclusive execution + %f filename containing content + %F %e, %f, and stdin is terminal not content + %l display listing prior to displaying content + %p %l, and ask for confirmation + %s subtype + %d content description */ + + obstack_init (&stk); + for (p = str; *p && isspace (*p); p++) + ; + + if (*p == '|') + p++; + + for ( ; *p; p++) { - if (type) - free (type); - type = strdup ("text/plain"); /* Default. */ + if (*p == '%') + { + switch (*++p) + { + case 'a': + /* additional arguments */ + obstack_grow (&stk, typeargs, strlen (typeargs)); + break; + + case 'e': + /* exclusive execution */ + *flags |= MHN_EXCLUSIVE_EXEC; + break; + + case 'f': + /* filename containing content */ + if (!*tempfile) + *tempfile = mu_tempname (NULL); + obstack_grow (&stk, *tempfile, strlen (*tempfile)); + break; + + case 'F': + /* %e, %f, and stdin is terminal not content */ + *flags |= MHN_STDIN|MHN_EXCLUSIVE_EXEC; + if (!*tempfile) + *tempfile = mu_tempname (NULL); + obstack_grow (&stk, *tempfile, strlen (*tempfile)); + break; + + case 'l': + /* display listing prior to displaying content */ + *flags |= MHN_LISTING; + break; + + case 'p': + /* %l, and ask for confirmation */ + *flags |= MHN_LISTING|MHN_CONFIRM; + break; + + case 's': + /* subtype */ + obstack_grow (&stk, subtype, strlen (subtype)); + break; + + case 'd': + /* content description */ + if (header_aget_value (hdr, MU_HEADER_CONTENT_DESCRIPTION, + &tmp) == 0) + { + obstack_grow (&stk, tmp, strlen (tmp)); + free (tmp); + } + break; + + default: + obstack_1grow (&stk, *p); + p++; + } + } + else + obstack_1grow (&stk, *p); } + obstack_1grow (&stk, 0); + + free (typestr); + free (type); + free (subtype); + + str = obstack_finish (&stk); + for (p = str; *p && isspace (*p); p++) + ; + if (!*p) + str = NULL; else - { - char *p = strchr (type, ';'); - if (p) - *p = 0; - } - *value = type; - return 0; + str = strdup (str); + + obstack_free (&stk, NULL); + return str; } -static int -_get_content_encoding (header_t hdr, char **value) +char * +mhn_store_command (message_t msg, msg_part_t part, char *name) { - char *encoding = NULL; - _get_hdr_value (hdr, MU_HEADER_CONTENT_TRANSFER_ENCODING, &encoding); - if (encoding == NULL || *encoding == '\0') + char *p, *str, *tmp; + char *typestr, *type, *subtype, *typeargs; + struct obstack stk; + header_t hdr; + char buf[64]; + + message_get_header (msg, &hdr); + _get_content_type (hdr, &typestr, &typeargs); + split_content (typestr, &type, &subtype); + str = _mhn_profile_get ("show", type, subtype, "%m%P.%s"); + + /* Expand macro-notations: + %m message number + %P .part + %p part + %s subtype */ + + obstack_init (&stk); + for (p = str; *p; p++) { - if (encoding) - free (encoding); - encoding = strdup ("7bit"); /* Default. */ + if (*p == '%') + { + switch (*++p) + { + case 'a': + /* additional arguments */ + obstack_grow (&stk, typeargs, strlen (typeargs)); + break; + + case 'm': + if (name) + obstack_grow (&stk, name, strlen (name)); + else + { + snprintf (buf, sizeof buf, "%lu", + (unsigned long) msg_part_subpart (part, 0)); + obstack_grow (&stk, buf, strlen (buf)); + } + break; + + case 'P': + obstack_1grow (&stk, '.'); + /*FALLTHRU*/ + case 'p': + msg_part_format_stk (&stk, part); + break; + + case 's': + /* subtype */ + obstack_grow (&stk, subtype, strlen (subtype)); + break; + + case 'd': + /* content description */ + if (header_aget_value (hdr, MU_HEADER_CONTENT_DESCRIPTION, + &tmp) == 0) + { + obstack_grow (&stk, tmp, strlen (tmp)); + free (tmp); + } + break; + + default: + obstack_1grow (&stk, *p); + p++; + } + } + else + obstack_1grow (&stk, *p); } - *value = encoding; - return 0; + obstack_1grow (&stk, *p); + + free (typestr); + free (type); + free (subtype); + + str = obstack_finish (&stk); + for (p = str; *p && isspace (*p); p++) + ; + if (!*p) + str = NULL; + else + str = strdup (str); + + obstack_free (&stk, NULL); + return str; } + +/* ************************** Message iterators *************************** */ + + typedef int (*msg_handler_t) __PMT((message_t msg, msg_part_t part, char *type, char *encoding, void *data)); @@ -518,8 +813,11 @@ match_content (char *content) split_content (content, &type, &subtype); - if (strcasecmp (content_type, type) == 0) - rc = strcasecmp (content_subtype, subtype); + if ((rc = strcasecmp (content_type, type)) == 0) + { + if (content_subtype) + rc = strcasecmp (content_subtype, subtype); + } else rc = strcasecmp (content_type, subtype); @@ -548,7 +846,7 @@ handle_message (message_t msg, msg_part_t part, msg_handler_t fun, void *data) int ismime = 0; message_get_header (msg, &hdr); - _get_content_type (hdr, &type); + _get_content_type (hdr, &type, NULL); _get_content_encoding (hdr, &encoding); fun (msg, part, type, encoding, data); @@ -570,12 +868,12 @@ handle_message (message_t msg, msg_part_t part, msg_handler_t fun, void *data) if (message_get_part (msg, i, &message) == 0) { message_get_header (message, &hdr); - _get_content_type (hdr, &type); + _get_content_type (hdr, &type, NULL); _get_content_encoding (hdr, &encoding); msg_part_set_subpart (part, i); - if (strcasecmp (type, "multipart/mixed") == 0) + if (message_is_multipart (message, &ismime) == 0 && ismime) handle_message (message, part, fun, data); else call_handler (message, part, type, encoding, fun, data); @@ -641,18 +939,23 @@ list_handler (message_t msg, msg_part_t part, char *type, char *encoding, header_t hdr; if (msg_part_level (part) == 0) - printf ("%3lu ", (unsigned long) msg_part_subpart (part, 0)); + printf ("%4lu ", (unsigned long) msg_part_subpart (part, 0)); else { - printf (" "); - msg_part_print (part, 5); + printf (" "); + msg_part_print (part, 4); putchar (' '); } - printf ("%-26s", type); + printf ("%-25s", type); mhn_message_size (msg, &size); - printf ("%4lu", (unsigned long) size); - + if (size < 1024) + printf (" %4lu", (unsigned long) size); + else if (size < 1024*1024) + printf ("%4luK", (unsigned long) size / 1024); + else + printf ("%4luM", (unsigned long) size / 1024 / 1024); + if (message_get_header (msg, &hdr) == 0) { char *descr; @@ -670,7 +973,11 @@ list_handler (message_t msg, msg_part_t part, char *type, char *encoding, int list_message (message_t msg, size_t num) { - msg_part_t part = msg_part_create (num); + size_t uid; + msg_part_t part; + + mh_message_number (msg, &uid); + part = msg_part_create (uid); handle_message (msg, part, list_handler, NULL); msg_part_destroy (part); return 0; @@ -688,7 +995,7 @@ mhn_list () int rc; if (mode_options & OPT_HEADERS) - printf (_("msg part type/subtype size description\n")); + printf (_(" msg part type/subtype size description\n")); if (message) rc = list_message (message, 0); @@ -717,43 +1024,189 @@ cat_message (stream_t out, stream_t in) } int +show_internal (message_t msg, msg_part_t part, char *encoding, stream_t out) +{ + int rc; + body_t body = NULL; + stream_t dstr, bstr; + + if ((rc = message_get_body (msg, &body))) + { + mh_error (_("%lu: can't get message body: %s"), + (unsigned long) msg_part_subpart (part, 0), + mu_strerror (rc)); + return 0; + } + body_get_stream (body, &bstr); + rc = filter_create(&dstr, bstr, encoding, + MU_FILTER_DECODE, MU_STREAM_READ); + if (rc == 0) + bstr = dstr; + cat_message (out, bstr); + if (dstr) + stream_destroy (&dstr, stream_get_owner (dstr)); + return 0; +} + +int +exec_internal (message_t msg, msg_part_t part, char *encoding, char *cmd) +{ + int rc; + stream_t tmp; + + rc = prog_stream_create (&tmp, cmd, MU_STREAM_WRITE); + if (rc) + { + mh_error (_("can't create proc stream (command %s): %s"), + cmd, mu_strerror (rc)); + return rc; + } + rc = stream_open (tmp); + if (rc) + { + mh_error (_("can't open proc stream (command %s): %s"), + cmd, mu_strerror (rc)); + return rc; + } + show_internal (msg, part, encoding, tmp); + stream_destroy (&tmp, stream_get_owner (tmp)); + return rc; +} + +/* FIXME: stdin is always opened when invoking subprocess, no matter + what the MHN_STDIN bit is */ + +int +mhn_run_command (message_t msg, msg_part_t part, + char *encoding, + char *cmd, int flags, + char *tempfile) +{ + int rc = 0; + + if (tempfile) + { + /* pass content via a tempfile */ + int status; + int argc; + char **argv; + stream_t tmp; + + if (argcv_get (cmd, "", "#", &argc, &argv)) + { + mh_error (_("can't parse command line `%s'"), cmd); + return ENOSYS; + } + + rc = file_stream_create (&tmp, tempfile, MU_STREAM_RDWR); + if (rc) + { + mh_error (_("can't create temporary stream (file %s): %s"), + tempfile, mu_strerror (rc)); + argcv_free (argc, argv); + return rc; + } + rc = stream_open (tmp); + if (rc) + { + mh_error (_("can't open temporary stream (file %s): %s"), + tempfile, mu_strerror (rc)); + stream_destroy (&tmp, stream_get_owner (tmp)); + argcv_free (argc, argv); + return rc; + } + show_internal (msg, part, encoding, tmp); + stream_destroy (&tmp, stream_get_owner (tmp)); + rc = mu_spawnvp (argv[0], argv, &status); + if (status) + rc = status; + argcv_free (argc, argv); + } + else + rc = exec_internal (msg, part, encoding, cmd); + + return rc; +} + +int show_handler (message_t msg, msg_part_t part, char *type, char *encoding, void *data) { stream_t out = data; - - if (strncasecmp (type, "multipart", 9) == 0) + char *cmd; + int flags = 0; + char buf[64]; + int fd = 1; + char *tempfile = NULL; + int ismime; + + if (message_is_multipart (msg, &ismime) == 0 && ismime) return 0; - if (mhl_format) - mhl_format_run (mhl_format, width, 0, MHL_DECODE, msg, out); - else + + stream_get_fd (out, &fd); + + if (mode_options & OPT_PAUSE) + flags |= MHN_CONFIRM; + cmd = mhn_show_command (msg, part, &flags, &tempfile); + if (!cmd) + flags |= MHN_LISTING; + if (flags & MHN_LISTING) { - int rc; - body_t body = NULL; - stream_t dstr, bstr; - - if ((rc = message_get_body (msg, &body))) + char *str; + size_t size = 0; + + str = _("part "); + stream_sequential_write (out, str, strlen (str)); + str = msg_part_format (part); + stream_sequential_write (out, str, strlen (str)); + free (str); + stream_sequential_write (out, " ", 1); + stream_sequential_write (out, type, strlen (type)); + mhn_message_size (msg, &size); + snprintf (buf, sizeof buf, " %lu", (unsigned long) size); + stream_sequential_write (out, buf, strlen (buf)); + stream_sequential_write (out, "\n", 1); + stream_flush (out); + } + + if (flags & MHN_CONFIRM) + { + if (isatty (fd) && isatty (0)) { - mh_error (_("%lu: can't get message body: %s"), - (unsigned long) msg_part_subpart (part, 0), - mu_strerror (rc)); - return 0; + printf (_("Press <return> to show content...")); + if (!fgets (buf, sizeof buf, stdin) || buf[0] != '\n') + return 0; } - body_get_stream (body, &bstr); - rc = filter_create(&dstr, bstr, encoding, - MU_FILTER_DECODE, MU_STREAM_READ); - if (rc == 0) - bstr = dstr; - cat_message (out, bstr); - if (dstr) - stream_destroy (&dstr, stream_get_owner (dstr)); + } + + if (!cmd) + { + char *pager = mh_global_profile_get ("moreproc", getenv ("PAGER")); + if (pager) + exec_internal (msg, part, encoding, pager); + else + show_internal (msg, part, encoding, out); + } + else + { + mhn_run_command (msg, part, encoding, cmd, flags, tempfile); + free (cmd); + unlink (tempfile); + free (tempfile); } + + return 0; } int show_message (message_t msg, size_t num, void *data) { msg_part_t part = msg_part_create (num); + + if (mhl_format) + mhl_format_run (mhl_format, width, 0, MHL_DISABLE_BODY, msg, + (stream_t) data); + handle_message (msg, part, show_handler, data); msg_part_destroy (part); return 0; @@ -762,7 +1215,12 @@ show_message (message_t msg, size_t num, void *data) void show_iterator (mailbox_t mbox, message_t msg, size_t num, void *data) { + msg_part_t part; + + mh_message_number (msg, &num); + part = msg_part_create (num); show_message (msg, num, data); + msg_part_destroy (part); } int @@ -807,6 +1265,222 @@ mhn_show () rc = mh_iterate (mbox, &msgset, show_iterator, ostr); return rc; } + +/* ***************************** Store Mode ****************************** */ + +char * +normalize_path (char *cwd, char *path) +{ + int len; + char *p; + char *pcwd = NULL; + + if (!path) + return path; + + if (path[0] == '/') + return NULL; + + if (!cwd) + cwd = pcwd = mu_getcwd (); + + len = strlen (cwd) + strlen (path) + 2; + p = xmalloc (len); + sprintf (p, "%s/%s", cwd, path); + path = p; + + /* delete trailing delimiter if any */ + if (len && path[len-1] == '/') + path[len-1] = 0; + + /* Eliminate any /../ */ + for (p = strchr (path, '.'); p; p = strchr (p, '.')) + { + if (p > path && p[-1] == '/') + { + if (p[1] == '.' && (p[2] == 0 || p[2] == '/')) + /* found */ + { + char *q, *s; + + /* Find previous delimiter */ + for (q = p-2; *q != '/' && q >= path; q--) + ; + + if (q < path) + break; + /* Copy stuff */ + s = p + 2; + p = q; + while (*q++ = *s++) + ; + continue; + } + } + + p++; + } + + if (path[0] == 0) + { + path[0] = '/'; + path[1] = 0; + } + + len = strlen (cwd); + if (strlen (path) < len || memcmp (path, cwd, len)) + sfree (&path); + else + memmove (path, path + len + 1, strlen (path) - len); + free (pcwd); + return path; +} + +int +store_handler (message_t msg, msg_part_t part, char *type, char *encoding, + void *data) +{ + char *prefix = data; + char *name = NULL; + char *tmp; + int ismime; + int rc; + stream_t out; + char *dir = mh_global_profile_get ("mhn-storage", NULL); + + if (message_is_multipart (msg, &ismime) == 0 && ismime) + return 0; + + if (mode_options & OPT_AUTO) + { + header_t hdr; + char *val; + + if (message_get_header (msg, &hdr) == 0 + && header_aget_value (hdr, MU_HEADER_CONTENT_DISPOSITION, &val) == 0) + { + int argc; + char **argv; + + if (argcv_get (val, "=", NULL, &argc, &argv) == 0) + { + int i; + + for (i = 0; i < argc; i++) + { + if (strcmp (argv[i], "filename") == 0 + && ++i < argc + && argv[i][0] == '=' + && ++i < argc) + { + name = normalize_path (dir, argv[i]); + break; + } + } + argcv_free (argc, argv); + } + free (val); + } + } + + if (!name) + { + char *fname = mhn_store_command (msg, part, prefix); + if (dir) + asprintf (&name, "%s/%s", dir, fname); + else + name = fname; + } + + tmp = msg_part_format (part); + if (prefix) + printf (_("storing message %s part %s as file %s\n"), + prefix, + tmp, + name); + else + printf (_("storing message %lu part %s as file %s\n"), + (unsigned long) msg_part_subpart (part, 0), + tmp, + name); + free (tmp); + + if (!(mode_options & OPT_QUIET) && access (name, R_OK) == 0) + { + char *p; + int rc; + + asprintf (&p, _("File %s already exists. Rewrite"), name); + rc = mh_getyn (p); + free (p); + if (!rc) + { + free (name); + return 0; + } + } + + rc = file_stream_create (&out, name, MU_STREAM_WRITE|MU_STREAM_CREAT); + if (rc) + { + mh_error (_("can't create output stream (file %s): %s"), + name, mu_strerror (rc)); + free (name); + return rc; + } + rc = stream_open (out); + if (rc) + { + mh_error (_("can't open output stream (file %s): %s"), + name, mu_strerror (rc)); + free (name); + stream_destroy (&out, stream_get_owner (out)); + return rc; + } + show_internal (msg, part, encoding, out); + + stream_destroy (&out, stream_get_owner (out)); + free (name); + + return 0; +} + +void +store_message (message_t msg, void *data) +{ + size_t uid; + msg_part_t part; + + mh_message_number (msg, &uid); + part = msg_part_create (uid); + handle_message (msg, part, store_handler, data); + msg_part_destroy (part); +} + +void +store_iterator (mailbox_t mbox, message_t msg, size_t num, void *data) +{ + store_message (msg, data); +} + +int +mhn_store () +{ + int rc = 0; + + if (message) + { + char *p = strrchr (input_file, '/'); + if (p) + p++; + else + p = input_file; + store_message (message, p); + } + else + rc = mh_iterate (mbox, &msgset, store_iterator, NULL); + return rc; +} @@ -859,8 +1533,7 @@ main (int argc, char **argv) break; case mode_store: - mh_error ("mode is not yet implemented"); - rc = 1; + rc = mhn_store (); break; default: |