From: Ali Polatel <alip@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@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@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@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@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@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