[RFC/PATCH] link: Add new command
Ali Polatel
polatel at gmail.com
Sat Oct 1 01:45:46 PDT 2011
From: Ali Polatel <alip at exherbo.org>
'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
More information about the notmuch
mailing list