From: Ali Polatel <[email protected]>

'link' is a new command to create links to specified target maildirs.
This is especially useful for integration with other mail agents.
---
 Makefile.local   |    2 +
 maildir.c        |  262 +++++++++++++++++++++++++++++++++++++++++
 maildir.h        |   53 +++++++++
 notmuch-client.h |    4 +
 notmuch-link.c   |  339 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 notmuch.c        |   44 +++++++
 6 files changed, 704 insertions(+), 0 deletions(-)
 create mode 100644 maildir.c
 create mode 100644 maildir.h
 create mode 100644 notmuch-link.c

diff --git a/Makefile.local b/Makefile.local
index 38f6c17..a613a4b 100644
--- a/Makefile.local
+++ b/Makefile.local
@@ -278,6 +278,7 @@ notmuch_client_srcs =               \
        notmuch-config.c        \
        notmuch-count.c         \
        notmuch-dump.c          \
+       notmuch-link.c          \
        notmuch-new.c           \
        notmuch-reply.c         \
        notmuch-restore.c       \
@@ -289,6 +290,7 @@ notmuch_client_srcs =               \
        query-string.c          \
        show-message.c          \
        json.c                  \
+       maildir.c               \
        xutil.c

 notmuch_client_modules = $(notmuch_client_srcs:.c=.o)
diff --git a/maildir.c b/maildir.c
new file mode 100644
index 0000000..b8c48b3
--- /dev/null
+++ b/maildir.c
@@ -0,0 +1,262 @@
+/* Maildir utilities for the notmuch mail library
+ *
+ * Copyright ? 2011 Ali Polatel
+ * Based in part upon mu which is:
+ *   Copyright ? 2008-2011 Dirk-Jan C. Binnema <djcb at djcbsoftware.nl>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, 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, see http://www.gnu.org/licenses/ .
+ *
+ * Author: Ali Polatel <polatel at gmail.com>
+ */
+
+#include "maildir.h"
+
+static const char *const maildir_subdirs_array[] = {"new", "cur", "tmp"};
+
+/* FIXME: The two functions below, dirent_sort_inode and get_dtype duplicate
+ * code from notmuch-new.c
+ */
+static int
+dirent_sort_inode (const struct dirent **a, const struct dirent **b)
+{
+    return ((*a)->d_ino < (*b)->d_ino) ? -1 : 1;
+}
+
+static unsigned char
+get_dtype(const char *fullpath, struct dirent *entry)
+{
+    struct stat buf;
+
+    if (entry->d_type != DT_UNKNOWN)
+       return entry->d_type;
+
+    if (lstat(fullpath, &buf) == -1) {
+       fprintf (stderr, "Warning: stat failed on `%s': %s\n",
+                fullpath, strerror(errno));
+       return DT_UNKNOWN;
+    }
+
+    if (S_ISREG (buf.st_mode)) {
+       return DT_REG;
+    } else if (S_ISDIR (buf.st_mode)) {
+       return DT_DIR;
+    } else if (S_ISLNK (buf.st_mode)) {
+       return DT_LNK;
+    }
+
+    return DT_UNKNOWN;
+}
+
+static notmuch_bool_t
+maildir_access (const char *path)
+{
+    struct stat buf;
+
+    if (access (path, R_OK | W_OK | X_OK) == -1) {
+       fprintf (stderr, "Failed to access path `%s': %s\n",
+                path, strerror(errno));
+       return FALSE;
+    }
+
+    if (lstat(path, &buf) == -1) {
+       fprintf (stderr, "Failed to access path `%s': %s\n",
+                path, strerror(errno));
+       return FALSE;
+    }
+
+    if (!S_ISDIR(buf.st_mode)) {
+       fprintf (stderr, "Path `%s' is not a directory\n", path);
+       return FALSE;
+    }
+
+    return TRUE;
+}
+
+/* Determine whether the source message is in 'new' or in 'cur';
+ * we ignore messages in 'tmp' for obvious reasons
+ */
+static notmuch_bool_t
+maildir_subdir (const char *src, notmuch_bool_t *in_cur)
+{
+    char *srcpath;
+
+    srcpath = g_path_get_dirname (src);
+
+    if (g_str_has_suffix (srcpath, "new"))
+       *in_cur = FALSE;
+    else if (g_str_has_suffix (srcpath, "cur"))
+       *in_cur = TRUE;
+    else {
+       g_free (srcpath);
+       errno = EINVAL;
+       return FALSE;
+    }
+
+    g_free(srcpath);
+    return TRUE;
+}
+
+static char *
+maildir_transform_path (const char *src, const char *targetpath)
+{
+    char *targetfullpath, *srcfile;
+    notmuch_bool_t in_cur;
+
+    if (!maildir_subdir (src, &in_cur)) {
+       fprintf (stderr, "Invalid maildir subdirectory `%s': %s\n",
+                src, strerror(errno));
+       return NULL;
+    }
+
+    srcfile = g_path_get_basename (src);
+    targetfullpath = g_strdup_printf ("%s%c%s%c%s",
+                                     targetpath,
+                                     G_DIR_SEPARATOR,
+                                     in_cur ? "cur" : "new",
+                                     G_DIR_SEPARATOR,
+                                     srcfile);
+    g_free (srcfile);
+
+    return targetfullpath;
+}
+
+notmuch_bool_t
+maildir_check (const char *path, notmuch_bool_t makedir, mode_t mode)
+{
+    int i;
+    char *fullpath = NULL;
+
+    for (i = 0; i != G_N_ELEMENTS (maildir_subdirs_array); i++) {
+           fullpath = g_build_filename (path, maildir_subdirs_array[i], NULL);
+           if (!makedir && !maildir_access(fullpath))
+               goto FAIL;
+           if (makedir && g_mkdir_with_parents (fullpath, (int)mode) != 0) {
+               fprintf (stderr, "Error creating %s: %s\n",
+                        fullpath, strerror(errno));
+               goto FAIL;
+           }
+           g_free (fullpath);
+    }
+
+    return TRUE;
+
+  FAIL:
+    g_free (fullpath);
+    return FALSE;
+}
+
+notmuch_bool_t
+maildir_rename (const char *src, const char *targetpath, rename_method_t methr)
+{
+    int ret;
+    char *targetfullpath;
+
+    targetfullpath = maildir_transform_path (src, targetpath);
+    if (!targetfullpath)
+       return FALSE;
+
+    switch (methr) {
+    case RENAME_SYMLINK:
+       ret = symlink (src, targetfullpath);
+       break;
+    case RENAME_HARDLINK:
+       ret = link (src, targetfullpath);
+       break;
+    default:
+       return FALSE;
+    }
+
+    if (ret == -1) {
+       if (errno != EEXIST) {
+           fprintf (stderr, "Failed to link %s to %s: %s\n",
+                    src, targetfullpath, strerror(errno));
+       }
+       g_free (targetfullpath);
+       return FALSE;
+    }
+
+    g_free (targetfullpath);
+    return TRUE;
+}
+
+int
+maildir_clean_recursive (const char *path, clean_method_t methc)
+{
+    int i, count, ret, num_fs_entries;
+    notmuch_bool_t delete;
+    char *fullpath = NULL;
+    struct dirent *entry = NULL;
+    struct dirent **fs_entries = NULL;
+    struct stat buf;
+
+    if (methc == CLEAN_NONE)
+       return 0;
+
+    num_fs_entries = scandir(path, &fs_entries, NULL, dirent_sort_inode);
+    if (num_fs_entries == -1) {
+       fprintf (stderr, "Error opening directory %s: %s\n",
+                path, strerror(errno));
+       return -1;
+    }
+
+    count = 0;
+    for (i = 0; i < num_fs_entries; i++) {
+       entry = fs_entries[i];
+
+       if (!entry->d_name ||
+           strcmp (entry->d_name, ".") == 0 ||
+           strcmp (entry->d_name, "..") == 0 ||
+           strcmp (entry->d_name, "tmp") == 0)
+       {
+           continue;
+       }
+
+       delete = FALSE;
+       fullpath = g_build_filename (path, entry->d_name, NULL);
+       switch (get_dtype(fullpath, entry)) {
+       case DT_REG:
+           if (methc == CLEAN_ALL)
+               delete = TRUE;
+           break;
+       case DT_LNK:
+           if (methc == CLEAN_ALL ||
+               methc == CLEAN_SYMLINK ||
+               (methc == CLEAN_DANGLING &&
+                (stat(fullpath, &buf) == -1 &&
+                 (errno == ENOENT || errno == ELOOP))))
+               delete = TRUE;
+           break;
+       case DT_DIR:
+           ret = maildir_clean_recursive (fullpath, methc);
+           if (ret != -1)
+               count += ret;
+           break;
+       default:
+           break; /* skip the rest */
+       }
+
+       if (delete) {
+           if (unlink (fullpath) == -1) {
+               fprintf (stderr, "Warning: error unlinking `%s': %s",
+                        fullpath, strerror(errno));
+           } else {
+               count++;
+           }
+       }
+
+       g_free (fullpath);
+    }
+
+    return count;
+}
diff --git a/maildir.h b/maildir.h
new file mode 100644
index 0000000..8b50ddf
--- /dev/null
+++ b/maildir.h
@@ -0,0 +1,53 @@
+/* Maildir utilities for the notmuch mail library
+ *
+ * Copyright ? 2011 Ali Polatel
+ * Based in part upon mu which is:
+ *   Copyright ? 2008-2011 Dirk-Jan C. Binnema <djcb at djcbsoftware.nl>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, 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, see http://www.gnu.org/licenses/ .
+ *
+ * Author: Ali Polatel <polatel at gmail.com>
+ */
+
+#ifndef NOTMUCH_MAILDIR_H
+#define NOTMUCH_MAILDIR_H
+
+#include "notmuch-client.h"
+
+typedef enum {
+    RENAME_SYMLINK,
+    RENAME_HARDLINK,
+    /* TODO:
+     * RENAME_COPY,
+     * RENAME_MOVE,
+     */
+} rename_method_t;
+
+typedef enum {
+    CLEAN_DANGLING,
+    CLEAN_SYMLINK,
+    CLEAN_ALL,
+    CLEAN_NONE
+} clean_method_t;
+
+notmuch_bool_t
+maildir_check (const char *path, notmuch_bool_t makedir, mode_t mode);
+
+notmuch_bool_t
+maildir_rename (const char *src, const char *targetpath, rename_method_t 
methr);
+
+int
+maildir_clean_recursive(const char *path, clean_method_t methc);
+
+#endif
diff --git a/notmuch-client.h b/notmuch-client.h
index b50cb38..979eafd 100644
--- a/notmuch-client.h
+++ b/notmuch-client.h
@@ -36,6 +36,7 @@
  * keep notmuch.c from looking into any internals, (which helps us
  * develop notmuch.h into a plausible library interface).
  */
+#include "maildir.h"
 #include "xutil.h"

 #include <stddef.h>
@@ -120,6 +121,9 @@ int
 notmuch_dump_command (void *ctx, int argc, char *argv[]);

 int
+notmuch_link_command (void *ctx, int argc, char *argv[]);
+
+int
 notmuch_new_command (void *ctx, int argc, char *argv[]);

 int
diff --git a/notmuch-link.c b/notmuch-link.c
new file mode 100644
index 0000000..a147206
--- /dev/null
+++ b/notmuch-link.c
@@ -0,0 +1,339 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright ? 2011 Ali Polatel
+ *
+ * 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 of the License, 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, see http://www.gnu.org/licenses/ .
+ *
+ * Author: Ali Polatel <alip at exherbo.org>
+ */
+
+#include "notmuch-client.h"
+
+typedef struct {
+    const char *maildir;
+
+    notmuch_bool_t cleandir;
+    notmuch_bool_t makedir;
+    notmuch_bool_t entire_thread;
+    mode_t mode;
+
+    clean_method_t clean_method;
+    rename_method_t rename_method;
+} link_options_t;
+
+static inline void
+init_link_options(link_options_t *options)
+{
+    options->maildir = NULL;
+
+    options->makedir = FALSE;
+    options->entire_thread = FALSE;
+    options->mode = 0700;
+
+    options->clean_method = CLEAN_NONE;
+    options->rename_method = RENAME_SYMLINK;
+}
+
+static inline const char *
+rename_method_abbr(rename_method_t method)
+{
+    switch (method) {
+    case RENAME_SYMLINK:
+       return "sym";
+    case RENAME_HARDLINK:
+       return "hard";
+    default:
+       return "love";
+    }
+}
+
+static inline const char *
+rename_method_verb(rename_method_t method)
+{
+    switch (method) {
+    case RENAME_SYMLINK:
+       return "symlinked";
+    case RENAME_HARDLINK:
+       return "hardlinked";
+    default:
+       return "made love all day long!";
+    }
+}
+
+static notmuch_bool_t
+prepare_maildir (const char *maildir,
+                notmuch_bool_t makedir,
+                mode_t mode,
+                clean_method_t methc)
+{
+    int ret;
+
+    if (!maildir_check(maildir, makedir, mode))
+       return FALSE;
+
+    ret = maildir_clean_recursive(maildir, methc);
+    if (ret == -1)
+       return FALSE;
+    else if (ret > 0)
+       printf("Unlinked %d entries under %s\n",
+              ret, maildir);
+
+    return TRUE;
+}
+
+static int
+link_messages (notmuch_messages_t *messages,
+              rename_method_t rename_method,
+              const char *maildir)
+{
+    int ret = 0;
+    const char *path;
+    notmuch_message_t *message;
+    notmuch_filenames_t *filenames;
+
+    for (;
+        notmuch_messages_valid (messages);
+        notmuch_messages_move_to_next (messages))
+    {
+       message = notmuch_messages_get (messages);
+
+       filenames = notmuch_message_get_filenames (message);
+       for (;
+            notmuch_filenames_valid (filenames);
+            notmuch_filenames_move_to_next (filenames))
+       {
+           path = notmuch_filenames_get (filenames);
+           if (maildir_rename (path, maildir, rename_method)) {
+               ret++;
+           }
+       }
+       notmuch_filenames_destroy( filenames );
+
+       notmuch_message_destroy (message);
+    }
+
+    return ret;
+}
+
+static int
+do_link_threads (notmuch_query_t *query,
+                const link_options_t *lopts)
+{
+    int message_count, thread_count;
+    notmuch_threads_t *threads;
+    notmuch_thread_t *thread;
+    notmuch_messages_t *messages;
+
+    threads = notmuch_query_search_threads (query);
+    if (threads == NULL)
+       return 1;
+
+    if (!prepare_maildir(lopts->maildir,
+                        lopts->makedir, lopts->mode,
+                        lopts->clean_method))
+       return 1;
+
+    message_count = thread_count = 0;
+    for (;
+        notmuch_threads_valid (threads);
+        notmuch_threads_move_to_next (threads))
+    {
+       thread = notmuch_threads_get (threads);
+
+       messages = notmuch_thread_get_toplevel_messages (thread);
+
+       if (messages == NULL)
+           INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
+                           notmuch_thread_get_thread_id (thread));
+       else
+           thread_count++;
+
+       message_count += link_messages (messages,
+                                       lopts->rename_method,
+                                       lopts->maildir);
+
+       notmuch_messages_destroy (messages);
+       notmuch_thread_destroy (thread);
+    }
+
+    if (message_count > 0) {
+       printf("%d messages in %d threads %s under %s\n",
+              message_count, thread_count,
+              rename_method_verb(lopts->rename_method),
+              lopts->maildir);
+    }
+
+    notmuch_threads_destroy (threads);
+
+    return 0;
+}
+
+static int
+do_link_messages (notmuch_query_t *query,
+                 const link_options_t *lopts)
+{
+    int message_count;
+    notmuch_messages_t *messages;
+
+    messages = notmuch_query_search_messages (query);
+    if (messages == NULL)
+       return 1;
+
+    if (!prepare_maildir(lopts->maildir,
+                        lopts->makedir,
+                        lopts->mode,
+                        lopts->clean_method))
+       return 1;
+
+    message_count = link_messages (messages,
+                                  lopts->rename_method,
+                                  lopts->maildir);
+    if (message_count > 0) {
+       printf("%d messages %s under %s\n",
+              message_count,
+              rename_method_verb(lopts->rename_method),
+              lopts->maildir);
+    }
+
+    notmuch_messages_destroy (messages);
+
+    return 0;
+}
+
+int
+notmuch_link_command (void *ctx, int argc, char *argv[])
+{
+    int i, ret;
+    char *query_str;
+    char *opt;
+    notmuch_config_t *config;
+    notmuch_database_t *notmuch;
+    notmuch_query_t *query;
+    link_options_t options;
+
+    init_link_options(&options);
+
+    for (i = 0; i < argc && argv[i][0] == '-'; i++) {
+       if (strcmp (argv[i], "--") == 0) {
+           i++;
+           break;
+       }
+       if (STRNCMP_LITERAL (argv[i], "--rename=") == 0) {
+           opt = argv[i] + sizeof ("--rename=") - 1;
+           if (strcmp (opt, "symlink") == 0) {
+               options.rename_method = RENAME_SYMLINK;
+           } else if (strcmp (opt, "hardlink") == 0) {
+               options.rename_method = RENAME_HARDLINK;
+#if 0
+#error TODO
+           } else if (strcmp (opt, "copy") == 0) {
+               options.rename_method = RENAME_COPY;
+           } else if (strcmp (opt, "move") == 0) {
+               options.rename_method = RENAME_MOVE;
+#endif
+           } else {
+               fprintf (stderr, "Invalid value for --rename: %s\n", opt);
+               return 1;
+           }
+       } else if (STRNCMP_LITERAL (argv[i], "--maildir=") == 0) {
+           opt = argv[i] + sizeof ("--maildir=") - 1;
+           options.maildir = talloc_strdup (ctx, opt);
+       } else if (STRNCMP_LITERAL (argv[i], "--clean=") == 0) {
+           opt = argv[i] + sizeof ("--clean=") - 1;
+           if (strcmp (opt, "dangling") == 0) {
+               options.clean_method = CLEAN_DANGLING;
+           } else if (strcmp (opt, "symlink") == 0) {
+               options.clean_method = CLEAN_SYMLINK;
+           } else if (strcmp (opt, "all") == 0) {
+               options.clean_method = CLEAN_ALL;
+           } else if (strcmp (opt, "none") == 0) {
+               options.clean_method = CLEAN_NONE;
+           } else {
+               fprintf (stderr, "Invalid value for --clean: %s\n", opt);
+               return 1;
+           }
+       } else if (STRNCMP_LITERAL (argv[i], "--mkdir") == 0) {
+           options.makedir = TRUE;
+
+           opt = argv[i] + sizeof ("--mkdir") - 1;
+
+           if (opt[0] == '\0') {
+               continue;
+           } else if (opt[0] != '=') {
+               fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
+               return 1;
+           } else {
+               opt++; /* skip '=' */
+           }
+
+           options.mode = 0;
+           while (*opt >= '0' && *opt <= '7')
+               options.mode = options.mode * 8 + (*opt++ - '0');
+           if (*opt) {
+               fprintf (stderr, "Invalid value for --mkdir: %s\n", opt);
+               return 1;
+           }
+       } else if (STRNCMP_LITERAL (argv[i], "--entire-thread") == 0) {
+           options.entire_thread = TRUE;
+       } else {
+           fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
+           return 1;
+       }
+    }
+
+    if (!options.maildir) {
+       fprintf (stderr, "Target directory must be specified "
+                        "using --maildir option\n");
+       return 1;
+    }
+
+    argc -= i;
+    argv += i;
+
+    config = notmuch_config_open (ctx, NULL, NULL);
+    if (config == NULL)
+       return 1;
+
+    notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
+                                    NOTMUCH_DATABASE_MODE_READ_ONLY);
+    if (notmuch == NULL)
+       return 1;
+
+    query_str = query_string_from_args (notmuch, argc, argv);
+    if (query_str == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       return 1;
+    }
+    if (*query_str == '\0') {
+       fprintf (stderr, "Error: notmuch link requires at least one search 
term.\n");
+       return 1;
+    }
+
+    query = notmuch_query_create (notmuch, query_str);
+    if (query == NULL) {
+       fprintf (stderr, "Out of memory\n");
+       return 1;
+    }
+
+    if (options.entire_thread) {
+       ret = do_link_threads (query, &options);
+    } else {
+       ret = do_link_messages (query, &options);
+    }
+
+    notmuch_query_destroy (query);
+    notmuch_database_close (notmuch);
+
+    return ret;
+}
diff --git a/notmuch.c b/notmuch.c
index f9d6629..2a8753c 100644
--- a/notmuch.c
+++ b/notmuch.c
@@ -374,6 +374,50 @@ static command_t commands[] = {
       "\n"
       "\tSee \"notmuch help search-terms\" for details of the search\n"
       "\tterms syntax." },
+    { "link", notmuch_link_command,
+      "--maildir=TARGET [options...] <search-terms> [...]",
+      "Link messages matching the given search terms.",
+      "\tMake links to the maildir specified with the --maildir option.\n"
+      "\tThis command may be used to create so-called \"virtual\" folders\n"
+      "\tfor integration with different mail agents.\n"
+      "\n"
+      "\tSupported options for link include:\n"
+      "\n"
+      "\t--entire-thread\n"
+      "\n"
+      "\t\tBy default only those messages that match the\n"
+      "\t\tsearch terms will be linked. With this option,\n"
+      "\t\tall messages in the same thread as any matched\n"
+      "\t\tmessage will be linked.\n"
+      "\n"
+      "\t--rename=(hardlink|symlink)\n"
+      "\n"
+      "\t\tSpecify the renaming method, either hardlink or\n"
+      "\t\tsymlink (default)\n"
+      "\n"
+      "\t--maildir=TARGET\n"
+      "\n"
+      "\t\tSpecify the target maildir. This option is mandatory.\n"
+      "\n"
+      "\t--mkdir[=OCTAL_MODE]\n"
+      "\n"
+      "\t\tCreate the target maildir specified with the --maildir option\n"
+      "\t\tand any non-existing parent directories.\n"
+      "\t\tAccepts an optional octal mode argument which may be used to\n"
+      "\t\tspecify permissions. Default is 0700.\n"
+      "\n"
+      "\t--clean=(dangling|symlink|all|none)\n"
+      "\n"
+      "\t\tClean the target maildir specified with --maildir option\n"
+      "\t\tbefore proceeding to create links, using the specified method.\n"
+      "\t\tMethod may be one of:\n"
+      "\t\t- dangling: Clean only dangling symbolic links\n"
+      "\t\t- symlink:  Clean all symbolic links\n"
+      "\t\t- all:      Clean regular files in addition to symbolic links\n"
+      "\t\t- none:     Clean no files (default)\n"
+      "\n"
+      "\tSee \"notmuch help search-terms\" for details of the search\n"
+      "\tterms syntax." },
     { "dump", notmuch_dump_command,
       "[<filename>]",
       "Create a plain-text dump of the tags for each message.",
-- 
1.7.6.1

Reply via email to