From: David Bremner Date: Wed, 18 Aug 2021 05:03:24 +0000 (-0700) Subject: Merge remote-tracking branch 'origin/debian/bullseye' into release X-Git-Tag: archive/debian/0.32.3-1~1 X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=commitdiff_plain;h=9d6f4641d17a6100cb8d96bc1e09d3d4999c34f3;hp=b6cfc8a61c20aec1e54185829718f87c7f7dea79 Merge remote-tracking branch 'origin/debian/bullseye' into release --- diff --git a/NEWS b/NEWS index 6e88ebd9..b826d280 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,155 @@ +Notmuch 0.32.3 (2021-08-17) +=========================== + +Library +------- + +Restore location of database via `MAILDIR` environment variable, which +was broken in 0.32. + +Bump libnotmuch minor version to match the documentation in +`notmuch.h`. + +Correct documentation for deprecated database opening functions to +point out that they (still) do not load configuration information. + +CLI +--- + +Restore "notmuch config get built_with.*", which was broken in 0.32. + +Notmuch 0.32.2 (2021-06-27) +=========================== + +General +------- + +Fix a bug from 2017 that can add duplicate thread-id terms to message +documents. + +CLI +--- + +Fix small memory leak in notmuch new. + +Emacs +----- + +Add `(require 'seq)` for `seq-some`. + +Documentation +------------- + +Fix man page build for Sphinx 4.x. Fix variable name in emacs docs. + +Tests +----- + +Fix backup creation in `perf-test/T00-new`. Check openssl +prerequisite in `add_gpgsm_home`. + +Notmuch 0.32.1 (2021-05-15) +=========================== + +General +------- + +Restore handling of relative values for `database.path` that was +broken by 0.32. Extend this handling to `database.mail_root`, +`database.backup_dir`, and `database.hook_dir`. + +Reload certain metadata from Xapian database in +notmuch_database_reopen. This fixes a bug when adding messages to the +database in a pre-new hook. + +Fix default of `$HOME/mail` for `database.path`. In release 0.32, this +default worked only in "notmuch config". + +Emacs +----- + +Restore the dynamically bound variables `tag-changes` and `query` in +in `notmuch-before-tag-hook` and `notmuch-after-tag-hook`. + +Notmuch 0.32 (2021-05-02) +========================= + +General +------- + +This release includes a significant overhaul of the configuration +management facilities for notmuch. The previous distinction between +configuration items that can be modified via plain text configuration +files and those that must be set in the database via the "notmuch +config" subcommand is gone, and all configuration items can be set in +both ways. The external configuration file overrides configuration +items in the database. The location of database, hooks, and +configuration files is now more flexible, with several new +configuration variables. In particular XDG locations are now supported +as fallbacks for database, configuration and hooks. For more +information see `notmuch-config(1)`. + +Library +------- + +To support the new configuration facilities, several functions and +constants have been added to the notmuch API. Most notably: + +- `notmuch_database_create_with_config` +- `notmuch_database_open_with_config` +- `notmuch_database_load_config` +- `notmuch_config_get` + +A previously requested API change is that `notmuch_database_reopen` is +now exposed (and generalized). + +The previously severe slowdowns from large numbers calls to +notmuch_database_remove_message or notmuch_message_delete in one +session has been fixed. + +As always, the canonical source of API documentation is +`lib/notmuch.h`, or the doxygen formatted documentation in `notmuch(3)` + +CLI +--- + +The `notmuch config set` subcommand gained a `--database` argument to +specify that the database should be updated, rather than a config file. + +The speed of `notmuch new` and `notmuch reindex` in dealing with large +numbers of mail file deletions is significantly improved. + +Emacs +----- + +Completion related updates include: de-duplicating tags offered for +completion, use the actual initial input in address completion, allow +users to opt out of notmuch address completion, and do not force Ido +when prompting for senders. + +Some keymaps used to contain bindings for unnamed commands. These +lambda expressions have been replaced by named commands (symbols), to +ease customization. + +Lexical binding is now used in all notmuch-emacs libraries. + +Fix bug in calling `notmuch-mua-mail` with a non-nil RETURN-ACTION. + +Removed, inlined or renamed functions and variables: + `notmuch-address-locate-command`, + `notmuch-documentation-first-line`, `notmuch-folder`, + `notmuch-hello-trim', `notmuch-hello-versions` => `notmuch-version`, + `notmuch-remove-if-not`, `notmuch-search-disjunctive-regexp`, + `notmuch-sexp-eof`, `notmuch-split-content-type`, and + `notmuch-tree-button-activate`. + +Keymaps are no longer fset, which means they need to be referred to in +define-key directly (without quotes). If your Emacs configuration has a +keybinding like: + (define-key 'notmuch-show-mode-map "7" 'foo) +you should change it to: + (define-key notmuch-show-mode-map "7" 'foo) + Notmuch 0.31.4 (2021-02-18) =========================== diff --git a/bindings/python-cffi/notmuch2/_build.py b/bindings/python-cffi/notmuch2/_build.py index f269f2a1..f712b6c5 100644 --- a/bindings/python-cffi/notmuch2/_build.py +++ b/bindings/python-cffi/notmuch2/_build.py @@ -47,6 +47,12 @@ ffibuilder.cdef( NOTMUCH_STATUS_UPGRADE_REQUIRED, NOTMUCH_STATUS_PATH_ERROR, NOTMUCH_STATUS_ILLEGAL_ARGUMENT, + NOTMUCH_STATUS_MALFORMED_CRYPTO_PROTOCOL, + NOTMUCH_STATUS_FAILED_CRYPTO_CONTEXT_CREATION, + NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL, + NOTMUCH_STATUS_NO_CONFIG, + NOTMUCH_STATUS_NO_DATABASE, + NOTMUCH_STATUS_DATABASE_EXISTS, NOTMUCH_STATUS_LAST_STATUS } notmuch_status_t; typedef enum { diff --git a/bindings/python-cffi/notmuch2/_errors.py b/bindings/python-cffi/notmuch2/_errors.py index 13369445..9301073e 100644 --- a/bindings/python-cffi/notmuch2/_errors.py +++ b/bindings/python-cffi/notmuch2/_errors.py @@ -50,6 +50,12 @@ class NotmuchError(Exception): PathError, capi.lib.NOTMUCH_STATUS_ILLEGAL_ARGUMENT: IllegalArgumentError, + capi.lib.NOTMUCH_STATUS_NO_CONFIG: + NoConfigError, + capi.lib.NOTMUCH_STATUS_NO_DATABASE: + NoDatabaseError, + capi.lib.NOTMUCH_STATUS_DATABASE_EXISTS: + DatabaseExistsError, } return types[status] @@ -94,7 +100,9 @@ class UnsupportedOperationError(NotmuchError): pass class UpgradeRequiredError(NotmuchError): pass class PathError(NotmuchError): pass class IllegalArgumentError(NotmuchError): pass - +class NoConfigError(NotmuchError): pass +class NoDatabaseError(NotmuchError): pass +class DatabaseExistsError(NotmuchError): pass class ObjectDestroyedError(NotmuchError): """The object has already been destroyed and it's memory freed. diff --git a/bindings/python-cffi/tests/test_database.py b/bindings/python-cffi/tests/test_database.py index a2c69de6..9b3219c0 100644 --- a/bindings/python-cffi/tests/test_database.py +++ b/bindings/python-cffi/tests/test_database.py @@ -80,7 +80,7 @@ class TestCreate: db.create(tmppath) def test_create_existing(self, tmppath, db): - with pytest.raises(errors.FileError): + with pytest.raises(errors.DatabaseExistsError): dbmod.Database.create(path=tmppath) def test_close(self, db): diff --git a/bindings/python-cffi/version.txt b/bindings/python-cffi/version.txt index a8a02172..d721c768 100644 --- a/bindings/python-cffi/version.txt +++ b/bindings/python-cffi/version.txt @@ -1 +1 @@ -0.31.4 +0.32.3 diff --git a/bindings/python/notmuch/version.py b/bindings/python/notmuch/version.py index 6f6372ab..e10408ac 100644 --- a/bindings/python/notmuch/version.py +++ b/bindings/python/notmuch/version.py @@ -1,3 +1,3 @@ # this file should be kept in sync with ../../../version -__VERSION__ = '0.31.4' +__VERSION__ = '0.32.3' SOVERSION = '5' diff --git a/bindings/python/setup.py b/bindings/python/setup.py index d986f0c6..6308b9f9 100644 --- a/bindings/python/setup.py +++ b/bindings/python/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """ This file is part of notmuch. diff --git a/bindings/ruby/init.c b/bindings/ruby/init.c index 5556b43e..819fd1e3 100644 --- a/bindings/ruby/init.c +++ b/bindings/ruby/init.c @@ -211,7 +211,7 @@ Init_notmuch (void) * * Notmuch database interaction */ - notmuch_rb_cDatabase = rb_define_class_under (mod, "Database", rb_cData); + notmuch_rb_cDatabase = rb_define_class_under (mod, "Database", rb_cObject); rb_define_alloc_func (notmuch_rb_cDatabase, notmuch_rb_database_alloc); rb_define_singleton_method (notmuch_rb_cDatabase, "open", notmuch_rb_database_open, -1); /* in database.c */ rb_define_method (notmuch_rb_cDatabase, "initialize", notmuch_rb_database_initialize, -1); /* in database.c */ @@ -237,7 +237,7 @@ Init_notmuch (void) * * Notmuch directory */ - notmuch_rb_cDirectory = rb_define_class_under (mod, "Directory", rb_cData); + notmuch_rb_cDirectory = rb_define_class_under (mod, "Directory", rb_cObject); rb_undef_method (notmuch_rb_cDirectory, "initialize"); rb_define_method (notmuch_rb_cDirectory, "destroy!", notmuch_rb_directory_destroy, 0); /* in directory.c */ rb_define_method (notmuch_rb_cDirectory, "mtime", notmuch_rb_directory_get_mtime, 0); /* in directory.c */ @@ -250,7 +250,7 @@ Init_notmuch (void) * * Notmuch file names */ - notmuch_rb_cFileNames = rb_define_class_under (mod, "FileNames", rb_cData); + notmuch_rb_cFileNames = rb_define_class_under (mod, "FileNames", rb_cObject); rb_undef_method (notmuch_rb_cFileNames, "initialize"); rb_define_method (notmuch_rb_cFileNames, "destroy!", notmuch_rb_filenames_destroy, 0); /* in filenames.c */ rb_define_method (notmuch_rb_cFileNames, "each", notmuch_rb_filenames_each, 0); /* in filenames.c */ @@ -261,7 +261,7 @@ Init_notmuch (void) * * Notmuch query */ - notmuch_rb_cQuery = rb_define_class_under (mod, "Query", rb_cData); + notmuch_rb_cQuery = rb_define_class_under (mod, "Query", rb_cObject); rb_undef_method (notmuch_rb_cQuery, "initialize"); rb_define_method (notmuch_rb_cQuery, "destroy!", notmuch_rb_query_destroy, 0); /* in query.c */ rb_define_method (notmuch_rb_cQuery, "sort", notmuch_rb_query_get_sort, 0); /* in query.c */ @@ -279,7 +279,7 @@ Init_notmuch (void) * * Notmuch threads */ - notmuch_rb_cThreads = rb_define_class_under (mod, "Threads", rb_cData); + notmuch_rb_cThreads = rb_define_class_under (mod, "Threads", rb_cObject); rb_undef_method (notmuch_rb_cThreads, "initialize"); rb_define_method (notmuch_rb_cThreads, "destroy!", notmuch_rb_threads_destroy, 0); /* in threads.c */ rb_define_method (notmuch_rb_cThreads, "each", notmuch_rb_threads_each, 0); /* in threads.c */ @@ -290,7 +290,7 @@ Init_notmuch (void) * * Notmuch messages */ - notmuch_rb_cMessages = rb_define_class_under (mod, "Messages", rb_cData); + notmuch_rb_cMessages = rb_define_class_under (mod, "Messages", rb_cObject); rb_undef_method (notmuch_rb_cMessages, "initialize"); rb_define_method (notmuch_rb_cMessages, "destroy!", notmuch_rb_messages_destroy, 0); /* in messages.c */ rb_define_method (notmuch_rb_cMessages, "each", notmuch_rb_messages_each, 0); /* in messages.c */ @@ -302,7 +302,7 @@ Init_notmuch (void) * * Notmuch thread */ - notmuch_rb_cThread = rb_define_class_under (mod, "Thread", rb_cData); + notmuch_rb_cThread = rb_define_class_under (mod, "Thread", rb_cObject); rb_undef_method (notmuch_rb_cThread, "initialize"); rb_define_method (notmuch_rb_cThread, "destroy!", notmuch_rb_thread_destroy, 0); /* in thread.c */ rb_define_method (notmuch_rb_cThread, "thread_id", notmuch_rb_thread_get_thread_id, 0); /* in thread.c */ @@ -321,7 +321,7 @@ Init_notmuch (void) * * Notmuch message */ - notmuch_rb_cMessage = rb_define_class_under (mod, "Message", rb_cData); + notmuch_rb_cMessage = rb_define_class_under (mod, "Message", rb_cObject); rb_undef_method (notmuch_rb_cMessage, "initialize"); rb_define_method (notmuch_rb_cMessage, "destroy!", notmuch_rb_message_destroy, 0); /* in message.c */ rb_define_method (notmuch_rb_cMessage, "message_id", notmuch_rb_message_get_message_id, 0); /* in message.c */ @@ -349,7 +349,7 @@ Init_notmuch (void) * * Notmuch tags */ - notmuch_rb_cTags = rb_define_class_under (mod, "Tags", rb_cData); + notmuch_rb_cTags = rb_define_class_under (mod, "Tags", rb_cObject); rb_undef_method (notmuch_rb_cTags, "initialize"); rb_define_method (notmuch_rb_cTags, "destroy!", notmuch_rb_tags_destroy, 0); /* in tags.c */ rb_define_method (notmuch_rb_cTags, "each", notmuch_rb_tags_each, 0); /* in tags.c */ diff --git a/command-line-arguments.c b/command-line-arguments.c index 169b12a3..5dea8281 100644 --- a/command-line-arguments.c +++ b/command-line-arguments.c @@ -47,17 +47,23 @@ _process_keyword_arg (const notmuch_opt_desc_t *arg_desc, char next, continue; *arg_desc->opt_keyword = keywords->value; - fprintf (stderr, "Warning: No known keyword option given for \"%s\", choosing value \"%s\"." - " Please specify the argument explicitly!\n", arg_desc->name, arg_desc->keyword_no_arg_value); + fprintf (stderr, + "Warning: No known keyword option given for \"%s\", choosing value \"%s\"." + " Please specify the argument explicitly!\n", arg_desc->name, + arg_desc->keyword_no_arg_value); return OPT_GIVEBACK; } - fprintf (stderr, "No matching keyword for option \"%s\" and default value \"%s\" is invalid.\n", arg_str, arg_desc->name); + fprintf (stderr, + "No matching keyword for option \"%s\" and default value \"%s\" is invalid.\n", + arg_str, + arg_desc->name); return OPT_FAILED; } if (next != '\0') - fprintf (stderr, "Unknown keyword argument \"%s\" for option \"%s\".\n", arg_str, arg_desc->name); + fprintf (stderr, "Unknown keyword argument \"%s\" for option \"%s\".\n", arg_str, + arg_desc->name); else fprintf (stderr, "Option \"%s\" needs a keyword argument.\n", arg_desc->name); return OPT_FAILED; @@ -74,7 +80,8 @@ _process_boolean_arg (const notmuch_opt_desc_t *arg_desc, char next, } else if (strcmp (arg_str, "false") == 0) { value = false; } else { - fprintf (stderr, "Unknown argument \"%s\" for (boolean) option \"%s\".\n", arg_str, arg_desc->name); + fprintf (stderr, "Unknown argument \"%s\" for (boolean) option \"%s\".\n", arg_str, + arg_desc->name); return OPT_FAILED; } @@ -202,6 +209,7 @@ parse_option (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_ const notmuch_opt_desc_t *try; const char *next_arg = NULL; + if (opt_index < argc - 1 && strncmp (argv[opt_index + 1], "--", 2) != 0) next_arg = argv[opt_index + 1]; diff --git a/compat/Makefile.local b/compat/Makefile.local index 2ee1b399..c58ca746 100644 --- a/compat/Makefile.local +++ b/compat/Makefile.local @@ -5,10 +5,6 @@ extra_cflags += -I$(srcdir)/$(dir) notmuch_compat_srcs := -ifneq ($(HAVE_CANONICALIZE_FILE_NAME),1) -notmuch_compat_srcs += $(dir)/canonicalize_file_name.c -endif - ifneq ($(HAVE_GETLINE),1) notmuch_compat_srcs += $(dir)/getline.c $(dir)/getdelim.c endif diff --git a/compat/canonicalize_file_name.c b/compat/canonicalize_file_name.c deleted file mode 100644 index 000f9e78..00000000 --- a/compat/canonicalize_file_name.c +++ /dev/null @@ -1,18 +0,0 @@ -#include "compat.h" -#include -#undef _GNU_SOURCE -#include - -char * -canonicalize_file_name (const char *path) -{ -#ifdef PATH_MAX - char *resolved_path = malloc (PATH_MAX + 1); - if (resolved_path == NULL) - return NULL; - - return realpath (path, resolved_path); -#else -#error undefined PATH_MAX _and_ missing canonicalize_file_name not supported -#endif -} diff --git a/compat/compat.h b/compat/compat.h index 8f15e585..59e91618 100644 --- a/compat/compat.h +++ b/compat/compat.h @@ -37,14 +37,6 @@ extern "C" { #define _POSIX_PTHREAD_SEMANTICS 1 #endif -#if ! HAVE_CANONICALIZE_FILE_NAME -/* we only call this function from C, and this makes testing easier */ -#ifndef __cplusplus -char * -canonicalize_file_name (const char *path); -#endif -#endif - #if ! HAVE_GETLINE #include #include diff --git a/debian/changelog b/debian/changelog index ebfd9209..aa4218c2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,49 @@ +notmuch (0.32.3-1) unstable; urgency=medium + + * new upstream bugfix release + * fixes for a few configuration related bugs introduced in 0.32 + * bump libnotmuch minor version to match documentation. + + -- David Bremner Tue, 17 Aug 2021 17:16:09 -0700 + +notmuch (0.32.2-1) experimental; urgency=medium + + * New upstream bugfix release + * Fix for memory leak in "notmuch new" introduced in 0.32 + * Fix for bug from 2017 that can add duplicate thread-ids to messages. + + -- David Bremner Sat, 26 Jun 2021 22:33:56 -0300 + +notmuch (0.32.1-1) experimental; urgency=medium + + * New upstream bugfix release + * Configuration bug fixes (see /usr/share/doc/notmuch/NEWS.gz) + * Bug fix for {pre,after}-tag hooks in emacs, related to lexical scope + transition. + + -- David Bremner Sat, 15 May 2021 09:01:27 -0300 + +notmuch (0.32-1) experimental; urgency=medium + + * New upstream release + * Speedup for handling deleted message files + * New configuration features (see /usr/share/doc/notmuch/NEWS.gz) + * Emacs interface codebase cleanup + + -- David Bremner Sun, 02 May 2021 07:05:15 -0300 + +notmuch (0.32~rc2-1) experimental; urgency=medium + + * New upstream release candidate + + -- David Bremner Wed, 28 Apr 2021 07:05:22 -0300 + +notmuch (0.32~rc1-1) experimental; urgency=medium + + * New upstream release candidate + + -- David Bremner Sat, 24 Apr 2021 12:46:10 -0300 + notmuch (0.31.4-2) unstable; urgency=medium * Cherry pick upstream commit 3f4de98e7c8, which fixes a bug where diff --git a/debian/libnotmuch5.symbols b/debian/libnotmuch5.symbols index ec01593b..4c7bffd9 100644 --- a/debian/libnotmuch5.symbols +++ b/debian/libnotmuch5.symbols @@ -1,17 +1,36 @@ libnotmuch.so.5 libnotmuch5 #MINVER# * Build-Depends-Package: libnotmuch-dev notmuch_built_with@Base 0.23~rc0 + notmuch_config_get@Base 0.32~rc0 + notmuch_config_get_bool@Base 0.32~rc0 + notmuch_config_get_pairs@Base 0.32~rc0 + notmuch_config_get_values@Base 0.32~rc0 + notmuch_config_get_values_string@Base 0.32~rc0 notmuch_config_list_destroy@Base 0.23~rc0 notmuch_config_list_key@Base 0.23~rc0 notmuch_config_list_move_to_next@Base 0.23~rc0 notmuch_config_list_valid@Base 0.23~rc0 notmuch_config_list_value@Base 0.23~rc0 + notmuch_config_pairs_destroy@Base 0.32~rc0 + notmuch_config_pairs_key@Base 0.32~rc0 + notmuch_config_pairs_move_to_next@Base 0.32~rc0 + notmuch_config_pairs_valid@Base 0.32~rc0 + notmuch_config_pairs_value@Base 0.32~rc0 + notmuch_config_path@Base 0.32~rc0 + notmuch_config_set@Base 0.32~rc0 + notmuch_config_values_destroy@Base 0.32~rc0 + notmuch_config_values_get@Base 0.32~rc0 + notmuch_config_values_move_to_next@Base 0.32~rc0 + notmuch_config_values_start@Base 0.32~rc0 + notmuch_config_values_valid@Base 0.32~rc0 notmuch_database_add_message@Base 0.3 notmuch_database_begin_atomic@Base 0.9~rc1 notmuch_database_close@Base 0.13~rc1 notmuch_database_compact@Base 0.17~rc1 + notmuch_database_compact_db@Base 0.32~rc0 notmuch_database_create@Base 0.3 notmuch_database_create_verbose@Base 0.20~rc1 + notmuch_database_create_with_config@Base 0.32~rc0 notmuch_database_destroy@Base 0.13~rc1 notmuch_database_end_atomic@Base 0.9~rc1 notmuch_database_find_message@Base 0.9~rc2 @@ -25,10 +44,13 @@ libnotmuch.so.5 libnotmuch5 #MINVER# notmuch_database_get_revision@Base 0.21~rc1 notmuch_database_get_version@Base 0.3 notmuch_database_index_file@Base 0.26~rc0 + notmuch_database_load_config@Base 0.32~rc0 notmuch_database_needs_upgrade@Base 0.3 notmuch_database_open@Base 0.3 notmuch_database_open_verbose@Base 0.20~rc1 + notmuch_database_open_with_config@Base 0.32~rc0 notmuch_database_remove_message@Base 0.3 + notmuch_database_reopen@Base 0.32~rc0 notmuch_database_set_config@Base 0.23~rc0 notmuch_database_status_string@Base 0.20~rc1 notmuch_database_upgrade@Base 0.3 @@ -130,6 +152,7 @@ libnotmuch.so.5 libnotmuch5 #MINVER# (c++)"typeinfo for Xapian::Error@Base" 0.6.1 (c++)"typeinfo for Xapian::DatabaseError@Base" 0.24~rc0 (c++)"typeinfo for Xapian::DatabaseModifiedError@Base" 0.24~rc0 + (c++)"typeinfo for Xapian::DatabaseOpeningError@Base" 0.32~rc0 (c++|optional=present with Xapian 1.4)"typeinfo for Xapian::QueryParserError@Base" 0.23~rc0 (c++)"typeinfo name for Xapian::LogicError@Base" 0.6.1 (c++)"typeinfo name for Xapian::RuntimeError@Base" 0.6.1 @@ -138,4 +161,5 @@ libnotmuch.so.5 libnotmuch5 #MINVER# (c++)"typeinfo name for Xapian::Error@Base" 0.6.1 (c++)"typeinfo name for Xapian::DatabaseError@Base" 0.24~rc0 (c++)"typeinfo name for Xapian::DatabaseModifiedError@Base" 0.24~rc0 + (c++)"typeinfo name for Xapian::DatabaseOpeningError@Base" 0.32~rc0 (c++|optional=present with Xapian 1.4)"typeinfo name for Xapian::QueryParserError@Base" 0.23~rc0 diff --git a/devel/nmbug/notmuch-report b/devel/nmbug/notmuch-report index 18a0bc70..9a6a31cc 100755 --- a/devel/nmbug/notmuch-report +++ b/devel/nmbug/notmuch-report @@ -370,9 +370,11 @@ header_template = config['meta'].get('header', ''' border-bottom-right-radius: {border_radius}; }} tbody:nth-child(4n+1) tr td {{ + color: #000; background-color: #ffd96e; }} tbody:nth-child(4n+3) tr td {{ + color: #000; background-color: #bce; }} hr {{ diff --git a/devel/uncrustify.cfg b/devel/uncrustify.cfg index c36c33d6..d203d4e1 100644 --- a/devel/uncrustify.cfg +++ b/devel/uncrustify.cfg @@ -119,3 +119,9 @@ cmt_star_cont = true # indent_brace = 0 indent_class = true + +# line width / line splitting +code_width 102 +ls_for_split_full True +ls_func_split_full True +ls_code_width True diff --git a/doc/conf.py b/doc/conf.py index 1a5c217b..d0f7f66c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -87,6 +87,8 @@ html_use_smartypants = False notmuch_authors = u'Carl Worth and many others' +man_make_section_directory = False + man_pages = [ ('man1/notmuch', 'notmuch', u'thread-based email index, search, and tagging', diff --git a/doc/doxygen.cfg b/doc/doxygen.cfg index a2c4fd07..4a022de1 100644 --- a/doc/doxygen.cfg +++ b/doc/doxygen.cfg @@ -27,7 +27,6 @@ INHERIT_DOCS = YES SEPARATE_MEMBER_PAGES = NO TAB_SIZE = 8 ALIASES = -TCL_SUBST = OPTIMIZE_OUTPUT_FOR_C = YES OPTIMIZE_OUTPUT_JAVA = NO OPTIMIZE_FOR_FORTRAN = NO diff --git a/doc/man1/notmuch-config.rst b/doc/man1/notmuch-config.rst index 769f336a..209226a3 100644 --- a/doc/man1/notmuch-config.rst +++ b/doc/man1/notmuch-config.rst @@ -7,7 +7,7 @@ SYNOPSIS **notmuch** **config** **get** <*section*>.<*item*> -**notmuch** **config** **set** <*section*>.<*item*> [*value* ...] +**notmuch** **config** **set** [--database] <*section*>.<*item*> [*value* ...] **notmuch** **config** **list** @@ -17,10 +17,6 @@ DESCRIPTION The **config** command can be used to get or set settings in the notmuch configuration file and corresponding database. -Items marked **[STORED IN DATABASE]** are only in the database. They -should not be placed in the configuration file, and should be accessed -programmatically as described in the SYNOPSIS above. - **get** The value of the specified configuration item is printed to stdout. If the item has multiple values (it is a list), each value @@ -34,6 +30,10 @@ programmatically as described in the SYNOPSIS above. If no values are provided, the specified configuration item will be removed from the configuration file. + With the `--database` option, updates configuration metadata + stored in the database, rather than the default (text) + configuration file. + **list** Every configuration item is printed to stdout, each on a separate line of the form:: @@ -44,15 +44,42 @@ programmatically as described in the SYNOPSIS above. characters. In a multiple-value item (a list), the values are separated by semicolon characters. -The available configuration items are described below. +The available configuration items are described below. Non-absolute +paths are presumed relative to `$HOME` for items in section +**database**. **database.path** + Notmuch will store its database here, (in + sub-directory named ``.notmuch`` if **database.mail\_root** + is unset). + + Default: ``$MAILDIR`` variable if set, otherwise ``$HOME/mail``. + +**database.mail_root** The top-level directory where your mail currently exists and to where mail will be delivered in the future. Files should be - individual email messages. Notmuch will store its database within - a sub-directory of the path configured here named ``.notmuch``. + individual email messages. - Default: ``$MAILDIR`` variable if set, otherwise ``$HOME/mail``. + History: this configuration value was introduced in notmuch 0.32. + + Default: For compatibility with older configurations, the value of + database.path is used if **database.mail\_root** is unset. + +**database.backup_dir** + Directory to store tag dumps when upgrading database. + + History: this configuration value was introduced in notmuch 0.32. + + Default: A sibling directory of the Xapian database called + `backups`. + +**database.hook_dir** + Directory containing hooks run by notmuch commands. See + **notmuch-hooks(5)**. + + History: this configuration value was introduced in notmuch 0.32. + + Default: See HOOKS, below. **user.name** Your full name. @@ -134,7 +161,7 @@ The available configuration items are described below. Default: ``true``. -**index.decrypt** **[STORED IN DATABASE]** +**index.decrypt** Policy for decrypting encrypted messages during indexing. Must be one of: ``false``, ``auto``, ``nostash``, or ``true``. @@ -187,7 +214,7 @@ The available configuration items are described below. Default: ``auto``. -**index.header.** **[STORED IN DATABASE]** +**index.header.** Define the query prefix , based on a mail header. For example ``index.header.List=List-Id`` will add a probabilistic prefix ``List:`` that searches the ``List-Id`` field. User @@ -202,7 +229,7 @@ The available configuration items are described below. (since notmuch 0.30, "compact" and "field_processor" are always included.) -**query.** **[STORED IN DATABASE]** +**query.** Expansion for named query called . See **notmuch-search-terms(7)** for more information about named queries. @@ -214,8 +241,32 @@ The following environment variables can be used to control the behavior of notmuch. **NOTMUCH\_CONFIG** - Specifies the location of the notmuch configuration file. Notmuch - will use ${HOME}/.notmuch-config if this variable is not set. + Specifies the location of the notmuch configuration file. + +**NOTMUCH_PROFILE** + Selects among notmuch configurations. + +FILES +===== + +CONFIGURATION +------------- + +If ``NOTMUCH_CONFIG`` is unset, notmuch tries (in order) + +- ``$XDG_CONFIG_HOME/notmuch//config`` where ```` is + defined by ``$NOTMUCH_PROFILE`` or "default" +- ``${HOME}/.notmuch-config`` where ```` is + ``.$NOTMUCH_PROFILE`` or "" + +HOOKS +----- + +If ``database.hook_dir`` is unset, notmuch tries (in order) + +- ``$XDG_CONFIG_HOME/notmuch//hooks`` where ```` is + defined by ``$NOTMUCH_PROFILE`` or "default" +- ``/.notmuch/hooks`` SEE ALSO ======== diff --git a/doc/man1/notmuch.rst b/doc/man1/notmuch.rst index fecfd08a..48351588 100644 --- a/doc/man1/notmuch.rst +++ b/doc/man1/notmuch.rst @@ -48,7 +48,9 @@ Supported global options for ``notmuch`` include ``--config=FILE`` Specify the configuration file to use. This overrides any - configuration file specified by ${NOTMUCH\_CONFIG}. + configuration file specified by ${NOTMUCH\_CONFIG}. The empty + string is a permitted and sometimes useful value of *FILE*, which + tells ``notmuch`` to use only configuration metadata from the database. ``--uuid=HEX`` Enforce that the database UUID (a unique identifier which persists diff --git a/doc/man5/notmuch-hooks.rst b/doc/man5/notmuch-hooks.rst index de2ed0c2..c509afb3 100644 --- a/doc/man5/notmuch-hooks.rst +++ b/doc/man5/notmuch-hooks.rst @@ -5,15 +5,15 @@ notmuch-hooks SYNOPSIS ======== -$DATABASEDIR/.notmuch/hooks/* +/{pre-new, post-new, post-insert} DESCRIPTION =========== Hooks are scripts (or arbitrary executables or symlinks to such) that notmuch invokes before and after certain actions. These scripts reside -in the .notmuch/hooks directory within the database directory and must -have executable permissions. +in a directory defined as described in **notmuch-config(1)**. They +must have executable permissions. The currently available hooks are described below. diff --git a/doc/notmuch-emacs.rst b/doc/notmuch-emacs.rst index de47b726..d9b497a3 100644 --- a/doc/notmuch-emacs.rst +++ b/doc/notmuch-emacs.rst @@ -100,7 +100,7 @@ unread mail, but there are several options for customization: The list of saved searches, including names, queries, and additional per-query options. -:index:`notmuch-saved-searches-sort-function` +:index:`notmuch-saved-search-sort-function` This variable controls how saved searches should be sorted. A value of ``nil`` displays the saved searches in the order they are stored in ‘notmuch-saved-searches’. diff --git a/emacs/coolj.el b/emacs/coolj.el index 39a8de2b..d820525b 100644 --- a/emacs/coolj.el +++ b/emacs/coolj.el @@ -1,4 +1,4 @@ -;;; coolj.el --- automatically wrap long lines -*- coding:utf-8 -*- +;;; coolj.el --- automatically wrap long lines -*- lexical-binding: t; coding: utf-8 -*- ;; Copyright (C) 2000, 2001, 2004-2009 Free Software Foundation, Inc. @@ -25,13 +25,13 @@ ;;; Commentary: -;;; This is a simple derivative of some functionality from -;;; `longlines.el'. The key difference is that this version will -;;; insert a prefix at the head of each wrapped line. The prefix is -;;; calculated from the originating long line. +;; This is a simple derivative of some functionality from +;; `longlines.el'. The key difference is that this version will +;; insert a prefix at the head of each wrapped line. The prefix is +;; calculated from the originating long line. -;;; No minor-mode is provided, the caller is expected to call -;;; `coolj-wrap-region' to wrap the region of interest. +;; No minor-mode is provided, the caller is expected to call +;; `coolj-wrap-region' to wrap the region of interest. ;;; Code: @@ -50,9 +50,7 @@ Otherwise respect `fill-column'." :group 'coolj :type 'regexp) -(defvar coolj-wrap-point nil) - -(make-variable-buffer-local 'coolj-wrap-point) +(defvar-local coolj-wrap-point nil) (defun coolj-determine-prefix () "Determine the prefix for the current line." diff --git a/emacs/make-deps.el b/emacs/make-deps.el index a7699fb1..8c9e0a27 100644 --- a/emacs/make-deps.el +++ b/emacs/make-deps.el @@ -1,4 +1,4 @@ -;;; make-deps.el --- compute make dependencies for Elisp sources +;;; make-deps.el --- compute make dependencies for Elisp sources -*- lexical-binding: t -*- ;; ;; Copyright © Austin Clements ;; diff --git a/emacs/notmuch-address.el b/emacs/notmuch-address.el index 8a6d299c..f0af6667 100644 --- a/emacs/notmuch-address.el +++ b/emacs/notmuch-address.el @@ -1,4 +1,4 @@ -;;; notmuch-address.el --- address completion with notmuch +;;; notmuch-address.el --- address completion with notmuch -*- lexical-binding: t -*- ;; ;; Copyright © David Edmondson ;; @@ -25,9 +25,11 @@ (require 'notmuch-parser) (require 'notmuch-lib) (require 'notmuch-company) -;; + (declare-function company-manual-begin "company") +;;; Cache internals + (defvar notmuch-address-last-harvest 0 "Time of last address harvest.") @@ -36,9 +38,9 @@ This variable is set by calling `notmuch-address-harvest'.") (defvar notmuch-address-full-harvest-finished nil - "t indicates that full completion address harvesting has been finished. -Use notmuch-address--harvest-ready to access as that will load a -saved hash if necessary (and available).") + "Whether full completion address harvesting has finished. +Use `notmuch-address--harvest-ready' to access as that will load +a saved hash if necessary (and available).") (defun notmuch-address--harvest-ready () "Return t if there is a full address hash available. @@ -47,24 +49,33 @@ If the hash is not present it attempts to load a saved hash." (or notmuch-address-full-harvest-finished (notmuch-address--load-address-hash))) +;;; Options + (defcustom notmuch-address-command 'internal "Determines how address completion candidates are generated. -If it is a string then that string should be an external program -which must take a single argument (searched string) and output a -list of completion candidates, one per line. +If this is a string, then that string should be an external +program, which must take a single argument (searched string) +and output a list of completion candidates, one per line. + +If this is the symbol `internal', then an implementation is used +that relies on the \"notmuch address\" command, but does not use +any third-party (i.e. \"external\") programs. -Alternatively, it can be the symbol `internal', in which case -internal completion is used; the variable -`notmuch-address-internal-completion' can be used to customize -this case. +If this is the symbol `as-is', then Notmuch does not modify the +value of `message-completion-alist'. This option has to be set to +this value before `notmuch' is loaded, otherwise the modification +to `message-completion-alist' may already have taken place. This +setting obviously does not prevent `message-completion-alist' +from being modified at all; the user or some third-party package +may still modify it. -Finally, if this variable is nil then address completion is -disabled." +Finally, if this is nil, then address completion is disabled." :type '(radio - (const :tag "Use internal address completion" internal) - (const :tag "Disable address completion" nil) - (string :tag "Use external completion command")) + (const :tag "Use internal address completion" internal) + (string :tag "Use external completion command") + (const :tag "Disable address completion" nil) + (const :tag "Use default or third-party mechanism" as-is)) :group 'notmuch-send :group 'notmuch-address :group 'notmuch-external) @@ -133,6 +144,14 @@ matching `notmuch-address-completion-headers-regexp'." :group 'notmuch-address :group 'notmuch-hooks) +(defcustom notmuch-address-use-company t + "If available, use company mode for address completion." + :type 'boolean + :group 'notmuch-send + :group 'notmuch-address) + +;;; Setup + (defun notmuch-address-selection-function (prompt collection initial-input) "Call (`completing-read' PROMPT COLLECTION nil nil INITIAL-INPUT 'notmuch-address-history)" @@ -147,22 +166,14 @@ matching `notmuch-address-completion-headers-regexp'." (defun notmuch-address-message-insinuate () (message "calling notmuch-address-message-insinuate is no longer needed")) -(defcustom notmuch-address-use-company t - "If available, use company mode for address completion." - :type 'boolean - :group 'notmuch-send - :group 'notmuch-address) - (defun notmuch-address-setup () - (let* ((setup-company (and notmuch-address-use-company - (require 'company nil t))) - (pair (cons notmuch-address-completion-headers-regexp - #'notmuch-address-expand-name))) - (when setup-company + (unless (eq notmuch-address-command 'as-is) + (when (and notmuch-address-use-company + (require 'company nil t)) (notmuch-company-setup)) - (unless (member pair message-completion-alist) - (setq message-completion-alist - (push pair message-completion-alist))))) + (cl-pushnew (cons notmuch-address-completion-headers-regexp + #'notmuch-address-expand-name) + message-completion-alist :test #'equal))) (defun notmuch-address-toggle-internal-completion () "Toggle use of internal completion for current buffer. @@ -178,12 +189,14 @@ toggles the setting in this buffer." (kill-local-variable 'company-idle-delay) (setq-local company-idle-delay nil)))) +;;; Completion + (defun notmuch-address-matching (substring) "Returns a list of completion candidates matching SUBSTRING. The candidates are taken from `notmuch-address-completions'." (let ((candidates) (re (regexp-quote substring))) - (maphash (lambda (key val) + (maphash (lambda (key _val) (when (string-match re key) (push key candidates))) notmuch-address-completions) @@ -231,14 +244,8 @@ requiring external commands." (t (funcall notmuch-address-selection-function (format "Address (%s matches): " num-options) - ;; We put the first match as the initial - ;; input; we put all the matches as - ;; possible completions, moving the - ;; first match to the end of the list - ;; makes cursor up/down in the list work - ;; better. - (append (cdr options) (list (car options))) - (car options)))))) + options + orig))))) (if chosen (progn (push chosen notmuch-address-history) @@ -250,31 +257,11 @@ requiring external commands." (ding)))) (t nil))) -;; Copied from `w3m-which-command'. -(defun notmuch-address-locate-command (command) - "Return non-nil if `command' is an executable either on -`exec-path' or an absolute pathname." - (and (stringp command) - (if (and (file-name-absolute-p command) - (file-executable-p command)) - command - (setq command (file-name-nondirectory command)) - (catch 'found-command - (let (bin) - (dolist (dir exec-path) - (setq bin (expand-file-name command dir)) - (when (or (and (file-executable-p bin) - (not (file-directory-p bin))) - (and (file-executable-p (setq bin (concat bin ".exe"))) - (not (file-directory-p bin)))) - (throw 'found-command bin)))))))) +;;; Harvest (defun notmuch-address-harvest-addr (result) - (let ((name-addr (plist-get result :name-addr))) - (puthash name-addr t notmuch-address-completions))) - -(defun notmuch-address-harvest-handle-result (obj) - (notmuch-address-harvest-addr obj)) + (puthash (plist-get result :name-addr) + t notmuch-address-completions)) (defun notmuch-address-harvest-filter (proc string) (when (buffer-live-p (process-buffer proc)) @@ -283,7 +270,7 @@ requiring external commands." (goto-char (point-max)) (insert string)) (notmuch-sexp-parse-partial-list - 'notmuch-address-harvest-handle-result (process-buffer proc))))) + 'notmuch-address-harvest-addr (process-buffer proc))))) (defvar notmuch-address-harvest-procs '(nil . nil) "The currently running harvests. @@ -294,7 +281,7 @@ The car is a partial harvest, and the cdr is a full harvest.") "Collect addresses completion candidates. It queries the notmuch database for messages sent/received (as -configured with `notmuch-address-command`) by the user, collects +configured with `notmuch-address-command') by the user, collects destination/source addresses from those messages and stores them in `notmuch-address-completions'. @@ -394,7 +381,7 @@ to be a saved address hash." (defun notmuch-address--save-address-hash () (when notmuch-address-save-filename (if (or (not (file-exists-p notmuch-address-save-filename)) - ;; The file exists, check it is a file we saved + ;; The file exists, check it is a file we saved. (notmuch-address--get-address-hash)) (with-temp-file notmuch-address-save-filename (let ((save-plist @@ -415,17 +402,16 @@ appear to be an address savefile. Not overwriting." (setq notmuch-address-last-harvest now) (notmuch-address-harvest nil nil - (lambda (proc event) + (lambda (_proc event) ;; If harvest fails, we want to try - ;; again when the trigger is next - ;; called + ;; again when the trigger is next called. (if (string= event "finished\n") (progn (notmuch-address--save-address-hash) (setq notmuch-address-full-harvest-finished t)) (setq notmuch-address-last-harvest 0))))))) -;; +;;; Standalone completion (defun notmuch-address-from-minibuffer (prompt) (if (not notmuch-address-command) @@ -444,7 +430,7 @@ appear to be an address savefile. Not overwriting." (let ((minibuffer-local-map rmap)) (read-string prompt))))) -;; +;;; _ (provide 'notmuch-address) diff --git a/emacs/notmuch-company.el b/emacs/notmuch-company.el index 9ee8ceca..c6a004ae 100644 --- a/emacs/notmuch-company.el +++ b/emacs/notmuch-company.el @@ -32,12 +32,10 @@ ;;; Code: -(eval-when-compile (require 'cl-lib)) - (require 'notmuch-lib) -(defvar notmuch-company-last-prefix nil) -(make-variable-buffer-local 'notmuch-company-last-prefix) +(defvar-local notmuch-company-last-prefix nil) + (declare-function company-begin-backend "company") (declare-function company-grab "company") (declare-function company-mode "company") @@ -55,8 +53,7 @@ ;;;###autoload (defun notmuch-company-setup () (company-mode) - (make-local-variable 'company-backends) - (setq company-backends '(notmuch-company)) + (setq-local company-backends '(notmuch-company)) ;; Disable automatic company completion unless an internal ;; completion method is configured. Company completion (using ;; internal completion) can still be accessed via standard company @@ -103,7 +100,6 @@ (run-hook-with-args 'notmuch-address-post-completion-functions arg)) (no-cache t)))) - (provide 'notmuch-company) ;;; notmuch-company.el ends here diff --git a/emacs/notmuch-compat.el b/emacs/notmuch-compat.el index 3ede6b36..179bf59c 100644 --- a/emacs/notmuch-compat.el +++ b/emacs/notmuch-compat.el @@ -1,4 +1,4 @@ -;;; notmuch-compat.el --- compatibility functions for earlier versions of emacs +;;; notmuch-compat.el --- compatibility functions for earlier versions of emacs -*- lexical-binding: t -*- ;; ;; The functions in this file are copied from more modern versions of ;; emacs and are Copyright (C) 1985-1986, 1992, 1994-1995, 1999-2017 @@ -21,10 +21,11 @@ ;;; Code: -;; emacs master has a bugfix for folding long headers when sending -;; messages. Include the fix for earlier versions of emacs. To avoid -;; interfering with gnus we only run the hook when called from -;; notmuch-message-mode. +;; Before Emacs 26.1 lines that are longer than 998 octets were not. +;; folded. Commit 77bbca8c82f6e553c42abbfafca28f55fc995d00 fixed +;; that. Until we drop support for Emacs 25 we have to backport that +;; fix. To avoid interfering with Gnus we only run the hook when +;; called from notmuch-message-mode. (declare-function mail-header-fold-field "mail-parse" nil) @@ -40,7 +41,17 @@ (unless (fboundp 'message--fold-long-headers) (add-hook 'message-header-hook 'notmuch-message--fold-long-headers)) -;; End of compatibility functions +;; `dlet' isn't available until Emacs 28.1. Below is a copy, with the +;; addition of `with-no-warnings'. +(defmacro notmuch-dlet (binders &rest body) + "Like `let*' but using dynamic scoping." + (declare (indent 1) (debug let)) + `(let (_) + (with-no-warnings ; Quiet "lacks a prefix" warning. + ,@(mapcar (lambda (binder) + `(defvar ,(if (consp binder) (car binder) binder))) + binders)) + (let* ,binders ,@body))) (provide 'notmuch-compat) diff --git a/emacs/notmuch-crypto.el b/emacs/notmuch-crypto.el index 276c9859..db7cb75d 100644 --- a/emacs/notmuch-crypto.el +++ b/emacs/notmuch-crypto.el @@ -1,4 +1,4 @@ -;;; notmuch-crypto.el --- functions for handling display of cryptographic metadata +;;; notmuch-crypto.el --- functions for handling display of cryptographic metadata -*- lexical-binding: t -*- ;; ;; Copyright © Jameson Rollins ;; @@ -26,8 +26,10 @@ (declare-function notmuch-show-get-message-id "notmuch-show" (&optional bare)) +;;; Options + (defcustom notmuch-crypto-process-mime t - "Should cryptographic MIME parts be processed? + "Whether to process cryptographic MIME parts. If this variable is non-nil signatures in multipart/signed messages will be verified and multipart/encrypted parts will be @@ -46,7 +48,7 @@ mode." :group 'notmuch-crypto) (defcustom notmuch-crypto-get-keys-asynchronously t - "Retrieve gpg keys asynchronously." + "Whether to retrieve openpgp keys asynchronously." :type 'boolean :group 'notmuch-crypto) @@ -55,6 +57,8 @@ mode." :type 'string :group 'notmuch-crypto) +;;; Faces + (defface notmuch-crypto-part-header '((((class color) (background dark)) @@ -96,15 +100,16 @@ mode." :group 'notmuch-crypto :group 'notmuch-faces) +;;; Functions + (define-button-type 'notmuch-crypto-status-button-type - 'action (lambda (button) (message (button-get button 'help-echo))) + 'action (lambda (button) (message "%s" (button-get button 'help-echo))) 'follow-link t 'help-echo "Set notmuch-crypto-process-mime to process cryptographic mime parts." :supertype 'notmuch-button-type) (defun notmuch-crypto-insert-sigstatus-button (sigstatus from) - "Insert a button describing the signature status SIGSTATUS sent -by user FROM." + "Insert a button describing the signature status SIGSTATUS sent by user FROM." (let* ((status (plist-get sigstatus :status)) (show-button t) (face 'notmuch-crypto-signature-unknown) @@ -166,7 +171,7 @@ by user FROM." (declare-function notmuch-show-refresh-view "notmuch-show" (&optional reset-state)) (declare-function notmuch-show-get-message-id "notmuch-show" (&optional bare)) -(defun notmuch-crypto--async-key-sentinel (process event) +(defun notmuch-crypto--async-key-sentinel (process _event) "When the user asks for a GPG key to be retrieved asynchronously, handle completion of that task. @@ -260,7 +265,7 @@ corresponding key when the status button is pressed." 'mouse-face 'notmuch-crypto-decryption) (insert "\n")) -;; +;;; _ (provide 'notmuch-crypto) diff --git a/emacs/notmuch-draft.el b/emacs/notmuch-draft.el index 283830ad..a68b7d8d 100644 --- a/emacs/notmuch-draft.el +++ b/emacs/notmuch-draft.el @@ -1,4 +1,4 @@ -;;; notmuch-draft.el --- functions for postponing and editing drafts +;;; notmuch-draft.el --- functions for postponing and editing drafts -*- lexical-binding: t -*- ;; ;; Copyright © Mark Walters ;; Copyright © David Bremner @@ -25,12 +25,18 @@ ;;; Code: +(require 'cl-lib) +(require 'pcase) +(require 'subr-x) + (require 'notmuch-maildir-fcc) (require 'notmuch-tag) (declare-function notmuch-show-get-message-id "notmuch-show" (&optional bare)) (declare-function notmuch-message-mode "notmuch-mua") +;;; Options + (defgroup notmuch-draft nil "Saving and editing drafts in Notmuch." :group 'notmuch) @@ -75,9 +81,11 @@ postponing and resuming a message." :group 'notmuch-send) (defcustom notmuch-draft-save-plaintext 'ask - "Should notmuch save/postpone in plaintext messages that seem -like they are intended to be sent encrypted -(i.e with an mml encryption tag in it)." + "Whether to allow saving plaintext when it seems encryption is intended. +When a message contains mml tags, then that suggest it is +intended to be encrypted. If the user requests that such a +message is saved locally, then this option controls whether +that is allowed. Beside a boolean, this can also be `ask'." :type '(radio (const :tag "Never" nil) (const :tag "Ask every time" ask) @@ -85,13 +93,14 @@ like they are intended to be sent encrypted :group 'notmuch-draft :group 'notmuch-crypto) +;;; Internal + (defvar notmuch-draft-encryption-tag-regex "<#\\(part encrypt\\|secure.*mode=.*encrypt>\\)" "Regular expression matching mml tags indicating encryption of part or message.") -(defvar notmuch-draft-id nil +(defvar-local notmuch-draft-id nil "Message-id of the most recent saved draft of this message.") -(make-variable-buffer-local 'notmuch-draft-id) (defun notmuch-draft--mark-deleted () "Tag the last saved draft deleted. @@ -101,7 +110,7 @@ Used when a new version is saved, or the message is sent." (notmuch-tag notmuch-draft-id '("+deleted")))) (defun notmuch-draft-quote-some-mml () - "Quote the mml tags in `notmuch-draft-quoted-tags`." + "Quote the mml tags in `notmuch-draft-quoted-tags'." (save-excursion ;; First we deal with any secure tag separately. (message-goto-body) @@ -122,7 +131,7 @@ Used when a new version is saved, or the message is sent." (insert "!")))))) (defun notmuch-draft-unquote-some-mml () - "Unquote the mml tags in `notmuch-draft-quoted-tags`." + "Unquote the mml tags in `notmuch-draft-quoted-tags'." (save-excursion (when notmuch-draft-quoted-tags (let ((re (concat "<#!+/?\\(" @@ -136,20 +145,20 @@ Used when a new version is saved, or the message is sent." (let (secure-tag) (save-restriction (message-narrow-to-headers) - (setq secure-tag (message-fetch-field "X-Notmuch-Emacs-Secure" 't)) + (setq secure-tag (message-fetch-field "X-Notmuch-Emacs-Secure" t)) (message-remove-header "X-Notmuch-Emacs-Secure")) (message-goto-body) (when secure-tag (insert secure-tag "\n"))))) (defun notmuch-draft--has-encryption-tag () - "Returns t if there is an mml secure tag." + "Return non-nil if there is an mml secure tag." (save-excursion (message-goto-body) - (re-search-forward notmuch-draft-encryption-tag-regex nil 't))) + (re-search-forward notmuch-draft-encryption-tag-regex nil t))) (defun notmuch-draft--query-encryption () - "Checks if we should save a message that should be encrypted. + "Return non-nil if we should save a message that should be encrypted. `notmuch-draft-save-plaintext' controls the behaviour." (cl-case notmuch-draft-save-plaintext @@ -170,11 +179,13 @@ Really save and index an unencrypted copy? ") ;; but notmuch doesn't want that form, so remove them. (concat "draft-" (substring (message-make-message-id) 1 -1))) +;;; Commands + (defun notmuch-draft-save () "Save the current draft message in the notmuch database. This saves the current message in the database with tags -`notmuch-draft-tags` (in addition to any default tags +`notmuch-draft-tags' (in addition to any default tags applied to newly inserted messages)." (interactive) (when (notmuch-draft--has-encryption-tag) @@ -185,7 +196,7 @@ applied to newly inserted messages)." ;; so that it is easier to search for the message, and the ;; latter so we have a way of accessing the saved message (for ;; example to delete it at a later time). We check that the - ;; user has these in `message-deletable-headers` (the default) + ;; user has these in `message-deletable-headers' (the default) ;; as otherwise they are doing something strange and we ;; shouldn't interfere. Note, since we are doing this in a new ;; buffer we don't change the version in the compose buffer. @@ -208,7 +219,7 @@ applied to newly inserted messages)." (notmuch-draft-quote-some-mml) (notmuch-maildir-setup-message-for-saving) (notmuch-maildir-notmuch-insert-current-buffer - notmuch-draft-folder 't notmuch-draft-tags)) + notmuch-draft-folder t notmuch-draft-tags)) ;; We are now back in the original compose buffer. Note the ;; function notmuch-call-notmuch-process (called by ;; notmuch-maildir-notmuch-insert-current-buffer) signals an error @@ -227,6 +238,7 @@ applied to newly inserted messages)." (defun notmuch-draft-resume (id) "Resume editing of message with id ID." + ;; Used by command `notmuch-show-resume-message'. (let* ((tags (process-lines notmuch-command "search" "--output=tags" "--exclude=false" id)) (draft (equal tags (notmuch-update-tags tags notmuch-draft-tags)))) @@ -266,10 +278,10 @@ applied to newly inserted messages)." ;; message is resaved or sent. (setq notmuch-draft-id (and draft id))))) +;;; _ (add-hook 'message-send-hook 'notmuch-draft--mark-deleted) - (provide 'notmuch-draft) ;;; notmuch-draft.el ends here diff --git a/emacs/notmuch-hello.el b/emacs/notmuch-hello.el index bb60a890..24d2d19e 100644 --- a/emacs/notmuch-hello.el +++ b/emacs/notmuch-hello.el @@ -1,4 +1,4 @@ -;;; notmuch-hello.el --- welcome to notmuch, a frontend +;;; notmuch-hello.el --- welcome to notmuch, a frontend -*- lexical-binding: t -*- ;; ;; Copyright © David Edmondson ;; @@ -21,8 +21,6 @@ ;;; Code: -(eval-when-compile (require 'cl-lib)) - (require 'widget) (require 'wid-edit) ; For `widget-forward'. @@ -38,6 +36,8 @@ (&optional query query-context target buffer-name open-target)) +;;; Options + (defun notmuch-saved-search-get (saved-search field) "Get FIELD from SAVED-SEARCH. @@ -138,8 +138,8 @@ a plist. Supported properties are shown. If not present then the :query property is used. :sort-order Specify the sort order to be used for the search. - Possible values are 'oldest-first 'newest-first or - nil. Nil means use the default sort order. + Possible values are `oldest-first', `newest-first' + or nil. Nil means use the default sort order. :search-type Specify whether to run the search in search-mode, tree mode or unthreaded mode. Set to 'tree to specify tree mode, 'unthreaded to specify unthreaded mode, and set to nil @@ -193,6 +193,8 @@ fields of the search." (defvar notmuch-hello-indent 4 "How much to indent non-headers.") +(defimage notmuch-hello-logo ((:type png :file "notmuch-logo.png"))) + (defcustom notmuch-show-logo t "Should the notmuch logo be shown?" :type 'boolean @@ -282,7 +284,7 @@ International Bureau of Weights and Measures." :group 'notmuch-hello :group 'notmuch-hooks) -(defvar notmuch-hello-url "https://notmuchmail.org" +(defconst notmuch-hello-url "https://notmuchmail.org" "The `notmuch' web site.") (defvar notmuch-hello-custom-section-options @@ -368,50 +370,68 @@ supported for \"Customized queries section\" items." :group 'notmuch-hello :type 'boolean) +;;; Internal variables + (defvar notmuch-hello-hidden-sections nil "List of sections titles whose contents are hidden.") (defvar notmuch-hello-first-run t - "True if `notmuch-hello' is run for the first time, set to nil -afterwards.") - -(defun notmuch-hello-nice-number (n) - (let (result) - (while (> n 0) - (push (% n 1000) result) - (setq n (/ n 1000))) - (setq result (or result '(0))) - (apply #'concat - (number-to-string (car result)) - (mapcar (lambda (elem) - (format "%s%03d" notmuch-hello-thousands-separator elem)) - (cdr result))))) - -(defun notmuch-hello-trim (search) - "Trim whitespace." - (if (string-match "^[[:space:]]*\\(.*[^[:space:]]\\)[[:space:]]*$" search) - (match-string 1 search) - search)) - -(defun notmuch-hello-search (&optional search) - (unless (null search) - (setq search (notmuch-hello-trim search)) - (let ((history-delete-duplicates t)) - (add-to-history 'notmuch-search-history search))) - (notmuch-search search notmuch-search-oldest-first)) - -(defun notmuch-hello-add-saved-search (widget) - (interactive) - (let ((search (widget-value - (symbol-value - (widget-get widget :notmuch-saved-search-widget)))) + "True if `notmuch-hello' is run for the first time, set to nil afterwards.") + +;;; Widgets for inserters + +(define-widget 'notmuch-search-item 'item + "A recent search." + :format "%v\n" + :value-create 'notmuch-search-item-value-create) + +(defun notmuch-search-item-value-create (widget) + (let ((value (widget-get widget :value))) + (widget-insert (make-string notmuch-hello-indent ?\s)) + (widget-create 'editable-field + :size (widget-get widget :size) + :parent widget + :action #'notmuch-hello-search + value) + (widget-insert " ") + (widget-create 'push-button + :parent widget + :notify #'notmuch-hello-add-saved-search + "save") + (widget-insert " ") + (widget-create 'push-button + :parent widget + :notify #'notmuch-hello-delete-search-from-history + "del"))) + +(defun notmuch-search-item-field-width () + (max 8 ; Don't let the search boxes be less than 8 characters wide. + (- (window-width) + notmuch-hello-indent ; space at bol + notmuch-hello-indent ; space at eol + 1 ; for the space before the [save] button + 6 ; for the [save] button + 1 ; for the space before the [del] button + 5))) ; for the [del] button + +;;; Widget actions + +(defun notmuch-hello-search (widget &rest _event) + (let ((search (widget-value widget))) + (when search + (setq search (string-trim search)) + (let ((history-delete-duplicates t)) + (add-to-history 'notmuch-search-history search))) + (notmuch-search search notmuch-search-oldest-first))) + +(defun notmuch-hello-add-saved-search (widget &rest _event) + (let ((search (widget-value (widget-get widget :parent))) (name (completing-read "Name for saved search: " notmuch-saved-searches))) ;; If an existing saved search with this name exists, remove it. (setq notmuch-saved-searches (cl-loop for elem in notmuch-saved-searches - if (not (equal name - (notmuch-saved-search-get elem :name))) + unless (equal name (notmuch-saved-search-get elem :name)) collect elem)) ;; Add the new one. (customize-save-variable 'notmuch-saved-searches @@ -420,15 +440,20 @@ afterwards.") (message "Saved '%s' as '%s'." search name) (notmuch-hello-update))) -(defun notmuch-hello-delete-search-from-history (widget) - (interactive) - (let ((search (widget-value - (symbol-value - (widget-get widget :notmuch-saved-search-widget))))) - (setq notmuch-search-history (delete search - notmuch-search-history)) +(defun notmuch-hello-delete-search-from-history (widget &rest _event) + (when (y-or-n-p "Are you sure you want to delete this search? ") + (let ((search (widget-value (widget-get widget :parent)))) + (setq notmuch-search-history + (delete search notmuch-search-history))) (notmuch-hello-update))) +;;; Button utilities + +;; `notmuch-hello-query-counts', `notmuch-hello-nice-number' and +;; `notmuch-hello-insert-buttons' are used outside this section. +;; All other functions that are defined in this section are only +;; used by these two functions. + (defun notmuch-hello-longest-label (searches-alist) (or (cl-loop for elem in searches-alist maximize (length (notmuch-saved-search-get elem :name))) @@ -453,19 +478,15 @@ diagonal." (cl-loop for row from 0 to (- nrows 1) append (notmuch-hello-reflect-generate-row ncols nrows row list)))) -(defun notmuch-hello-widget-search (widget &rest ignore) - (cond - ((eq (widget-get widget :notmuch-search-type) 'tree) - (notmuch-tree (widget-get widget - :notmuch-search-terms))) - ((eq (widget-get widget :notmuch-search-type) 'unthreaded) - (notmuch-unthreaded (widget-get widget - :notmuch-search-terms))) +(defun notmuch-hello-widget-search (widget &rest _ignore) + (cl-case (widget-get widget :notmuch-search-type) + (tree + (notmuch-tree (widget-get widget :notmuch-search-terms))) + (unthreaded + (notmuch-unthreaded (widget-get widget :notmuch-search-terms))) (t - (notmuch-search (widget-get widget - :notmuch-search-terms) - (widget-get widget - :notmuch-search-oldest-first))))) + (notmuch-search (widget-get widget :notmuch-search-terms) + (widget-get widget :notmuch-search-oldest-first))))) (defun notmuch-saved-search-count (search) (car (process-lines notmuch-command "count" search))) @@ -549,21 +570,31 @@ options will be handled as specified for --batch'. In general we recommend running matching versions of the CLI and emacs interface.")) (goto-char (point-min)) - (notmuch-remove-if-not - #'identity - (mapcar - (lambda (elem) - (let* ((elem-plist (notmuch-hello-saved-search-to-plist elem)) - (search-query (plist-get elem-plist :query)) - (filtered-query (notmuch-hello-filtered-query - search-query (plist-get options :filter))) - (message-count (prog1 (read (current-buffer)) - (forward-line 1)))) - (when (and filtered-query (or (plist-get options :show-empty-searches) - (> message-count 0))) - (setq elem-plist (plist-put elem-plist :query filtered-query)) - (plist-put elem-plist :count message-count)))) - query-list)))) + (cl-mapcan + (lambda (elem) + (let* ((elem-plist (notmuch-hello-saved-search-to-plist elem)) + (search-query (plist-get elem-plist :query)) + (filtered-query (notmuch-hello-filtered-query + search-query (plist-get options :filter))) + (message-count (prog1 (read (current-buffer)) + (forward-line 1)))) + (when (and filtered-query (or (plist-get options :show-empty-searches) + (> message-count 0))) + (setq elem-plist (plist-put elem-plist :query filtered-query)) + (list (plist-put elem-plist :count message-count))))) + query-list))) + +(defun notmuch-hello-nice-number (n) + (let (result) + (while (> n 0) + (push (% n 1000) result) + (setq n (/ n 1000))) + (setq result (or result '(0))) + (apply #'concat + (number-to-string (car result)) + (mapcar (lambda (elem) + (format "%s%03d" notmuch-hello-thousands-separator elem)) + (cdr result))))) (defun notmuch-hello-insert-buttons (searches) "Insert buttons for SEARCHES. @@ -619,7 +650,7 @@ with `notmuch-hello-query-counts'." (unless (eq (% count tags-per-line) 0) (widget-insert "\n")))) -(defimage notmuch-hello-logo ((:type png :file "notmuch-logo.png"))) +;;; Mode (defun notmuch-hello-update () "Update the notmuch-hello buffer." @@ -651,39 +682,19 @@ with `notmuch-hello-query-counts'." ;; Refresh hello as soon as we get back to redisplay. On Emacs ;; 24, we can't do it right here because something in this ;; hook's call stack overrides hello's point placement. + ;; FIXME And on Emacs releases that we still support? (run-at-time nil nil #'notmuch-hello t)) (unless hello-buf ;; Clean up hook (remove-hook 'window-configuration-change-hook #'notmuch-hello-window-configuration-change)))) -;; the following variable is defined as being defconst in notmuch-version.el -(defvar notmuch-emacs-version) - -(defun notmuch-hello-versions () - "Display the notmuch version(s)." - (interactive) - (let ((notmuch-cli-version (notmuch-cli-version))) - (message "notmuch version %s" - (if (string= notmuch-emacs-version notmuch-cli-version) - notmuch-cli-version - (concat notmuch-cli-version - " (emacs mua version " notmuch-emacs-version ")"))))) - (defvar notmuch-hello-mode-map - (let ((map (if (fboundp 'make-composed-keymap) - ;; Inherit both widget-keymap and - ;; notmuch-common-keymap. We have to use - ;; make-sparse-keymap to force this to be a new - ;; keymap (so that when we modify map it does not - ;; modify widget-keymap). - (make-composed-keymap (list (make-sparse-keymap) widget-keymap)) - ;; Before Emacs 24, keymaps didn't support multiple - ;; inheritance,, so just copy the widget keymap since - ;; it's unlikely to change. - (copy-keymap widget-keymap)))) + ;; Inherit both widget-keymap and notmuch-common-keymap. We have + ;; to use make-sparse-keymap to force this to be a new keymap (so + ;; that when we modify map it does not modify widget-keymap). + (let ((map (make-composed-keymap (list (make-sparse-keymap) widget-keymap)))) (set-keymap-parent map notmuch-common-keymap) - (define-key map "v" 'notmuch-hello-versions) (define-key map (kbd "") 'widget-backward) map) "Keymap for \"notmuch hello\" buffers.") @@ -719,18 +730,18 @@ The screen may be customized via `\\[customize]'. Complete list of currently available key bindings: \\{notmuch-hello-mode-map}" - (setq notmuch-buffer-refresh-function #'notmuch-hello-update) - ;;(setq buffer-read-only t) - ) + (setq notmuch-buffer-refresh-function #'notmuch-hello-update)) + +;;; Inserters (defun notmuch-hello-generate-tag-alist (&optional hide-tags) "Return an alist from tags to queries to display in the all-tags section." - (mapcar (lambda (tag) - (cons tag (concat "tag:" (notmuch-escape-boolean-term tag)))) - (notmuch-remove-if-not - (lambda (tag) - (not (member tag hide-tags))) - (process-lines notmuch-command "search" "--output=tags" "*")))) + (cl-mapcan (lambda (tag) + (and (not (member tag hide-tags)) + (list (cons tag + (concat "tag:" + (notmuch-escape-boolean-term tag)))))) + (process-lines notmuch-command "search" "--output=tags" "*"))) (defun notmuch-hello-insert-header () "Insert the default notmuch-hello header." @@ -756,14 +767,14 @@ Complete list of currently available key bindings: (let ((widget-link-prefix "") (widget-link-suffix "")) (widget-create 'link - :notify (lambda (&rest ignore) + :notify (lambda (&rest _ignore) (browse-url notmuch-hello-url)) :help-echo "Visit the notmuch website." "notmuch") (widget-insert ". ") (widget-insert "You have ") (widget-create 'link - :notify (lambda (&rest ignore) + :notify (lambda (&rest _ignore) (notmuch-hello-update)) :help-echo "Refresh" (notmuch-hello-nice-number @@ -782,7 +793,7 @@ Complete list of currently available key bindings: (when searches (widget-insert "Saved searches: ") (widget-create 'push-button - :notify (lambda (&rest ignore) + :notify (lambda (&rest _ignore) (customize-variable 'notmuch-saved-searches)) "edit") (widget-insert "\n\n") @@ -798,73 +809,31 @@ Complete list of currently available key bindings: ;; search boxes. :size (max 8 (- (window-width) notmuch-hello-indent (length "Search: "))) - :action (lambda (widget &rest ignore) - (notmuch-hello-search (widget-value widget)))) + :action #'notmuch-hello-search) ;; Add an invisible dot to make `widget-end-of-line' ignore ;; trailing spaces in the search widget field. A dot is used ;; instead of a space to make `show-trailing-whitespace' ;; happy, i.e. avoid it marking the whole line as trailing ;; spaces. - (widget-insert ".") - (put-text-property (1- (point)) (point) 'invisible t) + (widget-insert (propertize "." 'invisible t)) (widget-insert "\n")) (defun notmuch-hello-insert-recent-searches () "Insert recent searches." (when notmuch-search-history (widget-insert "Recent searches: ") - (widget-create 'push-button - :notify (lambda (&rest ignore) - (when (y-or-n-p "Are you sure you want to clear the searches? ") - (setq notmuch-search-history nil) - (notmuch-hello-update))) - "clear") + (widget-create + 'push-button + :notify (lambda (&rest _ignore) + (when (y-or-n-p "Are you sure you want to clear the searches? ") + (setq notmuch-search-history nil) + (notmuch-hello-update))) + "clear") (widget-insert "\n\n") - (let ((start (point))) - (cl-loop for i from 1 to notmuch-hello-recent-searches-max - for search in notmuch-search-history do - (let ((widget-symbol (intern (format "notmuch-hello-search-%d" i)))) - (set widget-symbol - (widget-create 'editable-field - ;; Don't let the search boxes be - ;; less than 8 characters wide. - :size (max 8 - (- (window-width) - ;; Leave some space - ;; at the start and - ;; end of the - ;; boxes. - (* 2 notmuch-hello-indent) - ;; 1 for the space - ;; before the - ;; `[save]' button. 6 - ;; for the `[save]' - ;; button. - 1 6 - ;; 1 for the space - ;; before the `[del]' - ;; button. 5 for the - ;; `[del]' button. - 1 5)) - :action (lambda (widget &rest ignore) - (notmuch-hello-search (widget-value widget))) - search)) - (widget-insert " ") - (widget-create 'push-button - :notify (lambda (widget &rest ignore) - (notmuch-hello-add-saved-search widget)) - :notmuch-saved-search-widget widget-symbol - "save") - (widget-insert " ") - (widget-create 'push-button - :notify (lambda (widget &rest ignore) - (when (y-or-n-p "Are you sure you want to delete this search? ") - (notmuch-hello-delete-search-from-history widget))) - :notmuch-saved-search-widget widget-symbol - "del")) - (widget-insert "\n")) - (indent-rigidly start (point) notmuch-hello-indent)) - nil)) + (let ((width (notmuch-search-item-field-width))) + (dolist (search (seq-take notmuch-search-history + notmuch-hello-recent-searches-max)) + (widget-create 'notmuch-search-item :value search :size width))))) (defun notmuch-hello-insert-searches (title query-list &rest options) "Insert a section with TITLE showing a list of buttons made from QUERY-LIST. @@ -895,13 +864,13 @@ Supports the following entries in OPTIONS as a plist: (start (point))) (if is-hidden (widget-create 'push-button - :notify `(lambda (widget &rest ignore) + :notify `(lambda (widget &rest _ignore) (setq notmuch-hello-hidden-sections (delete ,title notmuch-hello-hidden-sections)) (notmuch-hello-update)) "show") (widget-create 'push-button - :notify `(lambda (widget &rest ignore) + :notify `(lambda (widget &rest _ignore) (add-to-list 'notmuch-hello-hidden-sections ,title) (notmuch-hello-update)) @@ -950,19 +919,21 @@ following: (widget-insert "Hit `?' for context-sensitive help in any Notmuch screen.\n") (widget-insert "Customize ") (widget-create 'link - :notify (lambda (&rest ignore) + :notify (lambda (&rest _ignore) (customize-group 'notmuch)) :button-prefix "" :button-suffix "" "Notmuch") (widget-insert " or ") (widget-create 'link - :notify (lambda (&rest ignore) + :notify (lambda (&rest _ignore) (customize-variable 'notmuch-hello-sections)) :button-prefix "" :button-suffix "" "this page.") (let ((fill-column (- (window-width) notmuch-hello-indent))) (center-region start (point))))) +;;; Hello! + ;;;###autoload (defun notmuch-hello (&optional no-display) "Run notmuch and display saved searches, known tags, etc." @@ -1014,12 +985,7 @@ following: (run-hooks 'notmuch-hello-refresh-hook) (setq notmuch-hello-first-run nil)) -(defun notmuch-folder () - "Deprecated function for invoking notmuch---calling `notmuch' is preferred now." - (interactive) - (notmuch-hello)) - -;; +;;; _ (provide 'notmuch-hello) diff --git a/emacs/notmuch-jump.el b/emacs/notmuch-jump.el index 1e2d0497..6fab5a79 100644 --- a/emacs/notmuch-jump.el +++ b/emacs/notmuch-jump.el @@ -1,4 +1,4 @@ -;;; notmuch-jump.el --- User-friendly shortcut keys +;;; notmuch-jump.el --- User-friendly shortcut keys -*- lexical-binding: t -*- ;; ;; Copyright © Austin Clements ;; @@ -22,18 +22,9 @@ ;;; Code: -(eval-when-compile - (require 'cl-lib) - (require 'pcase)) - (require 'notmuch-lib) (require 'notmuch-hello) -(eval-and-compile - (unless (fboundp 'window-body-width) - ;; Compatibility for Emacs pre-24 - (defalias 'window-body-width 'window-width))) - ;;;###autoload (defun notmuch-jump-search () "Jump to a saved search by shortcut key. @@ -68,8 +59,8 @@ fast way to jump to a saved search from anywhere in Notmuch." (setq action-map (nreverse action-map)) (if action-map (notmuch-jump action-map "Search: ") - (error "To use notmuch-jump, \ -please customize shortcut keys in notmuch-saved-searches.")))) + (error "To use notmuch-jump, %s" + "please customize shortcut keys in notmuch-saved-searches.")))) (defvar notmuch-jump--action nil) @@ -125,7 +116,7 @@ ACTION-MAP. These strings can be inserted into a tabular buffer." ;; Compute the maximum key description width (let ((key-width 1)) - (pcase-dolist (`(,key ,desc) action-map) + (pcase-dolist (`(,key ,_desc) action-map) (setq key-width (max key-width (string-width (format-kbd-macro key))))) @@ -169,7 +160,7 @@ buffer." "Translate ACTION-MAP into a minibuffer keymap." (let ((map (make-sparse-keymap))) (set-keymap-parent map notmuch-jump-minibuffer-map) - (pcase-dolist (`(,key ,name ,fn) action-map) + (pcase-dolist (`(,key ,_name ,fn) action-map) (when (= (length key) 1) (define-key map key `(lambda () (interactive) @@ -178,7 +169,7 @@ buffer." ;; By doing this in two passes (and checking if we already have a ;; binding) we avoid problems if the user specifies a binding which ;; is a prefix of another binding. - (pcase-dolist (`(,key ,name ,fn) action-map) + (pcase-dolist (`(,key ,_name ,_fn) action-map) (when (> (length key) 1) (let* ((key (elt key 0)) (keystr (string key)) @@ -203,8 +194,6 @@ buffer." (exit-minibuffer))))))) map)) -;; - (provide 'notmuch-jump) ;;; notmuch-jump.el ends here diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el index 118faf1e..c7bb2091 100644 --- a/emacs/notmuch-lib.el +++ b/emacs/notmuch-lib.el @@ -1,4 +1,4 @@ -;;; notmuch-lib.el --- common variables, functions and function declarations +;;; notmuch-lib.el --- common variables, functions and function declarations -*- lexical-binding: t -*- ;; ;; Copyright © Carl Worth ;; @@ -22,6 +22,8 @@ ;;; Code: (require 'cl-lib) +(require 'pcase) +(require 'subr-x) (require 'mm-util) (require 'mm-view) @@ -33,6 +35,8 @@ (defconst notmuch-emacs-version "unknown" "Placeholder variable when notmuch-version.el[c] is not available.")) +;;; Groups + (defgroup notmuch nil "Notmuch mail reader for Emacs." :group 'mail) @@ -78,6 +82,8 @@ "Graphical attributes for displaying text" :group 'notmuch) +;;; Options + (defcustom notmuch-command "notmuch" "Name of the notmuch binary. @@ -97,6 +103,7 @@ search results. Note that any filtered searches created by search." :type 'boolean :group 'notmuch-search) +(make-variable-buffer-local 'notmuch-search-oldest-first) (defcustom notmuch-poll-script nil "[Deprecated] Command to run to incorporate new mail into the notmuch database. @@ -125,11 +132,6 @@ the user's needs: (string :tag "Custom script")) :group 'notmuch-external) -;; - -(defvar notmuch-search-history nil - "Variable to store notmuch searches history.") - (defcustom notmuch-archive-tags '("-inbox") "List of tag changes to apply to a message or a thread when it is archived. @@ -144,9 +146,15 @@ For example, if you wanted to remove an \"inbox\" tag and add an :group 'notmuch-search :group 'notmuch-show) +;;; Variables + +(defvar notmuch-search-history nil + "Variable to store notmuch searches history.") + (defvar notmuch-common-keymap (let ((map (make-sparse-keymap))) (define-key map "?" 'notmuch-help) + (define-key map "v" 'notmuch-version) (define-key map "q" 'notmuch-bury-or-kill-this-buffer) (define-key map "s" 'notmuch-search) (define-key map "t" 'notmuch-search-by-tag) @@ -176,6 +184,8 @@ For example, if you wanted to remove an \"inbox\" tag and add an (select-window (posn-window (event-start last-input-event))) (button-activate button))) +;;; CLI Utilities + (defun notmuch-command-to-string (&rest args) "Synchronously invoke \"notmuch\" with the given list of arguments. @@ -185,8 +195,8 @@ will be signaled. Otherwise the output will be returned." (with-temp-buffer - (let* ((status (apply #'call-process notmuch-command nil t nil args)) - (output (buffer-string))) + (let ((status (apply #'call-process notmuch-command nil t nil args)) + (output (buffer-string))) (notmuch-check-exit-status status (cons notmuch-command args) output) output))) @@ -218,13 +228,31 @@ on the command line, and then retry your notmuch command"))) (match-string 2 long-string) "unknown"))) +(defvar notmuch-emacs-version) + +(defun notmuch-version () + "Display the notmuch version. +The versions of the Emacs package and the `notmuch' executable +should match, but if and only if they don't, then this command +displays both values separately." + (interactive) + (let ((cli-version (notmuch-cli-version))) + (message "notmuch version %s" + (if (string= notmuch-emacs-version cli-version) + cli-version + (concat cli-version + " (emacs mua version " notmuch-emacs-version ")"))))) + +;;; Notmuch Configuration + (defun notmuch-config-get (item) "Return a value from the notmuch configuration." (let* ((val (notmuch-command-to-string "config" "get" item)) (len (length val))) ;; Trim off the trailing newline (if the value is empty or not - ;; configured, there will be no newline) - (if (and (> len 0) (= (aref val (- len 1)) ?\n)) + ;; configured, there will be no newline). + (if (and (> len 0) + (= (aref val (- len 1)) ?\n)) (substring val 0 -1) val))) @@ -247,6 +275,8 @@ on the command line, and then retry your notmuch command"))) (defun notmuch-user-emails () (cons (notmuch-user-primary-email) (notmuch-user-other-email))) +;;; Commands + (defun notmuch-poll () "Run \"notmuch new\" or an external script to import mail. @@ -255,7 +285,7 @@ depending on the value of `notmuch-poll-script'." (interactive) (message "Polling mail...") (if (stringp notmuch-poll-script) - (unless (string= notmuch-poll-script "") + (unless (string-empty-p notmuch-poll-script) (unless (equal (call-process notmuch-poll-script nil nil) 0) (error "Notmuch: poll script `%s' failed!" notmuch-poll-script))) (notmuch-call-notmuch-process "new")) @@ -271,17 +301,7 @@ it, in which case it is killed." (bury-buffer) (kill-buffer))) -(defun notmuch-documentation-first-line (symbol) - "Return the first line of the documentation string for SYMBOL." - (let ((doc (documentation symbol))) - (if doc - (with-temp-buffer - (insert (documentation symbol t)) - (goto-char (point-min)) - (let ((beg (point))) - (end-of-line) - (buffer-substring beg (point)))) - ""))) +;;; Describe Key Bindings (defun notmuch-prefix-key-description (key) "Given a prefix key code, return a human-readable string representation. @@ -293,7 +313,6 @@ This is basically just `format-kbd-macro' but we also convert ESC to M-." "M-" (concat desc " ")))) - (defun notmuch-describe-key (actual-key binding prefix ua-keys tail) "Prepend cons cells describing prefix-arg ACTUAL-KEY and ACTUAL-KEY to TAIL. @@ -315,7 +334,10 @@ It does not prepend if ACTUAL-KEY is already listed in TAIL." (or (and (symbolp binding) (get binding 'notmuch-doc)) (and (functionp binding) - (notmuch-documentation-first-line binding)))) + (let ((doc (documentation binding))) + (and doc + (string-match "\\`.+" doc) + (match-string 0 doc)))))) tail))) tail) @@ -396,9 +418,9 @@ A command that supports a prefix argument can explicitly document its prefixed behavior by setting the 'notmuch-prefix-doc property of its command symbol." (interactive) - (let* ((mode major-mode) - (doc (substitute-command-keys - (notmuch-substitute-command-keys (documentation mode t))))) + (let ((doc (substitute-command-keys + (notmuch-substitute-command-keys + (documentation major-mode t))))) (with-current-buffer (generate-new-buffer "*notmuch-help*") (insert doc) (goto-char (point-min)) @@ -428,9 +450,10 @@ of its command symbol." (insert desc))) (pop-to-buffer (help-buffer))))) -(defvar notmuch-buffer-refresh-function nil +;;; Refreshing Buffers + +(defvar-local notmuch-buffer-refresh-function nil "Function to call to refresh the current buffer.") -(make-variable-buffer-local 'notmuch-buffer-refresh-function) (defun notmuch-refresh-this-buffer () "Refresh the current buffer." @@ -460,9 +483,11 @@ be displayed." (with-current-buffer buffer (notmuch-refresh-this-buffer)))))) +;;; String Utilities + (defun notmuch-prettify-subject (subject) - ;; This function is used by `notmuch-search-process-filter' which - ;; requires that we not disrupt its' matching state. + ;; This function is used by `notmuch-search-process-filter', + ;; which requires that we not disrupt its matching state. (save-match-data (if (and subject (string-match "^[ \t]*$" subject)) @@ -503,8 +528,6 @@ This replaces spaces, percents, and double quotes in STR with (replace-regexp-in-string "[ %\"]" (lambda (match) (format "%%%02x" (aref match 0))) str)) -;; - (defun notmuch-common-do-stash (text) "Common function to stash text in kill ring, and display in minibuffer." (if text @@ -516,39 +539,32 @@ This replaces spaces, percents, and double quotes in STR with (kill-new "") (message "Nothing to stash!"))) -;; - -(defun notmuch-remove-if-not (predicate list) - "Return a copy of LIST with all items not satisfying PREDICATE removed." - (let (out) - (while list - (when (funcall predicate (car list)) - (push (car list) out)) - (setq list (cdr list))) - (nreverse out))) +;;; Generic Utilities (defun notmuch-plist-delete (plist property) - (let* ((xplist (cons nil plist)) - (pred xplist)) - (while (cdr pred) - (when (eq (cadr pred) property) - (setcdr pred (cdddr pred))) - (setq pred (cddr pred))) - (cdr xplist))) - -(defun notmuch-split-content-type (content-type) - "Split content/type into 'content' and 'type'." - (split-string content-type "/")) + (let (p) + (while plist + (unless (eq property (car plist)) + (setq p (plist-put p (car plist) (cadr plist)))) + (setq plist (cddr plist))) + p)) + +;;; MML Utilities (defun notmuch-match-content-type (t1 t2) - "Return t if t1 and t2 are matching content types, taking wildcards into account." - (let ((st1 (notmuch-split-content-type t1)) - (st2 (notmuch-split-content-type t2))) - (if (or (string= (cadr st1) "*") - (string= (cadr st2) "*")) - ;; Comparison of content types should be case insensitive. - (string= (downcase (car st1)) (downcase (car st2))) - (string= (downcase t1) (downcase t2))))) + "Return t if t1 and t2 are matching content types. +Take wildcards into account." + (and (stringp t1) + (stringp t2) + (let ((st1 (split-string t1 "/")) + (st2 (split-string t2 "/"))) + (if (or (string= (cadr st1) "*") + (string= (cadr st2) "*")) + ;; Comparison of content types should be case insensitive. + (string= (downcase (car st1)) + (downcase (car st2))) + (string= (downcase t1) + (downcase t2)))))) (defvar notmuch-multipart/alternative-discouraged '(;; Avoid HTML parts. @@ -653,18 +669,6 @@ If CACHE is non-nil, the content of this part will be saved in MSG (if it isn't already)." (notmuch--get-bodypart-raw msg part process-crypto nil cache)) -;; Workaround: The call to `mm-display-part' below triggers a bug in -;; Emacs 24 if it attempts to use the shr renderer to display an HTML -;; part with images in it (demonstrated in 24.1 and 24.2 on Debian and -;; Fedora 17, though unreproducible in other configurations). -;; `mm-shr' references the variable `gnus-inhibit-images' without -;; first loading gnus-art, which defines it, resulting in a -;; void-variable error. Hence, we advise `mm-shr' to ensure gnus-art -;; is loaded. -(define-advice mm-shr (:before (_handle) notmuch--load-gnus-args) - "Require `gnus-art' since we use its variables." - (require 'gnus-art nil t)) - (defun notmuch-mm-display-part-inline (msg part content-type process-crypto) "Use the mm-decode/mm-view functions to display a part in the current buffer, if possible." @@ -692,6 +696,8 @@ current buffer, if possible." (mm-display-part handle) t)))))) +;;; Generic Utilities + ;; Converts a plist of headers to an alist of headers. The input plist should ;; have symbols of the form :Header as keys, and the resulting alist will have ;; symbols of the form 'Header as keys. @@ -758,6 +764,8 @@ returned by FUNC." (put-text-property start next prop (funcall func value) object) (setq start next)))) +;;; Running Notmuch + (defun notmuch-logged-error (msg &optional extra) "Log MSG and EXTRA to *Notmuch errors* and signal MSG. @@ -819,20 +827,27 @@ You may need to restart Emacs or upgrade your notmuch Emacs package.")) Emacs requested a newer output format than supported by the notmuch CLI. You may need to restart Emacs or upgrade your notmuch package.")) (t - (let* ((command-string - (mapconcat (lambda (arg) - (shell-quote-argument - (cond ((stringp arg) arg) - ((symbolp arg) (symbol-name arg)) - (t "*UNKNOWN ARGUMENT*")))) - command " ")) - (extra - (concat "command: " command-string "\n" - (if (integerp exit-status) - (format "exit status: %s\n" exit-status) - (format "exit signal: %s\n" exit-status)) - (and err (concat "stderr:\n" err)) - (and output (concat "stdout:\n" output))))) + (pcase-let* + ((`(,command . ,args) command) + (command (if (equal (file-name-nondirectory command) + notmuch-command) + notmuch-command + command)) + (command-string + (mapconcat (lambda (arg) + (shell-quote-argument + (cond ((stringp arg) arg) + ((symbolp arg) (symbol-name arg)) + (t "*UNKNOWN ARGUMENT*")))) + (cons command args) + " ")) + (extra + (concat "command: " command-string "\n" + (if (integerp exit-status) + (format "exit status: %s\n" exit-status) + (format "exit signal: %s\n" exit-status)) + (and err (concat "stderr:\n" err)) + (and output (concat "stdout:\n" output))))) (if err ;; We have an error message straight from the CLI. (notmuch-logged-error @@ -840,7 +855,7 @@ You may need to restart Emacs or upgrade your notmuch package.")) ;; We only have combined output from the CLI; don't inundate ;; the user with it. Mimic `process-lines'. (notmuch-logged-error (format "%s exited with status %s" - (car command) exit-status) + command exit-status) extra)) ;; `notmuch-logged-error' does not return. )))) @@ -915,56 +930,29 @@ when the process exits, or nil for none. The caller must *not* invoke `set-process-sentinel' directly on the returned process, as that will interfere with the handling of stderr and the exit status." - (let (err-file err-buffer proc err-proc - ;; Find notmuch using Emacs' `exec-path' - (command (or (executable-find notmuch-command) - (error "Command not found: %s" notmuch-command)))) - (if (fboundp 'make-process) - (progn - (setq err-buffer (generate-new-buffer " *notmuch-stderr*")) - ;; Emacs 25 and newer has `make-process', which allows - ;; redirecting stderr independently from stdout to a - ;; separate buffer. As this allows us to avoid using a - ;; temporary file and shell invocation, use it when - ;; available. - (setq proc (make-process - :name name - :buffer buffer - :command (cons command args) - :connection-type 'pipe - :stderr err-buffer)) - (setq err-proc (get-buffer-process err-buffer)) - (process-put proc 'err-buffer err-buffer) - - (process-put err-proc 'err-file err-file) - (process-put err-proc 'err-buffer err-buffer) - (set-process-sentinel err-proc #'notmuch-start-notmuch-error-sentinel)) - ;; On Emacs versions before 25, there is no way to capture - ;; stdout and stderr separately for asynchronous processes, or - ;; even to redirect stderr to a file, so we use a trivial shell - ;; wrapper to send stderr to a temporary file and clean things - ;; up in the sentinel. - (setq err-file (make-temp-file "nmerr")) - (let ((process-connection-type nil)) ;; Use a pipe - (setq proc (apply #'start-process name buffer - "/bin/sh" "-c" - "exec 2>\"$1\"; shift; exec \"$0\" \"$@\"" - command err-file args))) - (process-put proc 'err-file err-file)) + (let* ((command (or (executable-find notmuch-command) + (error "Command not found: %s" notmuch-command))) + (err-buffer (generate-new-buffer " *notmuch-stderr*")) + (proc (make-process + :name name + :buffer buffer + :command (cons command args) + :connection-type 'pipe + :stderr err-buffer)) + (err-proc (get-buffer-process err-buffer))) + (process-put proc 'err-buffer err-buffer) (process-put proc 'sub-sentinel sentinel) - (process-put proc 'real-command (cons notmuch-command args)) (set-process-sentinel proc #'notmuch-start-notmuch-sentinel) + (set-process-sentinel err-proc #'notmuch-start-notmuch-error-sentinel) proc)) (defun notmuch-start-notmuch-sentinel (proc event) "Process sentinel function used by `notmuch-start-notmuch'." - (let* ((err-file (process-get proc 'err-file)) - (err-buffer (or (process-get proc 'err-buffer) - (find-file-noselect err-file))) - (err (and (not (zerop (buffer-size err-buffer))) + (let* ((err-buffer (process-get proc 'err-buffer)) + (err (and (buffer-live-p err-buffer) + (not (zerop (buffer-size err-buffer))) (with-current-buffer err-buffer (buffer-string)))) - (sub-sentinel (process-get proc 'sub-sentinel)) - (real-command (process-get proc 'real-command))) + (sub-sentinel (process-get proc 'sub-sentinel))) (condition-case err (progn ;; Invoke the sub-sentinel, if any @@ -976,7 +964,7 @@ status." ;; and there's no point in telling the user that (but we ;; still check for and report stderr output below). (when (buffer-live-p (process-buffer proc)) - (notmuch-check-async-exit-status proc event real-command err)) + (notmuch-check-async-exit-status proc event nil err)) ;; If that didn't signal an error, then any error output was ;; really warning output. Show warnings, if any. (let ((warnings @@ -996,21 +984,17 @@ status." (error ;; Emacs behaves strangely if an error escapes from a sentinel, ;; so turn errors into messages. - (message "%s" (error-message-string err)))) - (when err-file (ignore-errors (delete-file err-file))))) - -(defun notmuch-start-notmuch-error-sentinel (proc event) - (let* ((err-file (process-get proc 'err-file)) - ;; When `make-process' is available, use the error buffer - ;; associated with the process, otherwise the error file. - (err-buffer (or (process-get proc 'err-buffer) - (find-file-noselect err-file)))) - (when err-buffer (kill-buffer err-buffer)))) - -;; This variable is used only buffer local, but it needs to be -;; declared globally first to avoid compiler warnings. -(defvar notmuch-show-process-crypto nil) -(make-variable-buffer-local 'notmuch-show-process-crypto) + (message "%s" (error-message-string err)))))) + +(defun notmuch-start-notmuch-error-sentinel (proc _event) + (unless (process-live-p proc) + (let ((buffer (process-buffer proc))) + (when (buffer-live-p buffer) + (kill-buffer buffer))))) + +(defvar-local notmuch-show-process-crypto nil) + +;;; Generic Utilities (defun notmuch-interactive-region () "Return the bounds of the current interactive region. @@ -1026,6 +1010,8 @@ region if the region is active, or both `point' otherwise." 'notmuch-interactive-region "notmuch 0.29") +;;; _ + (provide 'notmuch-lib) ;;; notmuch-lib.el ends here diff --git a/emacs/notmuch-maildir-fcc.el b/emacs/notmuch-maildir-fcc.el index a9103a20..c715532b 100644 --- a/emacs/notmuch-maildir-fcc.el +++ b/emacs/notmuch-maildir-fcc.el @@ -1,4 +1,4 @@ -;;; notmuch-maildir-fcc.el --- inserting using a fcc handler +;;; notmuch-maildir-fcc.el --- inserting using a fcc handler -*- lexical-binding: t -*- ;; Copyright © Jesse Rosenthal ;; @@ -21,7 +21,7 @@ ;;; Code: -(eval-when-compile (require 'cl-lib)) +(require 'seq) (require 'message) @@ -29,6 +29,8 @@ (defvar notmuch-maildir-fcc-count 0) +;;; Options + (defcustom notmuch-fcc-dirs "sent" "Determines the Fcc Header which says where to save outgoing mail. @@ -76,23 +78,20 @@ directory if it does not exist yet when sending a mail." :require 'notmuch-fcc-initialization :group 'notmuch-send) -(defcustom notmuch-maildir-use-notmuch-insert 't +(defcustom notmuch-maildir-use-notmuch-insert t "Should fcc use notmuch insert instead of simple fcc." :type '(choice :tag "Fcc Method" (const :tag "Use notmuch insert" t) (const :tag "Use simple fcc" nil)) :group 'notmuch-send) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Functions which set up the fcc header in the message buffer. +;;; Functions which set up the fcc header in the message buffer. (defun notmuch-fcc-header-setup () "Add an Fcc header to the current message buffer. -Sets the Fcc header based on the values of `notmuch-fcc-dirs'. - -Originally intended to be use a hook function, but now called directly -by notmuch-mua-mail." +If the Fcc header is already set, then keep it as-is. +Otherwise set it according to `notmuch-fcc-dirs'." (let ((subdir (cond ((or (not notmuch-fcc-dirs) @@ -106,16 +105,13 @@ by notmuch-mua-mail." ;; Old style - no longer works. (error "Invalid `notmuch-fcc-dirs' setting (old style)")) ((listp notmuch-fcc-dirs) - (let* ((from (message-field-value "From")) - (match - (catch 'first-match - (dolist (re-folder notmuch-fcc-dirs) - (when (string-match-p (car re-folder) from) - (throw 'first-match re-folder)))))) - (if match - (cdr match) - (message "No Fcc header added.") - nil))) + (or (seq-some (let ((from (message-field-value "From"))) + (pcase-lambda (`(,regexp . ,folder)) + (and (string-match-p regexp from) + folder))) + notmuch-fcc-dirs) + (progn (message "No Fcc header added.") + nil))) (t (error "Invalid `notmuch-fcc-dirs' setting (neither string nor list)"))))) (when subdir @@ -127,9 +123,9 @@ by notmuch-mua-mail." ;; Notmuch insert does not accept absolute paths, so check the user ;; really want this header inserted. (when (or (not (= (elt subdir 0) ?/)) - (y-or-n-p - (format "Fcc header %s is an absolute path and notmuch insert is requested. -Insert header anyway? " subdir))) + (y-or-n-p (format "Fcc header %s is an absolute path %s %s" subdir + "and notmuch insert is requested." + "Insert header anyway? "))) (message-add-header (concat "Fcc: " subdir)))) (defun notmuch-maildir-add-file-style-fcc-header (subdir) @@ -142,9 +138,7 @@ Insert header anyway? " subdir))) subdir (concat (notmuch-database-path) "/" subdir)))))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Functions for saving a message either using notmuch insert or file -;; fcc. First functions common to the two cases. +;;; Functions for saving a message using either method. (defmacro with-temporary-notmuch-message-buffer (&rest body) "Set-up a temporary copy of the current message-mode buffer." @@ -157,8 +151,9 @@ Insert header anyway? " subdir))) ,@body))) (defun notmuch-maildir-setup-message-for-saving () - "Setup message for saving. Should be called on a temporary copy. + "Setup message for saving. +This should be called on a temporary copy. This is taken from the function message-do-fcc." (message-encode-message-body) (save-restriction @@ -174,7 +169,7 @@ This is taken from the function message-do-fcc." "Process Fcc headers in the current buffer. This is a rearranged version of message mode's message-do-fcc." - (let (list file) + (let (files file) (save-excursion (save-restriction (message-narrow-to-headers) @@ -184,28 +179,26 @@ This is a rearranged version of message mode's message-do-fcc." (save-restriction (message-narrow-to-headers) (while (setq file (message-fetch-field "fcc" t)) - (push file list) + (push file files) (message-remove-header "fcc" nil t))) (notmuch-maildir-setup-message-for-saving) ;; Process FCC operations. - (while list - (setq file (pop list)) - (notmuch-fcc-handler file)) + (mapc #'notmuch-fcc-handler files) (kill-buffer (current-buffer))))))) (defun notmuch-fcc-handler (fcc-header) "Store message with notmuch insert or normal (file) fcc. -If `notmuch-maildir-use-notmuch-insert` is set then store the +If `notmuch-maildir-use-notmuch-insert' is set then store the message using notmuch insert. Otherwise store the message using normal fcc." (message "Doing Fcc...") (if notmuch-maildir-use-notmuch-insert (notmuch-maildir-fcc-with-notmuch-insert fcc-header) - (notmuch-maildir-fcc-file-fcc fcc-header))) + (notmuch-maildir-fcc-file-fcc fcc-header)) + (message "Doing Fcc...done")) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Functions for saving a message using notmuch insert. +;;; Functions for saving a message using notmuch insert. (defun notmuch-maildir-notmuch-insert-current-buffer (folder &optional create tags) "Use notmuch insert to put the current buffer in the database. @@ -214,11 +207,11 @@ This inserts the current buffer as a message into the notmuch database in folder FOLDER. If CREATE is non-nil it will supply the --create-folder flag to create the folder if necessary. TAGS should be a list of tag changes to apply to the inserted message." - (let* ((args (append (and create (list "--create-folder")) - (list (concat "--folder=" folder)) - tags))) - (apply 'notmuch-call-notmuch-process - :stdin-string (buffer-string) "insert" args))) + (apply 'notmuch-call-notmuch-process + :stdin-string (buffer-string) "insert" + (append (and create (list "--create-folder")) + (list (concat "--folder=" folder)) + tags))) (defun notmuch-maildir-fcc-with-notmuch-insert (fcc-header &optional create) "Store message with notmuch insert. @@ -232,9 +225,8 @@ quoting each space with an immediately preceding backslash or surrounding the entire folder name in double quotes. If CREATE is non-nil then create the folder if necessary." - (let* ((args (split-string-and-unquote fcc-header)) - (folder (car args)) - (tags (cdr args))) + (pcase-let ((`(,folder . ,tags) + (split-string-and-unquote fcc-header))) (condition-case nil (notmuch-maildir-notmuch-insert-current-buffer folder create tags) ;; Since there are many reasons notmuch insert could fail, e.g., @@ -246,14 +238,12 @@ If CREATE is non-nil then create the folder if necessary." \(r)etry, (c)reate folder, (i)gnore, or (e)dit the header? " '(?r ?c ?i ?e)))) (cl-case response (?r (notmuch-maildir-fcc-with-notmuch-insert fcc-header)) - (?c (notmuch-maildir-fcc-with-notmuch-insert fcc-header 't)) - (?i 't) + (?c (notmuch-maildir-fcc-with-notmuch-insert fcc-header t)) + (?i t) (?e (notmuch-maildir-fcc-with-notmuch-insert (read-from-minibuffer "Fcc header: " fcc-header))))))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Functions for saving a message using file fcc. +;;; Functions for saving a message using file fcc. (defun notmuch-maildir-fcc-host-fixer (hostname) (replace-regexp-in-string "/\\|:" @@ -269,7 +259,7 @@ If CREATE is non-nil then create the folder if necessary." (let* ((ftime (float-time)) (microseconds (mod (* 1000000 ftime) 1000000)) (hostname (notmuch-maildir-fcc-host-fixer (system-name)))) - (setq notmuch-maildir-fcc-count (+ notmuch-maildir-fcc-count 1)) + (cl-incf notmuch-maildir-fcc-count) (format "%d.%d_%d_%d.%s" ftime (emacs-pid) @@ -302,9 +292,7 @@ if successful, nil if not." (write-file (concat destdir "/tmp/" msg-id)) msg-id) (t - (error (format "Can't write to %s. Not a maildir." - destdir)) - nil)))) + (error "Can't write to %s. Not a maildir." destdir))))) (defun notmuch-maildir-fcc-move-tmp-to-new (destdir msg-id) (add-name-to-file @@ -319,10 +307,10 @@ if successful, nil if not." (defun notmuch-maildir-fcc-file-fcc (fcc-header) "Write the message to the file specified by FCC-HEADER. -It offers the user a chance to correct the header, or filesystem, -if needed." +If that fails, then offer the user a chance to correct the header +or filesystem." (if (notmuch-maildir-fcc-dir-is-maildir-p fcc-header) - (notmuch-maildir-fcc-write-buffer-to-maildir fcc-header 't) + (notmuch-maildir-fcc-write-buffer-to-maildir fcc-header t) ;; The fcc-header is not a valid maildir see if the user wants to ;; fix it in some way. (let* ((prompt (format "Fcc %s is not a maildir: \ @@ -335,33 +323,33 @@ if needed." (message "No permission to create %s." fcc-header) (sit-for 2)) (notmuch-maildir-fcc-file-fcc fcc-header)) - (?i 't) + (?i t) (?e (notmuch-maildir-fcc-file-fcc (read-from-minibuffer "Fcc header: " fcc-header))))))) (defun notmuch-maildir-fcc-write-buffer-to-maildir (destdir &optional mark-seen) - "Writes the current buffer to maildir destdir. If mark-seen is -non-nil, it will write it to cur/, and mark it as read. It should -return t if successful, and nil otherwise." + "Write the current buffer to maildir destdir. + +If mark-seen is non-nil, then write it to \"cur/\", and mark it +as read, otherwise write it to \"new/\". Return t if successful, +and nil otherwise." (let ((orig-buffer (buffer-name))) (with-temp-buffer (insert-buffer-substring orig-buffer) (catch 'link-error (let ((msg-id (notmuch-maildir-fcc-save-buffer-to-tmp destdir))) (when msg-id - (cond (mark-seen - (condition-case err - (notmuch-maildir-fcc-move-tmp-to-cur destdir msg-id t) - (file-already-exists - (throw 'link-error nil)))) - (t - (condition-case err - (notmuch-maildir-fcc-move-tmp-to-new destdir msg-id) - (file-already-exists - (throw 'link-error nil)))))) + (condition-case nil + (if mark-seen + (notmuch-maildir-fcc-move-tmp-to-cur destdir msg-id t) + (notmuch-maildir-fcc-move-tmp-to-new destdir msg-id)) + (file-already-exists + (throw 'link-error nil)))) (delete-file (concat destdir "/tmp/" msg-id)))) t))) +;;; _ + (provide 'notmuch-maildir-fcc) ;;; notmuch-maildir-fcc.el ends here diff --git a/emacs/notmuch-message.el b/emacs/notmuch-message.el index c2242070..0856a2e9 100644 --- a/emacs/notmuch-message.el +++ b/emacs/notmuch-message.el @@ -1,4 +1,4 @@ -;;; notmuch-message.el --- message-mode functions specific to notmuch +;;; notmuch-message.el --- message-mode functions specific to notmuch -*- lexical-binding: t -*- ;; ;; Copyright © Jesse Rosenthal ;; @@ -21,6 +21,10 @@ ;;; Code: +(require 'cl-lib) +(require 'pcase) +(require 'subr-x) + (require 'message) (require 'notmuch-tag) @@ -50,21 +54,20 @@ the \"inbox\" tag, you would set: :type '(repeat string) :group 'notmuch-send) -(defconst notmuch-message-queued-tag-changes nil - "List of messages and corresponding tag-changes to be applied when sending a message. +(defvar-local notmuch-message-queued-tag-changes nil + "List of tag changes to be applied when sending a message. -This variable is overridden by buffer-local versions in message -buffers where tag changes should be triggered when sending off -the message. Each item in this list is a list of strings, where -the first is a notmuch query and the rest are the tag changes to -be applied to the matching messages.") +A list of queries and tag changes that are to be applied to them +when the message that was composed in the current buffer is being +send. Each item in this list is a list of strings, where the +first is a notmuch query and the rest are the tag changes to be +applied to the matching messages.") (defun notmuch-message-apply-queued-tag-changes () ;; Apply the tag changes queued in the buffer-local variable ;; notmuch-message-queued-tag-changes. - (dolist (query-and-tags notmuch-message-queued-tag-changes) - (notmuch-tag (car query-and-tags) - (cdr query-and-tags)))) + (pcase-dolist (`(,query . ,tags) notmuch-message-queued-tag-changes) + (notmuch-tag query tags))) (add-hook 'message-send-hook 'notmuch-message-apply-queued-tag-changes) diff --git a/emacs/notmuch-mua.el b/emacs/notmuch-mua.el index 03c7cc97..bbf059a2 100644 --- a/emacs/notmuch-mua.el +++ b/emacs/notmuch-mua.el @@ -1,4 +1,4 @@ -;;; notmuch-mua.el --- emacs style mail-user-agent +;;; notmuch-mua.el --- emacs style mail-user-agent -*- lexical-binding: t -*- ;; ;; Copyright © David Edmondson ;; @@ -21,8 +21,6 @@ ;;; Code: -(eval-when-compile (require 'cl-lib)) - (require 'message) (require 'mm-view) (require 'format-spec) @@ -38,7 +36,12 @@ (declare-function notmuch-draft-postpone "notmuch-draft" ()) (declare-function notmuch-draft-save "notmuch-draft" ()) -;; +(defvar notmuch-show-indent-multipart) +(defvar notmuch-show-insert-header-p-function) +(defvar notmuch-show-max-text-part-size) +(defvar notmuch-show-insert-text/plain-hook) + +;;; Options (defcustom notmuch-mua-send-hook nil "Hook run before sending messages." @@ -47,8 +50,7 @@ :group 'notmuch-hooks) (defcustom notmuch-mua-compose-in 'current-window - (concat - "Where to create the mail buffer used to compose a new message. + "Where to create the mail buffer used to compose a new message. Possible values are `current-window' (default), `new-window' and `new-frame'. If set to `current-window', the mail buffer will be displayed in the current window, so the old buffer will be @@ -57,18 +59,14 @@ or `new-frame', the mail buffer will be displayed in a new window/frame that will be destroyed when the buffer is killed. You may want to customize `message-kill-buffer-on-exit' accordingly." - (when (< emacs-major-version 24) - " Due to a known bug in Emacs 23, you should not set -this to `new-window' if `message-kill-buffer-on-exit' is -disabled: this would result in an incorrect behavior.")) :group 'notmuch-send :type '(choice (const :tag "Compose in the current window" current-window) (const :tag "Compose mail in a new window" new-window) (const :tag "Compose mail in a new frame" new-frame))) (defcustom notmuch-mua-user-agent-function nil - "Function used to generate a `User-Agent:' string. If this is -`nil' then no `User-Agent:' will be generated." + "Function used to generate a `User-Agent:' string. +If this is `nil' then no `User-Agent:' will be generated." :type '(choice (const :tag "No user agent string" nil) (const :tag "Full" notmuch-mua-user-agent-full) (const :tag "Notmuch" notmuch-mua-user-agent-notmuch) @@ -78,20 +76,36 @@ disabled: this would result in an incorrect behavior.")) :group 'notmuch-send) (defcustom notmuch-mua-hidden-headers nil - "Headers that are added to the `message-mode' hidden headers -list." + "Headers that are added to the `message-mode' hidden headers list." + :type '(repeat string) + :group 'notmuch-send) + +(defcustom notmuch-identities nil + "Identities that can be used as the From: address when composing a new message. + +If this variable is left unset, then a list will be constructed from the +name and addresses configured in the notmuch configuration file." :type '(repeat string) :group 'notmuch-send) +(defcustom notmuch-always-prompt-for-sender nil + "Always prompt for the From: address when composing or forwarding a message. + +This is not taken into account when replying to a message, because in that case +the From: header is already filled in by notmuch." + :type 'boolean + :group 'notmuch-send) + (defgroup notmuch-reply nil - "Replying to messages in notmuch" + "Replying to messages in notmuch." :group 'notmuch) (defcustom notmuch-mua-cite-function 'message-cite-original - "*Function for citing an original message. + "Function for citing an original message. + Predefined functions include `message-cite-original' and -`message-cite-original-without-signature'. -Note that these functions use `mail-citation-hook' if that is non-nil." +`message-cite-original-without-signature'. Note that these +functions use `mail-citation-hook' if that is non-nil." :type '(radio (function-item message-cite-original) (function-item message-cite-original-without-signature) (function-item sc-cite-original) @@ -125,12 +139,13 @@ to `notmuch-mua-send-hook'." :type 'regexp :group 'notmuch-send) -;; +;;; Various functions (defun notmuch-mua-attachment-check () - "Signal an error if the message text indicates that an -attachment is expected but no MML referencing an attachment is -found. + "Signal an error an attachement is expected but missing. + +Signal an error if the message text indicates that an attachment +is expected but no MML referencing an attachment is found. Typically this is added to `notmuch-mua-send-hook'." (when (and @@ -163,17 +178,14 @@ Typically this is added to `notmuch-mua-send-hook'." (defun notmuch-mua-get-switch-function () "Get a switch function according to `notmuch-mua-compose-in'." - (cond ((eq notmuch-mua-compose-in 'current-window) - 'switch-to-buffer) - ((eq notmuch-mua-compose-in 'new-window) - 'switch-to-buffer-other-window) - ((eq notmuch-mua-compose-in 'new-frame) - 'switch-to-buffer-other-frame) - (t (error "Invalid value for `notmuch-mua-compose-in'")))) + (pcase notmuch-mua-compose-in + ('current-window 'switch-to-buffer) + ('new-window 'switch-to-buffer-other-window) + ('new-frame 'switch-to-buffer-other-frame) + (_ (error "Invalid value for `notmuch-mua-compose-in'")))) (defun notmuch-mua-maybe-set-window-dedicated () - "Set the selected window as dedicated according to -`notmuch-mua-compose-in'." + "Set the selected window as dedicated according to `notmuch-mua-compose-in'." (when (or (eq notmuch-mua-compose-in 'new-frame) (eq notmuch-mua-compose-in 'new-window)) (set-window-dedicated-p (selected-window) t))) @@ -205,14 +217,13 @@ Typically this is added to `notmuch-mua-send-hook'." (defun notmuch-mua-reply-crypto (parts) "Add mml sign-encrypt flag if any part of original message is encrypted." (cl-loop for part in parts - if (notmuch-match-content-type (plist-get part :content-type) - "multipart/encrypted") + for type = (plist-get part :content-type) + if (notmuch-match-content-type type "multipart/encrypted") do (mml-secure-message-sign-encrypt) - else if (notmuch-match-content-type (plist-get part :content-type) - "multipart/*") + else if (notmuch-match-content-type type "multipart/*") do (notmuch-mua-reply-crypto (plist-get part :content)))) -;; There is a bug in emacs 23's message.el that results in a newline +;; There is a bug in Emacs' message.el that results in a newline ;; not being inserted after the References header, so the next header ;; is concatenated to the end of it. This function fixes the problem, ;; while guarding against the possibility that some current or future @@ -221,6 +232,8 @@ Typically this is added to `notmuch-mua-send-hook'." (funcall original-func header references) (unless (bolp) (insert "\n"))) +;;; Mua reply + (defun notmuch-mua-reply (query-string &optional sender reply-all) (let ((args '("reply" "--format=sexp" "--format-version=4")) (process-crypto notmuch-show-process-crypto) @@ -265,8 +278,8 @@ Typically this is added to `notmuch-mua-send-hook'." ;; Create a buffer-local queue for tag changes triggered when ;; sending the reply. (when notmuch-message-replied-tags - (setq-local notmuch-message-queued-tag-changes - (list (cons query-string notmuch-message-replied-tags)))) + (setq notmuch-message-queued-tag-changes + (list (cons query-string notmuch-message-replied-tags)))) ;; Insert the message body - but put it in front of the signature ;; if one is present, and after any other content ;; message*setup-hooks may have added to the message body already. @@ -324,21 +337,29 @@ Typically this is added to `notmuch-mua-send-hook'." (message-goto-body) (set-buffer-modified-p nil)) +;;; Mode and keymap + +(defvar notmuch-message-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-c C-c") #'notmuch-mua-send-and-exit) + (define-key map (kbd "C-c C-s") #'notmuch-mua-send) + (define-key map (kbd "C-c C-p") #'notmuch-draft-postpone) + (define-key map (kbd "C-x C-s") #'notmuch-draft-save) + map) + "Keymap for `notmuch-message-mode'.") + (define-derived-mode notmuch-message-mode message-mode "Message[Notmuch]" "Notmuch message composition mode. Mostly like `message-mode'." (notmuch-address-setup)) (put 'notmuch-message-mode 'flyspell-mode-predicate 'mail-mode-flyspell-verify) -(define-key notmuch-message-mode-map (kbd "C-c C-c") #'notmuch-mua-send-and-exit) -(define-key notmuch-message-mode-map (kbd "C-c C-s") #'notmuch-mua-send) -(define-key notmuch-message-mode-map (kbd "C-c C-p") #'notmuch-draft-postpone) -(define-key notmuch-message-mode-map (kbd "C-x C-s") #'notmuch-draft-save) +;;; New messages (defun notmuch-mua-pop-to-buffer (name switch-function) - "Pop to buffer NAME, and warn if it already exists and is -modified. This function is notmuch adaptation of -`message-pop-to-buffer'." + "Pop to buffer NAME, and warn if it already exists and is modified. +Like `message-pop-to-buffer' but enable `notmuch-message-mode' +instead of `message-mode' and SWITCH-FUNCTION is mandatory." (let ((buffer (get-buffer name))) (if (and buffer (buffer-name buffer)) @@ -350,25 +371,23 @@ modified. This function is notmuch adaptation of (select-window window)) (funcall switch-function buffer) (set-buffer buffer)) - (when (and (buffer-modified-p) - (not (prog1 - (y-or-n-p - "Message already being composed; erase? ") - (message nil)))) - (error "Message being composed"))) + (when (buffer-modified-p) + (if (y-or-n-p "Message already being composed; erase? ") + (message nil) + (error "Message being composed")))) (funcall switch-function name) (set-buffer name)) (erase-buffer) (notmuch-message-mode))) -(defun notmuch-mua-mail (&optional to subject other-headers continue +(defun notmuch-mua-mail (&optional to subject other-headers _continue switch-function yank-action send-actions return-action &rest ignored) "Invoke the notmuch mail composition window." (interactive) (when notmuch-mua-user-agent-function (let ((user-agent (funcall notmuch-mua-user-agent-function))) - (unless (string= "" user-agent) + (unless (string-empty-p user-agent) (push (cons 'User-Agent user-agent) other-headers)))) (unless (assq 'From other-headers) (push (cons 'From (message-make-from @@ -390,16 +409,10 @@ modified. This function is notmuch adaptation of (dolist (h other-headers other-headers) (when (stringp (car h)) (setcar h (intern (capitalize (car h)))))))) - (args (list yank-action send-actions)) ;; Cause `message-setup-1' to do things relevant for mail, ;; such as observe `message-default-mail-headers'. (message-this-is-mail t)) - ;; message-setup-1 in Emacs 23 does not accept return-action - ;; argument. Pass it only if it is supplied by the caller. This - ;; will never be the case when we're called by `compose-mail' in - ;; Emacs 23. - (when return-action (nconc args '(return-action))) - (apply 'message-setup-1 headers args)) + (message-setup-1 headers yank-action send-actions return-action)) (notmuch-fcc-header-setup) (message-sort-headers) (message-hide-headers) @@ -407,37 +420,21 @@ modified. This function is notmuch adaptation of (notmuch-mua-maybe-set-window-dedicated) (message-goto-to)) -(defcustom notmuch-identities nil - "Identities that can be used as the From: address when composing a new message. - -If this variable is left unset, then a list will be constructed from the -name and addresses configured in the notmuch configuration file." - :type '(repeat string) - :group 'notmuch-send) - -(defcustom notmuch-always-prompt-for-sender nil - "Always prompt for the From: address when composing or forwarding a message. - -This is not taken into account when replying to a message, because in that case -the From: header is already filled in by notmuch." - :type 'boolean - :group 'notmuch-send) - (defvar notmuch-mua-sender-history nil) (defun notmuch-mua-prompt-for-sender () "Prompt for a sender from the user's configured identities." (if notmuch-identities - (ido-completing-read "Send mail from: " notmuch-identities - nil nil nil 'notmuch-mua-sender-history - (car notmuch-identities)) + (completing-read "Send mail from: " notmuch-identities + nil nil nil 'notmuch-mua-sender-history + (car notmuch-identities)) (let* ((name (notmuch-user-name)) (addrs (cons (notmuch-user-primary-email) (notmuch-user-other-email))) (address - (ido-completing-read (concat "Sender address for " name ": ") addrs - nil nil nil 'notmuch-mua-sender-history - (car addrs)))) + (completing-read (concat "Sender address for " name ": ") addrs + nil nil nil 'notmuch-mua-sender-history + (car addrs)))) (message-make-from name address)))) (put 'notmuch-mua-new-mail 'notmuch-prefix-doc "... and prompt for sender") @@ -504,10 +501,10 @@ the From: address." ;; Create a buffer-local queue for tag changes triggered when ;; sending the message. (when notmuch-message-forwarded-tags - (setq-local notmuch-message-queued-tag-changes - (cl-loop for id in forward-queries - collect - (cons id notmuch-message-forwarded-tags)))) + (setq notmuch-message-queued-tag-changes + (cl-loop for id in forward-queries + collect + (cons id notmuch-message-forwarded-tags)))) ;; `message-forward-make-body' shows the User-agent header. Hide ;; it again. (message-hide-headers) @@ -519,10 +516,10 @@ the From: address." If PROMPT-FOR-SENDER is non-nil, the user will be prompted for the From: address first. If REPLY-ALL is non-nil, the message will be addressed to all recipients of the source message." - ;; In current emacs (24.3) select-active-regions is set to t by - ;; default. The reply insertion code sets the region to the quoted - ;; message to make it easy to delete (kill-region or C-w). These two - ;; things combine to put the quoted message in the primary selection. + ;; `select-active-regions' is t by default. The reply insertion code + ;; sets the region to the quoted message to make it easy to delete + ;; (kill-region or C-w). These two things combine to put the quoted + ;; message in the primary selection. ;; ;; This is not what the user wanted and is a privacy risk (accidental ;; pasting of the quoted message). We can avoid some of the problems @@ -536,6 +533,8 @@ will be addressed to all recipients of the source message." (notmuch-mua-reply query-string sender reply-all) (deactivate-mark))) +;;; Checks + (defun notmuch-mua-check-no-misplaced-secure-tag () "Query user if there is a misplaced secure mml tag. @@ -547,11 +546,11 @@ tag, or the user confirms they mean it." (goto-char (point-max)) (or ;; We are always fine if there is no secure tag. - (not (search-backward "<#secure" nil 't)) + (not (search-backward "<#secure" nil t)) ;; There is a secure tag, so it must be at the start of the ;; body, with no secure tag earlier (i.e., in the headers). (and (= (point) body-start) - (not (search-backward "<#secure" nil 't))) + (not (search-backward "<#secure" nil t))) ;; The user confirms they means it. (yes-or-no-p "\ There is a <#secure> tag not at the start of the body. It is @@ -578,6 +577,8 @@ The <#secure> tag at the start of the body is not followed by a newline. It is likely that the message will be sent unsigned and unencrypted. Really send? ")))) +;;; Finishing commands + (defun notmuch-mua-send-common (arg &optional exit) (interactive "P") (run-hooks 'notmuch-mua-send-hook) @@ -591,7 +592,7 @@ unencrypted. Really send? ")))) (defun notmuch-mua-send-and-exit (&optional arg) (interactive "P") - (notmuch-mua-send-common arg 't)) + (notmuch-mua-send-common arg t)) (defun notmuch-mua-send (&optional arg) (interactive "P") @@ -601,18 +602,18 @@ unencrypted. Really send? ")))) (interactive) (message-kill-buffer)) -;; +;;; _ (define-mail-user-agent 'notmuch-user-agent - 'notmuch-mua-mail 'notmuch-mua-send-and-exit - 'notmuch-mua-kill-buffer 'notmuch-mua-send-hook) + 'notmuch-mua-mail + 'notmuch-mua-send-and-exit + 'notmuch-mua-kill-buffer + 'notmuch-mua-send-hook) ;; Add some more headers to the list that `message-mode' hides when ;; composing a message. (notmuch-mua-add-more-hidden-headers) -;; - (provide 'notmuch-mua) ;;; notmuch-mua.el ends here diff --git a/emacs/notmuch-parser.el b/emacs/notmuch-parser.el index 3aa5bd8f..f04b07c2 100644 --- a/emacs/notmuch-parser.el +++ b/emacs/notmuch-parser.el @@ -1,4 +1,4 @@ -;;; notmuch-parser.el --- streaming S-expression parser +;;; notmuch-parser.el --- streaming S-expression parser -*- lexical-binding: t -*- ;; ;; Copyright © Austin Clements ;; @@ -21,7 +21,9 @@ ;;; Code: -(eval-when-compile (require 'cl-lib)) +(require 'cl-lib) +(require 'pcase) +(require 'subr-x) (defun notmuch-sexp-create-parser () "Return a new streaming S-expression parser. @@ -140,15 +142,6 @@ beginning of a list, throw invalid-read-syntax." (forward-char) (signal 'invalid-read-syntax (list (string (char-before))))))) -(defun notmuch-sexp-eof (sp) - "Signal an error if there is more data in SP's buffer. - -Moves point to the beginning of any trailing data or to the end -of the buffer if there is only trailing whitespace." - (skip-chars-forward " \n\r\t") - (unless (eobp) - (error "Trailing garbage following expression"))) - (defvar notmuch-sexp--parser nil "The buffer-local notmuch-sexp-parser instance. @@ -168,9 +161,8 @@ additional data. The caller just needs to ensure it does not move point in the input buffer." ;; Set up the initial state (unless (local-variable-p 'notmuch-sexp--parser) - (set (make-local-variable 'notmuch-sexp--parser) - (notmuch-sexp-create-parser)) - (set (make-local-variable 'notmuch-sexp--state) 'begin)) + (setq-local notmuch-sexp--parser (notmuch-sexp-create-parser)) + (setq-local notmuch-sexp--state 'begin)) (let (done) (while (not done) (cl-case notmuch-sexp--state @@ -188,8 +180,11 @@ move point in the input buffer." (t (with-current-buffer result-buffer (funcall result-function result)))))) (end - ;; Any trailing data is unexpected - (notmuch-sexp-eof notmuch-sexp--parser) + ;; Skip over trailing whitespace. + (skip-chars-forward " \n\r\t") + ;; Any trailing data is unexpected. + (unless (eobp) + (error "Trailing garbage following expression")) (setq done t))))) ;; Clear out what we've parsed (delete-region (point-min) (point))) diff --git a/emacs/notmuch-print.el b/emacs/notmuch-print.el index 6dd9f775..d0061499 100644 --- a/emacs/notmuch-print.el +++ b/emacs/notmuch-print.el @@ -1,4 +1,4 @@ -;;; notmuch-print.el --- printing messages from notmuch +;;; notmuch-print.el --- printing messages from notmuch -*- lexical-binding: t -*- ;; ;; Copyright © David Edmondson ;; @@ -25,6 +25,8 @@ (declare-function notmuch-show-get-prop "notmuch-show" (prop &optional props)) +;;; Options + (defcustom notmuch-print-mechanism 'notmuch-print-lpr "How should printing be done?" :group 'notmuch-show @@ -36,7 +38,7 @@ (function :tag "Use muttprint then evince" notmuch-print-muttprint/evince) (function :tag "Using a custom function"))) -;; Utility functions: +;;; Utility functions (defun notmuch-print-run-evince (file) "View FILE using 'evince'." @@ -54,9 +56,9 @@ Optional OUTPUT allows passing a list of flags to muttprint." "--printed-headers" "Date_To_From_CC_Newsgroups_*Subject*_/Tags/" output)) -;; User-visible functions: +;;; User-visible functions -(defun notmuch-print-lpr (msg) +(defun notmuch-print-lpr (_msg) "Print a message buffer using lpr." (lpr-buffer)) @@ -76,11 +78,11 @@ Optional OUTPUT allows passing a list of flags to muttprint." (ps-print-buffer ps-file) (notmuch-print-run-evince ps-file))) -(defun notmuch-print-muttprint (msg) +(defun notmuch-print-muttprint (_msg) "Print a message using muttprint." (notmuch-print-run-muttprint)) -(defun notmuch-print-muttprint/evince (msg) +(defun notmuch-print-muttprint/evince (_msg) "Preview a message buffer using muttprint and evince." (let ((ps-file (make-temp-file "notmuch" nil ".ps"))) (notmuch-print-run-muttprint (list "--printer" (concat "TO_FILE:" ps-file))) @@ -91,6 +93,8 @@ Optional OUTPUT allows passing a list of flags to muttprint." (set-buffer-modified-p nil) (funcall notmuch-print-mechanism msg)) +;;; _ + (provide 'notmuch-print) ;;; notmuch-print.el ends here diff --git a/emacs/notmuch-query.el b/emacs/notmuch-query.el index 3cfccbc3..d7349b77 100644 --- a/emacs/notmuch-query.el +++ b/emacs/notmuch-query.el @@ -1,4 +1,4 @@ -;;; notmuch-query.el --- provide an emacs api to query notmuch +;;; notmuch-query.el --- provide an emacs api to query notmuch -*- lexical-binding: t -*- ;; ;; Copyright © David Bremner ;; @@ -23,6 +23,8 @@ (require 'notmuch-lib) +;;; Basic query function + (defun notmuch-query-get-threads (search-terms) "Return a list of threads of messages matching SEARCH-TERMS. @@ -35,16 +37,13 @@ is a possibly empty forest of replies." (setq args (append args search-terms)) (apply #'notmuch-call-notmuch-sexp args))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Mapping functions across collections of messages. +;;; Mapping functions across collections of messages (defun notmuch-query-map-aux (mapper function seq) "Private function to do the actual mapping and flattening." - (apply 'append - (mapcar - (lambda (tree) - (funcall mapper function tree)) - seq))) + (cl-mapcan (lambda (tree) + (funcall mapper function tree)) + seq)) (defun notmuch-query-map-threads (fn threads) "Apply function FN to every thread in THREADS. @@ -62,10 +61,10 @@ Flatten results to a list. See the function "Apply function FN to every message in TREE. Flatten results to a list. See the function `notmuch-query-get-threads' for more information." - (cons (funcall fn (car tree)) (notmuch-query-map-forest fn (cadr tree)))) + (cons (funcall fn (car tree)) + (notmuch-query-map-forest fn (cadr tree)))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Predefined queries +;;; Predefined queries (defun notmuch-query-get-message-ids (&rest search-terms) "Return a list of message-ids of messages that match SEARCH-TERMS." diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el index b08ceb97..ba93febb 100644 --- a/emacs/notmuch-show.el +++ b/emacs/notmuch-show.el @@ -1,4 +1,4 @@ -;;; notmuch-show.el --- displaying notmuch forests +;;; notmuch-show.el --- displaying notmuch forests -*- lexical-binding: t -*- ;; ;; Copyright © Carl Worth ;; Copyright © David Edmondson @@ -23,10 +23,6 @@ ;;; Code: -(eval-when-compile - (require 'cl-lib) - (require 'pcase)) - (require 'mm-view) (require 'message) (require 'mm-decode) @@ -59,6 +55,12 @@ (declare-function notmuch-read-query "notmuch" (prompt)) (declare-function notmuch-draft-resume "notmuch-draft" (id)) +(defvar shr-blocked-images) +(defvar gnus-blocked-images) +(defvar shr-content-function) + +;;; Options + (defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date") "Headers that should be shown in a message, in this order. @@ -162,23 +164,19 @@ indentation." :type '(choice (const nil) regexp) :group 'notmuch-show) -(defvar notmuch-show-thread-id nil) -(make-variable-buffer-local 'notmuch-show-thread-id) +;;; Variables + +(defvar-local notmuch-show-thread-id nil) -(defvar notmuch-show-parent-buffer nil) -(make-variable-buffer-local 'notmuch-show-parent-buffer) +(defvar-local notmuch-show-parent-buffer nil) -(defvar notmuch-show-query-context nil) -(make-variable-buffer-local 'notmuch-show-query-context) +(defvar-local notmuch-show-query-context nil) -(defvar notmuch-show-process-crypto nil) -(make-variable-buffer-local 'notmuch-show-process-crypto) +(defvar-local notmuch-show-process-crypto nil) -(defvar notmuch-show-elide-non-matching-messages nil) -(make-variable-buffer-local 'notmuch-show-elide-non-matching-messages) +(defvar-local notmuch-show-elide-non-matching-messages nil) -(defvar notmuch-show-indent-content t) -(make-variable-buffer-local 'notmuch-show-indent-content) +(defvar-local notmuch-show-indent-content t) (defvar notmuch-show-attachment-debug nil "If t log stdout and stderr from attachment handlers. @@ -186,8 +184,9 @@ indentation." When set to nil (the default) stdout and stderr from attachment handlers is discarded. When set to t the stdout and stderr from each attachment handler is logged in buffers with names beginning -\" *notmuch-part*\". This option requires emacs version at least -24.3 to work.") +\" *notmuch-part*\".") + +;;; Options (defcustom notmuch-show-stash-mlarchive-link-alist '(("Gmane" . "https://mid.gmane.org/") @@ -267,6 +266,8 @@ position of the message in the thread." :type 'boolean :group 'notmuch-show) +;;; Utilities + (defmacro with-current-notmuch-show-message (&rest body) "Evaluate body with current buffer set to the text of current message." `(save-excursion @@ -282,6 +283,8 @@ position of the message in the thread." "Enable Visual Line mode." (visual-line-mode t)) +;;; Commands + ;; DEPRECATED in Notmuch 0.16 since we now have convenient part ;; commands. We'll keep the command around for a version or two in ;; case people want to bind it themselves. @@ -330,7 +333,7 @@ operation on the contents of the current buffer." (header (concat "Subject: " subject "\n" "To: " to "\n" - (if (not (string= cc "")) + (if (not (string-empty-p cc)) (concat "Cc: " cc "\n") "") "From: " from "\n" @@ -362,6 +365,8 @@ operation on the contents of the current buffer." (interactive) (notmuch-show-with-message-as-text 'notmuch-print-message)) +;;; Headers + (defun notmuch-show-fontify-header () (let ((face (cond ((looking-at "[Tt]o:") @@ -500,13 +505,15 @@ message at DEPTH in the current thread." (narrow-to-region start (point-max)) (run-hooks 'notmuch-show-markup-headers-hook))))) +;;; Parts + (define-button-type 'notmuch-show-part-button-type 'action 'notmuch-show-part-button-default 'follow-link t 'face 'message-mml :supertype 'notmuch-button-type) -(defun notmuch-show-insert-part-header (nth content-type declared-type +(defun notmuch-show-insert-part-header (_nth content-type declared-type &optional name comment) (let ((base-label (concat (and name (concat name ": ")) declared-type @@ -555,7 +562,7 @@ message at DEPTH in the current thread." (overlay-put overlay 'invisible (not show)) t))))))) -;; Part content ID handling +;;; Part content ID handling (defvar notmuch-show--cids nil "Alist from raw content ID to (MSG PART).") @@ -574,15 +581,17 @@ message at DEPTH in the current thread." ;; alternative (even if we can't render it). (push (list content-id msg part) notmuch-show--cids))) ;; Recurse on sub-parts - (let ((ctype (notmuch-split-content-type - (downcase (plist-get part :content-type))))) - (cond ((equal (car ctype) "multipart") - (mapc (apply-partially #'notmuch-show--register-cids msg) - (plist-get part :content))) - ((equal ctype '("message" "rfc822")) - (notmuch-show--register-cids - msg - (car (plist-get (car (plist-get part :content)) :body))))))) + (when-let ((type (plist-get part :content-type))) + (pcase-let ((`(,type ,subtype) + (split-string (downcase type) "/"))) + (cond ((equal type "multipart") + (mapc (apply-partially #'notmuch-show--register-cids msg) + (plist-get part :content))) + ((and (equal type "message") + (equal subtype "rfc822")) + (notmuch-show--register-cids + msg + (car (plist-get (car (plist-get part :content)) :body)))))))) (defun notmuch-show--get-cid-content (cid) "Return a list (CID-content content-type) or nil. @@ -591,16 +600,13 @@ This will only find parts from messages that have been inserted into the current buffer. CID must be a raw content ID, without enclosing angle brackets, a cid: prefix, or URL encoding. This will return nil if the CID is unknown or cannot be retrieved." - (let ((descriptor (cdr (assoc cid notmuch-show--cids)))) - (when descriptor - (let* ((msg (car descriptor)) - (part (cadr descriptor)) - ;; Request caching for this content, as some messages - ;; reference the same cid: part many times (hundreds!). - (content (notmuch-get-bodypart-binary - msg part notmuch-show-process-crypto 'cache)) - (content-type (plist-get part :content-type))) - (list content content-type))))) + (when-let ((descriptor (cdr (assoc cid notmuch-show--cids)))) + (pcase-let ((`(,msg ,part) descriptor)) + ;; Request caching for this content, as some messages + ;; reference the same cid: part many times (hundreds!). + (list (notmuch-get-bodypart-binary + msg part notmuch-show-process-crypto 'cache) + (plist-get part :content-type))))) (defun notmuch-show-setup-w3m () "Instruct w3m how to retrieve content from a \"related\" part of a message." @@ -612,7 +618,7 @@ will return nil if the CID is unknown or cannot be retrieved." (setq mm-html-inhibit-images nil)) (defvar w3m-current-buffer) ;; From `w3m.el'. -(defun notmuch-show--cid-w3m-retrieve (url &rest args) +(defun notmuch-show--cid-w3m-retrieve (url &rest _args) ;; url includes the cid: prefix and is URL encoded (see RFC 2392). (let* ((cid (url-unhex-string (substring url 4))) (content-and-type @@ -628,7 +634,7 @@ will return nil if the CID is unknown or cannot be retrieved." (mapcar (lambda (inner-part) (plist-get inner-part :content-type)) (plist-get part :content))) -(defun notmuch-show-insert-part-multipart/alternative (msg part content-type nth depth button) +(defun notmuch-show-insert-part-multipart/alternative (msg part _content-type _nth depth _button) (let ((chosen-type (car (notmuch-multipart/alternative-choose msg (notmuch-show-multipart/*-to-list part)))) (inner-parts (plist-get part :content)) @@ -647,7 +653,7 @@ will return nil if the CID is unknown or cannot be retrieved." (indent-rigidly start (point) 1))) t) -(defun notmuch-show-insert-part-multipart/related (msg part content-type nth depth button) +(defun notmuch-show-insert-part-multipart/related (msg part _content-type _nth depth _button) (let ((inner-parts (plist-get part :content)) (start (point))) ;; Render the primary part. FIXME: Support RFC 2387 Start header. @@ -660,7 +666,7 @@ will return nil if the CID is unknown or cannot be retrieved." (indent-rigidly start (point) 1))) t) -(defun notmuch-show-insert-part-multipart/signed (msg part content-type nth depth button) +(defun notmuch-show-insert-part-multipart/signed (msg part _content-type _nth depth button) (when button (button-put button 'face 'notmuch-crypto-part-header)) ;; Insert a button detailing the signature status. @@ -676,7 +682,7 @@ will return nil if the CID is unknown or cannot be retrieved." (indent-rigidly start (point) 1))) t) -(defun notmuch-show-insert-part-multipart/encrypted (msg part content-type nth depth button) +(defun notmuch-show-insert-part-multipart/encrypted (msg part _content-type _nth depth button) (when button (button-put button 'face 'notmuch-crypto-part-header)) ;; Insert a button detailing the encryption status. @@ -694,10 +700,10 @@ will return nil if the CID is unknown or cannot be retrieved." (indent-rigidly start (point) 1))) t) -(defun notmuch-show-insert-part-application/pgp-encrypted (msg part content-type nth depth button) +(defun notmuch-show-insert-part-application/pgp-encrypted (_msg _part _content-type _nth _depth _button) t) -(defun notmuch-show-insert-part-multipart/* (msg part content-type nth depth button) +(defun notmuch-show-insert-part-multipart/* (msg part _content-type _nth depth _button) (let ((inner-parts (plist-get part :content)) (start (point))) ;; Show all of the parts. @@ -708,7 +714,7 @@ will return nil if the CID is unknown or cannot be retrieved." (indent-rigidly start (point) 1))) t) -(defun notmuch-show-insert-part-message/rfc822 (msg part content-type nth depth button) +(defun notmuch-show-insert-part-message/rfc822 (msg part _content-type _nth depth _button) (let* ((message (car (plist-get part :content))) (body (car (plist-get message :body))) (start (point))) @@ -725,7 +731,7 @@ will return nil if the CID is unknown or cannot be retrieved." (indent-rigidly start (point) 1))) t) -(defun notmuch-show-insert-part-text/plain (msg part content-type nth depth button) +(defun notmuch-show-insert-part-text/plain (msg part _content-type _nth depth button) ;; For backward compatibility we want to apply the text/plain hook ;; to the whole of the part including the part button if there is ;; one. @@ -739,7 +745,7 @@ will return nil if the CID is unknown or cannot be retrieved." (run-hook-with-args 'notmuch-show-insert-text/plain-hook msg depth)))) t) -(defun notmuch-show-insert-part-text/calendar (msg part content-type nth depth button) +(defun notmuch-show-insert-part-text/calendar (msg part _content-type _nth _depth _button) (insert (with-temp-buffer (insert (notmuch-get-bodypart-text msg part notmuch-show-process-crypto)) ;; notmuch-get-bodypart-text does no newline conversion. @@ -763,8 +769,8 @@ will return nil if the CID is unknown or cannot be retrieved." t) ;; For backwards compatibility. -(defun notmuch-show-insert-part-text/x-vcalendar (msg part content-type nth depth button) - (notmuch-show-insert-part-text/calendar msg part content-type nth depth button)) +(defun notmuch-show-insert-part-text/x-vcalendar (msg part _content-type _nth depth _button) + (notmuch-show-insert-part-text/calendar msg part nil nil depth nil)) (when (version< emacs-version "25.3") ;; https://bugs.gnu.org/28350 @@ -780,7 +786,7 @@ will return nil if the CID is unknown or cannot be retrieved." ;; the first time). (require 'enriched) (cl-letf (((symbol-function 'enriched-decode-display-prop) - (lambda (start end &optional param) (list start end)))) + (lambda (start end &optional _param) (list start end)))) (notmuch-show-insert-part-*/* msg part content-type nth depth button)))) (defun notmuch-show-get-mime-type-of-application/octet-stream (part) @@ -817,7 +823,8 @@ will return nil if the CID is unknown or cannot be retrieved." (gnus-blocked-images notmuch-show-text/html-blocked-images)) (notmuch-show-insert-part-*/* msg part content-type nth depth button)))) -;; These functions are used by notmuch-show--insert-part-text/html-shr +;;; Functions used by notmuch-show--insert-part-text/html-shr + (declare-function libxml-parse-html-region "xml.c") (declare-function shr-insert-document "shr") @@ -837,12 +844,12 @@ will return nil if the CID is unknown or cannot be retrieved." (shr-insert-document dom) t)) -(defun notmuch-show-insert-part-*/* (msg part content-type nth depth button) +(defun notmuch-show-insert-part-*/* (msg part content-type _nth _depth _button) ;; This handler _must_ succeed - it is the handler of last resort. (notmuch-mm-display-part-inline msg part content-type notmuch-show-process-crypto) t) -;; Functions for determining how to handle MIME parts. +;;; Functions for determining how to handle MIME parts. (defun notmuch-show-handlers-for (content-type) "Return a list of content handlers for a part of type CONTENT-TYPE." @@ -852,14 +859,13 @@ will return nil if the CID is unknown or cannot be retrieved." (push func result))) ;; Reverse order of prefrence. (list (intern (concat "notmuch-show-insert-part-*/*")) - (intern (concat - "notmuch-show-insert-part-" - (car (notmuch-split-content-type content-type)) - "/*")) + (intern (concat "notmuch-show-insert-part-" + (car (split-string content-type "/")) + "/*")) (intern (concat "notmuch-show-insert-part-" content-type)))) result)) -;; +;;; Parts (defun notmuch-show-insert-bodypart-internal (msg part content-type nth depth button) ;; Run the handlers until one of them succeeds. @@ -943,7 +949,8 @@ will return nil if the CID is unknown or cannot be retrieved." (defun notmuch-show-mime-type (part) "Return the correct mime-type to use for PART." - (let ((content-type (downcase (plist-get part :content-type)))) + (when-let ((content-type (plist-get part :content-type))) + (setq content-type (downcase content-type)) (or (and (string= content-type "application/octet-stream") (notmuch-show-get-mime-type-of-application/octet-stream part)) (and (string= content-type "inline patch") @@ -958,13 +965,13 @@ The function should take two parameters, PART and HIDE, and should return non-NIL if a header button should be inserted for this part.") -(defun notmuch-show-insert-header-p (part hide) +(defun notmuch-show-insert-header-p (part _hide) ;; Show all part buttons except for the first part if it is text/plain. (let ((mime-type (notmuch-show-mime-type part))) (not (and (string= mime-type "text/plain") (<= (plist-get part :id) 1))))) -(defun notmuch-show-reply-insert-header-p-never (part hide) +(defun notmuch-show-reply-insert-header-p-never (_part _hide) nil) (defun notmuch-show-reply-insert-header-p-trimmed (part hide) @@ -983,7 +990,7 @@ this part.") HIDE determines whether to show or hide the part and the button as follows: If HIDE is nil, show the part and the button. If HIDE is t, hide the part initially and show the button." - (let* ((content-type (downcase (plist-get part :content-type))) + (let* ((content-type (plist-get part :content-type)) (mime-type (notmuch-show-mime-type part)) (nth (plist-get part :id)) (long (and (notmuch-match-content-type mime-type "text/*") @@ -995,7 +1002,8 @@ is t, hide the part initially and show the button." ;; the first (or only) part if this is text/plain. (button (and (funcall notmuch-show-insert-header-p-function part hide) (notmuch-show-insert-part-header - nth mime-type content-type + nth mime-type + (and content-type (downcase content-type)) (plist-get part :filename)))) ;; Hide the part initially if HIDE is t, or if it is too long ;; and we have a button to allow toggling. @@ -1105,6 +1113,8 @@ is t, hide the part initially and show the button." (notmuch-show-message-visible msg (and (plist-get msg :match) (not (plist-get msg :excluded)))))) +;;; Toggle commands + (defun notmuch-show-toggle-process-crypto () "Toggle the processing of cryptographic MIME parts." (interactive) @@ -1133,6 +1143,8 @@ is t, hide the part initially and show the button." "Content is not indented.")) (notmuch-show-refresh-view)) +;;; Main insert functions + (defun notmuch-show-insert-tree (tree depth) "Insert the message tree TREE at depth DEPTH in the current thread." (let ((msg (car tree)) @@ -1150,6 +1162,8 @@ is t, hide the part initially and show the button." "Insert the forest of threads FOREST." (mapc (lambda (thread) (notmuch-show-insert-thread thread 0)) forest)) +;;; Link buttons + (defvar notmuch-id-regexp (concat ;; Match the id: prefix only if it begins a word (to disallow, for @@ -1210,6 +1224,8 @@ buttons for a corresponding notmuch search." 'help-echo "Mouse-1, RET: search for this message" 'face goto-address-mail-face))))) +;;; Show command + ;;;###autoload (defun notmuch-show (thread-id &optional elide-toggle parent-buffer query-context buffer-name) "Run \"notmuch show\" with the given thread ID and display results. @@ -1332,6 +1348,8 @@ If no messages match the query return NIL." ;; Report back to the caller whether any messages matched. forest)) +;;; Refresh command + (defun notmuch-show-capture-state () "Capture the state of the current buffer. @@ -1406,6 +1424,8 @@ reset based on the original query." (ding) (message "Refreshing the buffer resulted in no messages!")))) +;;; Keymaps + (defvar notmuch-show-stash-map (let ((map (make-sparse-keymap))) (define-key map "c" 'notmuch-show-stash-cc) @@ -1485,7 +1505,8 @@ reset based on the original query." (define-key map "B" 'notmuch-show-browse-urls) map) "Keymap for \"notmuch show\" buffers.") -(fset 'notmuch-show-mode-map notmuch-show-mode-map) + +;;; Mode (define-derived-mode notmuch-show-mode fundamental-mode "notmuch-show" "Major mode for viewing a thread with notmuch. @@ -1523,6 +1544,8 @@ All currently available key bindings: (setq imenu-extract-index-name-function #'notmuch-show-imenu-extract-index-name-function)) +;;; Tree commands + (defun notmuch-tree-from-show-current-query () "Call notmuch tree with the current query." (interactive) @@ -1537,17 +1560,14 @@ All currently available key bindings: notmuch-show-query-context (notmuch-show-get-message-id))) +;;; Movement related functions. + (defun notmuch-show-move-to-message-top () (goto-char (notmuch-show-message-top))) (defun notmuch-show-move-to-message-bottom () (goto-char (notmuch-show-message-bottom))) -(defun notmuch-show-message-adjust () - (recenter 0)) - -;; Movement related functions. - ;; There's some strangeness here where a text property applied to a ;; region a->b is not found when point is at b. We walk backwards ;; until finding the property. @@ -1591,8 +1611,7 @@ effects." (cl-loop do (funcall function) while (notmuch-show-goto-message-next)))) -;; Functions relating to the visibility of messages and their -;; components. +;;; Functions relating to the visibility of messages and their components. (defun notmuch-show-message-visible (props visible-p) (overlay-put (plist-get props :message-overlay) 'invisible (not visible-p)) @@ -1602,8 +1621,7 @@ effects." (overlay-put (plist-get props :headers-overlay) 'invisible (not visible-p)) (notmuch-show-set-prop :headers-visible visible-p props)) -;; Functions for setting and getting attributes of the current -;; message. +;;; Functions for setting and getting attributes of the current message. (defun notmuch-show-set-message-properties (props) (save-excursion @@ -1644,13 +1662,13 @@ It gets property PROP from PROPS or, if PROPS is nil, the current message in either tree or show. This means that several utility functions in notmuch-show can be used directly by notmuch-tree as they just need the correct message properties." - (let ((props (or props - (cond ((eq major-mode 'notmuch-show-mode) - (notmuch-show-get-message-properties)) - ((eq major-mode 'notmuch-tree-mode) - (notmuch-tree-get-message-properties)) - (t nil))))) - (plist-get props prop))) + (plist-get (or props + (cond ((eq major-mode 'notmuch-show-mode) + (notmuch-show-get-message-properties)) + ((eq major-mode 'notmuch-tree-mode) + (notmuch-tree-get-message-properties)) + (t nil))) + prop)) (defun notmuch-show-get-message-id (&optional bare) "Return an id: query for the Message-Id of the current message. @@ -1737,7 +1755,7 @@ marked as unread, i.e. the tag changes in (apply 'notmuch-show-tag-message (notmuch-tag-change-list notmuch-show-mark-read-tags unread)))) -(defun notmuch-show-seen-current-message (start end) +(defun notmuch-show-seen-current-message (_start _end) "Mark the current message read if it is open. We only mark it read once: if it is changed back then that is a @@ -1755,11 +1773,11 @@ user decision and we should not override it." ;; We need to redisplay to get window-start and window-end correct. (redisplay) (save-excursion - (condition-case err + (condition-case nil (funcall notmuch-show-mark-read-function (window-start) (window-end)) ((debug error) (unless notmuch-show--seen-has-errored - (setq notmuch-show--seen-has-errored 't) + (setq notmuch-show--seen-has-errored t) (setq header-line-format (concat header-line-format (propertize @@ -1772,12 +1790,11 @@ user decision and we should not override it." Reshows the current thread with matches defined by the new query-string." (interactive (list (notmuch-read-query "Filter thread: "))) (let ((msg-id (notmuch-show-get-message-id))) - (setq notmuch-show-query-context (if (string= query "") nil query)) + (setq notmuch-show-query-context (if (string-empty-p query) nil query)) (notmuch-show-refresh-view t) (notmuch-show-goto-message msg-id))) -;; Functions for getting attributes of several messages in the current -;; thread. +;;; Functions for getting attributes of several messages in the current thread. (defun notmuch-show-get-message-ids-for-open-messages () "Return a list of all id: queries for open messages in the current thread." @@ -1791,7 +1808,7 @@ Reshows the current thread with matches defined by the new query-string." (setq done (not (notmuch-show-goto-message-next)))) message-ids))) -;; Commands typically bound to keys. +;;; Commands typically bound to keys. (defun notmuch-show-advance () "Advance through thread. @@ -1919,6 +1936,9 @@ any effects from previous calls to (message-resend addresses) (notmuch-bury-or-kill-this-buffer))) +(defun notmuch-show-message-adjust () + (recenter 0)) + (defun notmuch-show-next-message (&optional pop-at-end) "Show the next message. @@ -2349,7 +2369,9 @@ the user (see `notmuch-show-stash-mlarchive-link-alist')." (browse-url (current-kill 0 t))) (defun notmuch-show-stash-git-helper (addresses prefix) - "Escape, trim, quote, and add PREFIX to each address in list of ADDRESSES, and return the result as a single string." + "Normalize all ADDRESSES while adding PREFIX. +Escape, trim, quote and add PREFIX to each address in list +of ADDRESSES, and return the result as a single string." (mapconcat (lambda (x) (concat prefix "\"" ;; escape double-quotes @@ -2362,10 +2384,12 @@ the user (see `notmuch-show-stash-mlarchive-link-alist')." addresses " ")) (put 'notmuch-show-stash-git-send-email 'notmuch-prefix-doc - "Copy From/To/Cc of current message to kill-ring in a form suitable for pasting to git send-email command line.") + "Copy From/To/Cc of current message to kill-ring. +Use a form suitable for pasting to git send-email command line.") (defun notmuch-show-stash-git-send-email (&optional no-in-reply-to) - "Copy From/To/Cc/Message-Id of current message to kill-ring in a form suitable for pasting to git send-email command line. + "Copy From/To/Cc/Message-Id of current message to kill-ring. +Use a form suitable for pasting to git send-email command line. If invoked with a prefix argument (or NO-IN-REPLY-TO is non-nil), omit --in-reply-to=." @@ -2385,7 +2409,7 @@ omit --in-reply-to=." (list (notmuch-show-get-message-id t)) "--in-reply-to=")))) " "))) -;; Interactive part functions and their helpers +;;; Interactive part functions and their helpers (defun notmuch-show-generate-part-buffer (msg part) "Return a temporary buffer containing the specified part's content." @@ -2418,10 +2442,9 @@ This ensures that the temporary buffer created for the mm-handle is destroyed when FN returns. If MIME-TYPE is given then force part to be treated as if it had that mime-type." (let ((handle (notmuch-show-current-part-handle mime-type))) - ;; emacs 24.3+ puts stdout/stderr into the calling buffer so we - ;; call it from a temp-buffer, unless - ;; notmuch-show-attachment-debug is non-nil in which case we put - ;; it in " *notmuch-part*". + ;; Emacs puts stdout/stderr into the calling buffer so we call + ;; it from a temp-buffer, unless notmuch-show-attachment-debug + ;; is non-nil, in which case we put it in " *notmuch-part*". (unwind-protect (if notmuch-show-attachment-debug (with-current-buffer (generate-new-buffer " *notmuch-part*") @@ -2533,6 +2556,8 @@ browsing." (funcall fn (completing-read prompt urls nil nil nil nil (car urls))) (message "No URLs found.")))) +;;; _ + (provide 'notmuch-show) ;;; notmuch-show.el ends here diff --git a/emacs/notmuch-tag.el b/emacs/notmuch-tag.el index 5d4a6865..ebccb5a0 100644 --- a/emacs/notmuch-tag.el +++ b/emacs/notmuch-tag.el @@ -1,4 +1,4 @@ -;;; notmuch-tag.el --- tag messages within emacs +;;; notmuch-tag.el --- tag messages within emacs -*- lexical-binding: t -*- ;; ;; Copyright © Damien Cassou ;; Copyright © Carl Worth @@ -23,10 +23,6 @@ ;;; Code: -(require 'cl-lib) -(eval-when-compile - (require 'pcase)) - (require 'crm) (require 'notmuch-lib) @@ -37,6 +33,8 @@ (declare-function notmuch-tree-tag "notmuch-tree" (tag-changes)) (declare-function notmuch-jump "notmuch-jump" (action-map prompt)) +;;; Keys + (define-widget 'notmuch-tag-key-type 'list "A single key tagging binding." :format "%v" @@ -68,15 +66,15 @@ The key `notmuch-tag-jump-reverse-key' (k by default) should not be used (either as a key, or as the start of a key sequence) as it is already bound: it switches the menu to a menu of the reverse tagging operations. The reverse of a tagging operation is -the same list of individual tag-ops but with `+tag` replaced by -`-tag` and vice versa. +the same list of individual tag-ops but with `+tag' replaced by +`-tag' and vice versa. If setting this variable outside of customize then it should be a list of triples (lists of three elements). Each triple should be of the form (key-binding tagging-operations name). KEY-BINDING can be a single character or a key sequence; TAGGING-OPERATIONS should either be a list of individual tag operations each of the -form `+tag` or `-tag`, or the variable name of a variable that is +form `+tag' or `-tag', or the variable name of a variable that is a list of tagging operations; NAME should be a name for the tagging operation, if omitted or empty than then name is taken from TAGGING-OPERATIONS." @@ -84,6 +82,8 @@ from TAGGING-OPERATIONS." :type '(repeat notmuch-tag-key-type) :group 'notmuch-tag) +;;; Faces and Formats + (define-widget 'notmuch-tag-format-type 'lazy "Customize widget for notmuch-tag-format and friends." :type '(alist :key-type (regexp :tag "Tag") @@ -116,7 +116,7 @@ from TAGGING-OPERATIONS." '((t :foreground "red")) "Default face used for the unread tag. -Used in the default value of `notmuch-tag-formats`." +Used in the default value of `notmuch-tag-formats'." :group 'notmuch-faces) (defface notmuch-tag-flagged @@ -128,7 +128,7 @@ Used in the default value of `notmuch-tag-formats`." (:foreground "blue"))) "Face used for the flagged tag. -Used in the default value of `notmuch-tag-formats`." +Used in the default value of `notmuch-tag-formats'." :group 'notmuch-faces) (defcustom notmuch-tag-formats @@ -137,20 +137,23 @@ Used in the default value of `notmuch-tag-formats`." (notmuch-tag-format-image-data tag (notmuch-tag-star-icon)))) "Custom formats for individual tags. -This is an association list that maps from tag name regexps to -lists of formatting expressions. The first entry whose car -regexp-matches a tag will be used to format that tag. The regexp -is implicitly anchored, so to match a literal tag name, just use -that tag name (if it contains special regexp characters like -\".\" or \"*\", these have to be escaped). The cdr of the -matching entry gives a list of Elisp expressions that modify the -tag. If the list is empty, the tag will simply be hidden. -Otherwise, each expression will be evaluated in order: for the -first expression, the variable `tag' will be bound to the tag -name; for each later expression, the variable `tag' will be bound -to the result of the previous expression. In this way, each +This is an association list of the form ((MATCH EXPR...)...), +mapping tag name regexps to lists of formatting expressions. + +The first entry whose MATCH regexp-matches a tag is used to +format that tag. The regexp is implicitly anchored, so to match +a literal tag name, just use that tag name (if it contains +special regexp characters like \".\" or \"*\", these have to be +escaped). + +The cdr of the matching entry gives a list of Elisp expressions +that modify the tag. If the list is empty, the tag is simply +hidden. Otherwise, each expression EXPR is evaluated in order: +for the first expression, the variable `tag' is bound to the tag +name; for each later expression, the variable `tag' is bound to +the result of the previous expression. In this way, each expression can build on the formatting performed by the previous -expression. The result of the last expression will displayed in +expression. The result of the last expression is displayed in place of the tag. For example, to replace a tag with another string, simply use @@ -170,7 +173,7 @@ with images." (t :inverse-video t)) "Face used to display deleted tags. -Used in the default value of `notmuch-tag-deleted-formats`." +Used in the default value of `notmuch-tag-deleted-formats'." :group 'notmuch-faces) (defcustom notmuch-tag-deleted-formats @@ -178,7 +181,7 @@ Used in the default value of `notmuch-tag-deleted-formats`." (".*" (notmuch-apply-face tag `notmuch-tag-deleted))) "Custom formats for tags when deleted. -For deleted tags the formats in `notmuch-tag-formats` are applied +For deleted tags the formats in `notmuch-tag-formats' are applied first and then these formats are applied on top; that is `tag' passed to the function is the tag with all these previous formattings applied. The formatted can access the original @@ -199,14 +202,14 @@ See `notmuch-tag-formats' for full documentation." '((t :underline "green")) "Default face used for added tags. -Used in the default value for `notmuch-tag-added-formats`." +Used in the default value for `notmuch-tag-added-formats'." :group 'notmuch-faces) (defcustom notmuch-tag-added-formats '((".*" (notmuch-apply-face tag 'notmuch-tag-added))) "Custom formats for tags when added. -For added tags the formats in `notmuch-tag-formats` are applied +For added tags the formats in `notmuch-tag-formats' are applied first and then these formats are applied on top. To disable special formatting of added tags, set this variable to @@ -217,6 +220,8 @@ See `notmuch-tag-formats' for full documentation." :group 'notmuch-faces :type 'notmuch-tag-format-type) +;;; Icons + (defun notmuch-tag-format-image-data (tag data) "Replace TAG with image DATA, if available. @@ -270,6 +275,8 @@ This can be used with `notmuch-tag-format-image-data'." ") +;;; Format Handling + (defvar notmuch-tag--format-cache (make-hash-table :test 'equal) "Cache of tag format lookup. Internal to `notmuch-tag-format-tag'.") @@ -277,32 +284,34 @@ This can be used with `notmuch-tag-format-image-data'." "Clear the internal cache of tag formats." (clrhash notmuch-tag--format-cache)) -(defun notmuch-tag--get-formats (tag format-alist) +(defun notmuch-tag--get-formats (tag alist) "Find the first item whose car regexp-matches TAG." (save-match-data ;; Don't use assoc-default since there's no way to distinguish a ;; missing key from a present key with a null cdr. - (cl-assoc tag format-alist + (cl-assoc tag alist :test (lambda (tag key) (and (eq (string-match key tag) 0) (= (match-end 0) (length tag))))))) -(defun notmuch-tag--do-format (tag formatted-tag formats) +(defun notmuch-tag--do-format (bare-tag tag formats) "Apply a tag-formats entry to TAG." (cond ((null formats) ;; - Tag not in `formats', - formatted-tag) ;; the format is the tag itself. + tag) ;; the format is the tag itself. ((null (cdr formats)) ;; - Tag was deliberately hidden, nil) ;; no format must be returned (t ;; Tag was found and has formats, we must apply all the ;; formats. TAG may be null so treat that as a special case. - (let ((bare-tag tag) - (tag (copy-sequence (or formatted-tag "")))) + (let ((return-tag (copy-sequence (or tag "")))) (dolist (format (cdr formats)) - (setq tag (eval format))) - (if (and (null formatted-tag) (equal tag "")) + (setq return-tag + (eval format + `((bare-tag . ,bare-tag) + (tag . ,return-tag))))) + (if (and (null tag) (equal return-tag "")) nil - tag))))) + return-tag))))) (defun notmuch-tag-format-tag (tags orig-tags tag) "Format TAG according to `notmuch-tag-formats'. @@ -347,6 +356,8 @@ changed (the normal case) are shown using formats from face t))) +;;; Hooks + (defcustom notmuch-before-tag-hook nil "Hooks that are run before tags of a message are modified. @@ -369,18 +380,18 @@ the messages that were tagged." :options '(notmuch-hl-line-mode) :group 'notmuch-hooks) +;;; User Input + (defvar notmuch-select-tag-history nil - "Variable to store minibuffer history for -`notmuch-select-tag-with-completion' function.") + "Minibuffer history of `notmuch-select-tag-with-completion' function.") (defvar notmuch-read-tag-changes-history nil - "Variable to store minibuffer history for -`notmuch-read-tag-changes' function.") + "Minibuffer history of `notmuch-read-tag-changes' function.") (defun notmuch-tag-completions (&rest search-terms) "Return a list of tags for messages matching SEARCH-TERMS. -Returns all tags if no search terms are given." +Return all tags if no search terms are given." (unless search-terms (setq search-terms (list "*"))) (split-string @@ -391,14 +402,15 @@ Returns all tags if no search terms are given." "\n+" t)) (defun notmuch-select-tag-with-completion (prompt &rest search-terms) - (let ((tag-list (apply #'notmuch-tag-completions search-terms))) - (completing-read prompt tag-list nil nil nil 'notmuch-select-tag-history))) + (completing-read prompt + (apply #'notmuch-tag-completions search-terms) + nil nil nil 'notmuch-select-tag-history)) (defun notmuch-read-tag-changes (current-tags &optional prompt initial-input) "Prompt for tag changes in the minibuffer. -CURRENT-TAGS is a list of tags that are present on the message or -messages to be changed. These are offered as tag removal +CURRENT-TAGS is a list of tags that are present on the message +or messages to be changed. These are offered as tag removal completions. CURRENT-TAGS may contain duplicates. PROMPT, if non-nil, is the query string to present in the minibuffer. It defaults to \"Tags\". INITIAL-INPUT, if non-nil, will be the @@ -429,6 +441,8 @@ initial input in the minibuffer." nil nil initial-input 'notmuch-read-tag-changes-history)))) +;;; Tagging + (defun notmuch-update-tags (tags tag-changes) "Return a copy of TAGS with additions and removals from TAG-CHANGES. @@ -438,9 +452,9 @@ present or a \"-\" to indicate that the tag should be removed from TAGS if present." (let ((result-tags (copy-sequence tags))) (dolist (tag-change tag-changes) - (let ((op (string-to-char tag-change)) - (tag (unless (string= tag-change "") (substring tag-change 1)))) - (cl-case op + (let ((tag (and (not (string-empty-p tag-change)) + (substring tag-change 1)))) + (cl-case (aref tag-change 0) (?+ (unless (member tag result-tags) (push tag result-tags))) (?- (setq result-tags (delete tag result-tags))) @@ -466,14 +480,15 @@ messages instead of running (notmuch-call-notmuch-process \"tag\" ..) directly, so that hooks specified in notmuch-before-tag-hook and notmuch-after-tag-hook will be run." ;; Perform some validation - (mapc (lambda (tag-change) - (unless (string-match-p "^[-+]\\S-+$" tag-change) - (error "Tag must be of the form `+this_tag' or `-that_tag'"))) - tag-changes) + (dolist (tag-change tag-changes) + (unless (string-match-p "^[-+]\\S-+$" tag-change) + (error "Tag must be of the form `+this_tag' or `-that_tag'"))) (unless query (error "Nothing to tag!")) - (unless (null tag-changes) - (run-hooks 'notmuch-before-tag-hook) + (when tag-changes + (notmuch-dlet ((tag-changes tag-changes) + (query query)) + (run-hooks 'notmuch-before-tag-hook)) (if (<= (length query) notmuch-tag-argument-limit) (apply 'notmuch-call-notmuch-process "tag" (append tag-changes (list "--" query))) @@ -481,7 +496,9 @@ notmuch-after-tag-hook will be run." (let ((batch-op (concat (mapconcat #'notmuch-hex-encode tag-changes " ") " -- " query))) (notmuch-call-notmuch-process :stdin-string batch-op "tag" "--batch"))) - (run-hooks 'notmuch-after-tag-hook))) + (notmuch-dlet ((tag-changes tag-changes) + (query query)) + (run-hooks 'notmuch-after-tag-hook)))) (defun notmuch-tag-change-list (tags &optional reverse) "Convert TAGS into a list of tag changes. @@ -506,7 +523,7 @@ begin with a \"+\" or a \"-\". If REVERSE is non-nil, replace all Creates and displays a jump menu for the tagging operations specified in `notmuch-tagging-keys'. If REVERSE is set then it offers a menu of the reverses of the operations specified in -`notmuch-tagging-keys'; i.e. each `+tag` is replaced by `-tag` +`notmuch-tagging-keys'; i.e. each `+tag' is replaced by `-tag' and vice versa." ;; In principle this function is simple, but it has to deal with ;; lots of cases: different modes (search/show/tree), whether a name @@ -524,7 +541,7 @@ and vice versa." (symbol-value tag) tag)) (tag-change (if reverse - (notmuch-tag-change-list tag 't) + (notmuch-tag-change-list tag t) tag)) (name (or (and (not (string= name "")) name) @@ -547,7 +564,7 @@ and vice versa." (setq action-map (nreverse action-map)) (notmuch-jump action-map "Tag: "))) -;; +;;; _ (provide 'notmuch-tag) diff --git a/emacs/notmuch-tree.el b/emacs/notmuch-tree.el index f342f85a..13007a13 100644 --- a/emacs/notmuch-tree.el +++ b/emacs/notmuch-tree.el @@ -1,4 +1,4 @@ -;;; notmuch-tree.el --- displaying notmuch forests +;;; notmuch-tree.el --- displaying notmuch forests -*- lexical-binding: t -*- ;; ;; Copyright © Carl Worth ;; Copyright © David Edmondson @@ -24,8 +24,6 @@ ;;; Code: -(eval-when-compile (require 'cl-lib)) - (require 'mail-parse) (require 'notmuch-lib) @@ -42,13 +40,19 @@ (declare-function notmuch-search-find-thread-id "notmuch" (&optional bare)) (declare-function notmuch-search-find-subject "notmuch" ()) +;; For `notmuch-tree-next-thread-from-search'. +(declare-function notmuch-search-next-thread "notmuch" ()) +(declare-function notmuch-search-previous-thread "notmuch" ()) +(declare-function notmuch-tree-from-search-thread "notmuch" ()) + ;; the following variable is defined in notmuch.el (defvar notmuch-search-query-string) ;; this variable distinguishes the unthreaded display from the normal tree display -(defvar notmuch-tree-unthreaded nil +(defvar-local notmuch-tree-unthreaded nil "A buffer local copy of argument unthreaded to the function notmuch-tree.") -(make-variable-buffer-local 'notmuch-tree-unthreaded) + +;;; Options (defgroup notmuch-tree nil "Showing message and thread structure." @@ -114,7 +118,9 @@ For example: notmuch-unthreaded-result-format notmuch-tree-result-format)) -;; Faces for messages that match the query. +;;; Faces +;;;; Faces for messages that match the query + (defface notmuch-tree-match-face '((t :inherit default)) "Default face used in tree mode face for matching messages" @@ -165,7 +171,8 @@ For example: :group 'notmuch-tree :group 'notmuch-faces) -;; Faces for messages that do not match the query. +;;;; Faces for messages that do not match the query + (defface notmuch-tree-no-match-face '((t (:foreground "gray"))) "Default face used in tree mode face for non-matching messages." @@ -202,105 +209,121 @@ For example: :group 'notmuch-tree :group 'notmuch-faces) -(defvar notmuch-tree-previous-subject +;;; Variables + +(defvar-local notmuch-tree-previous-subject "The subject of the most recent result shown during the async display.") -(make-variable-buffer-local 'notmuch-tree-previous-subject) -(defvar notmuch-tree-basic-query nil +(defvar-local notmuch-tree-basic-query nil "A buffer local copy of argument query to the function notmuch-tree.") -(make-variable-buffer-local 'notmuch-tree-basic-query) -(defvar notmuch-tree-query-context nil +(defvar-local notmuch-tree-query-context nil "A buffer local copy of argument query-context to the function notmuch-tree.") -(make-variable-buffer-local 'notmuch-tree-query-context) -(defvar notmuch-tree-target-msg nil +(defvar-local notmuch-tree-target-msg nil "A buffer local copy of argument target to the function notmuch-tree.") -(make-variable-buffer-local 'notmuch-tree-target-msg) -(defvar notmuch-tree-open-target nil +(defvar-local notmuch-tree-open-target nil "A buffer local copy of argument open-target to the function notmuch-tree.") -(make-variable-buffer-local 'notmuch-tree-open-target) -(defvar notmuch-tree-parent-buffer nil) -(make-variable-buffer-local 'notmuch-tree-parent-buffer) +(defvar-local notmuch-tree-parent-buffer nil) -(defvar notmuch-tree-message-window nil +(defvar-local notmuch-tree-message-window nil "The window of the message pane. It is set in both the tree buffer and the child show buffer. It is used to try and close the message pane when quitting tree view or the child show buffer.") -(make-variable-buffer-local 'notmuch-tree-message-window) (put 'notmuch-tree-message-window 'permanent-local t) -(defvar notmuch-tree-message-buffer nil +(defvar-local notmuch-tree-message-buffer nil "The buffer name of the show buffer in the message pane. This is used to try and make sure we don't close the message pane if the user has loaded a different buffer in that window.") -(make-variable-buffer-local 'notmuch-tree-message-buffer) (put 'notmuch-tree-message-buffer 'permanent-local t) -(defun notmuch-tree-to-message-pane (func) - "Execute FUNC in message pane. +;;; Tree wrapper commands -This function returns a function (so can be used as a keybinding) -which executes function FUNC in the message pane if it is -open (if the message pane is closed it does nothing)." - `(lambda () - ,(concat "(In message pane) " (documentation func t)) +(defmacro notmuch-tree--define-do-in-message-window (name cmd) + "Define NAME as a command that calls CMD interactively in the message window. +If the message pane is closed then this command does nothing. +Avoid using this macro in new code; it will be removed." + `(defun ,name () + ,(concat "(In message window) " (documentation cmd t)) (interactive) (when (window-live-p notmuch-tree-message-window) (with-selected-window notmuch-tree-message-window - (call-interactively #',func))))) - -(defun notmuch-tree-inherit-from-message-pane (sym) - "Return value of SYM in message-pane if open, or tree-pane if not." + (call-interactively #',cmd))))) + +(notmuch-tree--define-do-in-message-window + notmuch-tree-previous-message-button + notmuch-show-previous-button) +(notmuch-tree--define-do-in-message-window + notmuch-tree-next-message-button + notmuch-show-next-button) +(notmuch-tree--define-do-in-message-window + notmuch-tree-toggle-message-process-crypto + notmuch-show-toggle-process-crypto) + +(defun notmuch-tree--message-process-crypto () + "Return value of `notmuch-show-process-crypto' in the message window. +If that window isn't alive, then return the current value. +Avoid using this function in new code; it will be removed." (if (window-live-p notmuch-tree-message-window) (with-selected-window notmuch-tree-message-window - (symbol-value sym)) - (symbol-value sym))) - -(defun notmuch-tree-button-activate (&optional button) - "Activate BUTTON or button at point. - -This function does not give an error if there is no button." - (interactive) - (let ((button (or button (button-at (point))))) - (when button (button-activate button)))) - -(defun notmuch-tree-close-message-pane-and (func) - "Close message pane and execute FUNC. - -This function returns a function (so can be used as a keybinding) -which closes the message pane if open and then executes function -FUNC." - `(lambda () - ,(concat "(Close message pane and) " (documentation func t)) + notmuch-show-process-crypto) + notmuch-show-process-crypto)) + +(defmacro notmuch-tree--define-close-message-window-and (name cmd) + "Define NAME as a variant of CMD. + +NAME determines the value of `notmuch-show-process-crypto' in the +message window, closes the window, and then call CMD interactively +with that value let-bound. If the message window does not exist, +then NAME behaves like CMD." + `(defun ,name () + ,(concat "(Close message pane and) " (documentation cmd t)) (interactive) (let ((notmuch-show-process-crypto - (notmuch-tree-inherit-from-message-pane 'notmuch-show-process-crypto))) + (notmuch-tree--message-process-crypto))) (notmuch-tree-close-message-window) - (call-interactively #',func)))) + (call-interactively #',cmd)))) + +(notmuch-tree--define-close-message-window-and + notmuch-tree-help + notmuch-help) +(notmuch-tree--define-close-message-window-and + notmuch-tree-new-mail + notmuch-mua-new-mail) +(notmuch-tree--define-close-message-window-and + notmuch-tree-jump-search + notmuch-jump-search) +(notmuch-tree--define-close-message-window-and + notmuch-tree-forward-message + notmuch-show-forward-message) +(notmuch-tree--define-close-message-window-and + notmuch-tree-reply-sender + notmuch-show-reply-sender) +(notmuch-tree--define-close-message-window-and + notmuch-tree-reply + notmuch-show-reply) +(notmuch-tree--define-close-message-window-and + notmuch-tree-view-raw-message + notmuch-show-view-raw-message) + +;;; Keymap (defvar notmuch-tree-mode-map (let ((map (make-sparse-keymap))) (set-keymap-parent map notmuch-common-keymap) - ;; The following override the global keymap. - ;; Override because we want to close message pane first. - (define-key map [remap notmuch-help] - (notmuch-tree-close-message-pane-and #'notmuch-help)) - ;; Override because we first close message pane and then close tree buffer. + ;; These bindings shadow common bindings with variants + ;; that additionally close the message window. (define-key map [remap notmuch-bury-or-kill-this-buffer] 'notmuch-tree-quit) - ;; Override because we close message pane after the search query is entered. - (define-key map [remap notmuch-search] 'notmuch-tree-to-search) - ;; Override because we want to close message pane first. - (define-key map [remap notmuch-mua-new-mail] - (notmuch-tree-close-message-pane-and #'notmuch-mua-new-mail)) - ;; Override because we want to close message pane first. - (define-key map [remap notmuch-jump-search] - (notmuch-tree-close-message-pane-and #'notmuch-jump-search)) + (define-key map [remap notmuch-search] 'notmuch-tree-to-search) + (define-key map [remap notmuch-help] 'notmuch-tree-help) + (define-key map [remap notmuch-mua-new-mail] 'notmuch-tree-new-mail) + (define-key map [remap notmuch-jump-search] 'notmuch-tree-jump-search) (define-key map "S" 'notmuch-search-from-tree-current-query) (define-key map "U" 'notmuch-unthreaded-from-tree-current-query) @@ -314,24 +337,16 @@ FUNC." (define-key map "b" 'notmuch-show-resend-message) ;; these apply to the message pane - (define-key map (kbd "M-TAB") - (notmuch-tree-to-message-pane #'notmuch-show-previous-button)) - (define-key map (kbd "") - (notmuch-tree-to-message-pane #'notmuch-show-previous-button)) - (define-key map (kbd "TAB") - (notmuch-tree-to-message-pane #'notmuch-show-next-button)) - (define-key map "$" - (notmuch-tree-to-message-pane #'notmuch-show-toggle-process-crypto)) + (define-key map (kbd "M-TAB") 'notmuch-tree-previous-message-button) + (define-key map (kbd "") 'notmuch-tree-previous-message-button) + (define-key map (kbd "TAB") 'notmuch-tree-next-message-button) + (define-key map "$" 'notmuch-tree-toggle-message-process-crypto) ;; bindings from show (or elsewhere) but we close the message pane first. - (define-key map "f" - (notmuch-tree-close-message-pane-and #'notmuch-show-forward-message)) - (define-key map "r" - (notmuch-tree-close-message-pane-and #'notmuch-show-reply-sender)) - (define-key map "R" - (notmuch-tree-close-message-pane-and #'notmuch-show-reply)) - (define-key map "V" - (notmuch-tree-close-message-pane-and #'notmuch-show-view-raw-message)) + (define-key map "f" 'notmuch-tree-forward-message) + (define-key map "r" 'notmuch-tree-reply-sender) + (define-key map "R" 'notmuch-tree-reply) + (define-key map "V" 'notmuch-tree-view-raw-message) ;; The main tree view bindings (define-key map (kbd "RET") 'notmuch-tree-show-message) @@ -354,8 +369,10 @@ FUNC." (define-key map " " 'notmuch-tree-scroll-or-next) (define-key map (kbd "DEL") 'notmuch-tree-scroll-message-window-back) (define-key map "e" 'notmuch-tree-resume-message) - map)) -(fset 'notmuch-tree-mode-map notmuch-tree-mode-map) + map) + "Keymap for \"notmuch tree\" buffers.") + +;;; Message properties (defun notmuch-tree-get-message-properties () "Return the properties of the current message as a plist. @@ -382,9 +399,8 @@ Some useful entries are: (notmuch-tree-set-message-properties props))) (defun notmuch-tree-get-prop (prop &optional props) - (let ((props (or props - (notmuch-tree-get-message-properties)))) - (plist-get props prop))) + (plist-get (or props (notmuch-tree-get-message-properties)) + prop)) (defun notmuch-tree-set-tags (tags) "Set the tags of the current message." @@ -405,9 +421,10 @@ Some useful entries are: (defun notmuch-tree-get-match () "Return whether the current message is a match." - (interactive) (notmuch-tree-get-prop :match)) +;;; Update display + (defun notmuch-tree-refresh-result () "Redisplay the current message line. @@ -450,6 +467,8 @@ NOT change the database." (when (string= tree-msg-id (notmuch-show-get-message-id)) (notmuch-show-update-tags new-tags))))))) +;;; Commands (and some helper functions used by them) + (defun notmuch-tree-tag (tag-changes) "Change tags for the current message." (interactive @@ -526,9 +545,10 @@ NOT change the database." (let ((buffer (current-buffer))) (when (and (window-live-p notmuch-tree-message-window) (eq (window-buffer notmuch-tree-message-window) buffer)) - ;; We do not want an error if this is the sole window in the - ;; frame and I do not know how to test for that in emacs pre - ;; 24. Hence we just ignore-errors. + ;; We could check whether this is the only window in its frame, + ;; but simply ignoring the error that is thrown otherwise is + ;; what we had to do for Emacs 24 and we stick to that because + ;; it is still the simplest approach. (ignore-errors (delete-window notmuch-tree-message-window))))) @@ -574,8 +594,7 @@ NOT change the database." "Show the current message (in whole window)." (interactive) (let ((id (notmuch-tree-get-message-id)) - (inhibit-read-only t) - buffer) + (inhibit-read-only t)) (when id ;; We close the window to kill off un-needed buffers. (notmuch-tree-close-message-window) @@ -768,8 +787,7 @@ search results instead." (notmuch-tree-from-search-thread)))) (defun notmuch-tree-next-thread (&optional previous) - "Move to the next thread in the current tree or parent search -results + "Move to the next thread in the current tree or parent search results. If PREVIOUS is non-nil, move to the previous thread in the tree or search results instead." @@ -779,14 +797,13 @@ search results instead." (notmuch-tree-next-thread-from-search previous))) (defun notmuch-tree-prev-thread () - "Move to the previous thread in the current tree or parent search -results" + "Move to the previous thread in the current tree or parent search results." (interactive) (notmuch-tree-next-thread t)) (defun notmuch-tree-thread-mapcar (function) - "Iterate through all messages in the current thread - and call FUNCTION for side effects." + "Call FUNCTION for each message in the current thread. +FUNCTION is called for side effects only." (save-excursion (notmuch-tree-thread-top) (cl-loop collect (funcall function) @@ -828,7 +845,7 @@ buffer." (notmuch-tree-tag-thread (notmuch-tag-change-list notmuch-archive-tags unarchive)))) -;; Functions below here display the tree buffer itself. +;;; Functions for displaying the tree buffer itself (defun notmuch-tree-clean-address (address) "Try to clean a single email ADDRESS for display. Return @@ -1009,19 +1026,17 @@ Complete list of currently available key bindings: (setq buffer-read-only t) (setq truncate-lines t)) -(defun notmuch-tree-process-sentinel (proc msg) +(defun notmuch-tree-process-sentinel (proc _msg) "Add a message to let user know when \"notmuch tree\" exits." (let ((buffer (process-buffer proc)) (status (process-status proc)) - (exit-status (process-exit-status proc)) - (never-found-target-thread nil)) + (exit-status (process-exit-status proc))) (when (memq status '(exit signal)) (kill-buffer (process-get proc 'parse-buf)) (when (buffer-live-p buffer) (with-current-buffer buffer (save-excursion - (let ((inhibit-read-only t) - (atbob (bobp))) + (let ((inhibit-read-only t)) (goto-char (point-max)) (when (eq status 'signal) (insert "Incomplete search results (tree view process was killed).\n")) @@ -1035,8 +1050,7 @@ Complete list of currently available key bindings: "Process and filter the output of \"notmuch show\" for tree view." (let ((results-buf (process-buffer proc)) (parse-buf (process-get proc 'parse-buf)) - (inhibit-read-only t) - done) + (inhibit-read-only t)) (if (not (buffer-live-p results-buf)) (delete-process proc) (with-current-buffer parse-buf @@ -1126,7 +1140,7 @@ The arguments are: (inhibit-read-only t)) (pop-to-buffer-same-window buffer)) ;; Don't track undo information for this buffer - (set 'buffer-undo-list t) + (setq buffer-undo-list t) (notmuch-tree-worker query query-context target open-target unthreaded) (setq notmuch-tree-parent-buffer parent-buffer) (setq truncate-lines t)) @@ -1135,7 +1149,7 @@ The arguments are: (interactive) (notmuch-tree query query-context target buffer-name open-target t)) -;; +;;; _ (provide 'notmuch-tree) diff --git a/emacs/notmuch-wash.el b/emacs/notmuch-wash.el index ce4b9637..653ecc2a 100644 --- a/emacs/notmuch-wash.el +++ b/emacs/notmuch-wash.el @@ -1,4 +1,4 @@ -;;; notmuch-wash.el --- cleaning up message bodies +;;; notmuch-wash.el --- cleaning up message bodies -*- lexical-binding: t -*- ;; ;; Copyright © Carl Worth ;; Copyright © David Edmondson @@ -24,13 +24,14 @@ ;;; Code: (require 'coolj) +(require 'diff-mode) (require 'notmuch-lib) (declare-function notmuch-show-insert-bodypart "notmuch-show" (msg part depth &optional hide)) (defvar notmuch-show-indent-messages-width) -;; +;;; Options (defgroup notmuch-wash nil "Cleaning up messages for display." @@ -130,6 +131,8 @@ or at the window width (whichever one is lower)." (integer :tag "number of characters")) :group 'notmuch-wash) +;;; Faces + (defface notmuch-wash-toggle-button '((t (:inherit font-lock-comment-face))) "Face used for buttons toggling the visibility of washed away @@ -143,6 +146,8 @@ message parts." :group 'notmuch-wash :group 'notmuch-faces) +;;; Buttons + (defun notmuch-wash-toggle-invisible-action (cite-button) ;; Toggle overlay visibility (let ((overlay (button-get cite-button 'overlay))) @@ -196,7 +201,7 @@ message parts." (overlay-end overlay)))) (format label-format lines-count))) -(defun notmuch-wash-region-to-button (msg beg end type &optional prefix) +(defun notmuch-wash-region-to-button (beg end type &optional prefix) "Auxiliary function to do the actual making of overlays and buttons. BEG and END are buffer locations. TYPE should a string, either @@ -225,17 +230,17 @@ that PREFIX should not include a newline." :type button-type))) (overlay-put overlay 'notmuch-wash-button button)))))) -(defun notmuch-wash-excerpt-citations (msg depth) +;;; Hook functions + +(defun notmuch-wash-excerpt-citations (_msg _depth) "Excerpt citations and up to one signature." (goto-char (point-min)) (beginning-of-line) (when (and (< (point) (point-max)) (re-search-forward notmuch-wash-original-regexp nil t)) - (let* ((msg-start (match-beginning 0)) - (msg-end (point-max)) - (msg-lines (count-lines msg-start msg-end))) - (notmuch-wash-region-to-button - msg msg-start msg-end "original"))) + (notmuch-wash-region-to-button (match-beginning 0) + (point-max) + "original")) (while (and (< (point) (point-max)) (re-search-forward notmuch-wash-citation-regexp nil t)) (let* ((cite-start (match-beginning 0)) @@ -252,14 +257,13 @@ that PREFIX should not include a newline." (goto-char cite-end) (forward-line (- notmuch-wash-citation-lines-suffix)) (notmuch-wash-region-to-button - msg hidden-start (point-marker) + hidden-start (point-marker) "citation"))))) (when (and (not (eobp)) (re-search-forward notmuch-wash-signature-regexp nil t)) - (let* ((sig-start (match-beginning 0)) - (sig-end (match-end 0)) - (sig-lines (count-lines sig-start (point-max)))) - (when (<= sig-lines notmuch-wash-signature-lines-max) + (let ((sig-start (match-beginning 0))) + (when (<= (count-lines sig-start (point-max)) + notmuch-wash-signature-lines-max) (let ((sig-start-marker (make-marker)) (sig-end-marker (make-marker))) (set-marker sig-start-marker sig-start) @@ -267,12 +271,10 @@ that PREFIX should not include a newline." (overlay-put (make-overlay sig-start-marker sig-end-marker) 'face 'message-cited-text) (notmuch-wash-region-to-button - msg sig-start-marker sig-end-marker + sig-start-marker sig-end-marker "signature")))))) -;; - -(defun notmuch-wash-elide-blank-lines (msg depth) +(defun notmuch-wash-elide-blank-lines (_msg _depth) "Elide leading, trailing and successive blank lines." ;; Algorithm derived from `article-strip-multiple-blank-lines' in ;; `gnus-art.el'. @@ -293,9 +295,7 @@ that PREFIX should not include a newline." (when (looking-at "\n") (delete-region (match-beginning 0) (match-end 0)))) -;; - -(defun notmuch-wash-tidy-citations (msg depth) +(defun notmuch-wash-tidy-citations (_msg _depth) "Improve the display of cited regions of a message. Perform several transformations on the message body: @@ -319,9 +319,7 @@ Perform several transformations on the message body: (while (re-search-forward "\\(^>[> ]*\n\\)\\(^$\\|^[^>].*\\)" nil t) (replace-match "\\2"))) -;; - -(defun notmuch-wash-wrap-long-lines (msg depth) +(defun notmuch-wash-wrap-long-lines (_msg depth) "Wrap long lines in the message. If `notmuch-wash-wrap-lines-length' is a number, this will wrap @@ -342,11 +340,7 @@ the wrapped text are maintained." 2))) (coolj-wrap-region (point-min) (point-max)))) -;; - -(require 'diff-mode) - -(defvar diff-file-header-re) ; From `diff-mode.el'. +;;;; Convert Inline Patches (defun notmuch-wash-subject-to-filename (subject &optional maxlen) "Convert a mail SUBJECT into a filename. @@ -417,7 +411,7 @@ for error." (delete-region (point-min) (point-max)) (notmuch-show-insert-bodypart nil part depth))))) -;; +;;; _ (provide 'notmuch-wash) diff --git a/emacs/notmuch.el b/emacs/notmuch.el index 83bcee57..6d37c623 100644 --- a/emacs/notmuch.el +++ b/emacs/notmuch.el @@ -1,4 +1,4 @@ -;;; notmuch.el --- run notmuch within emacs +;;; notmuch.el --- run notmuch within emacs -*- lexical-binding: t -*- ;; ;; Copyright © Carl Worth ;; @@ -65,11 +65,11 @@ ;;; Code: -(eval-when-compile (require 'cl-lib)) - (require 'mm-view) (require 'message) +(require 'hl-line) + (require 'notmuch-lib) (require 'notmuch-tag) (require 'notmuch-show) @@ -80,6 +80,8 @@ (require 'notmuch-message) (require 'notmuch-parser) +;;; Options + (defcustom notmuch-search-result-format `(("date" . "%12s ") ("count" . "%-7s ") @@ -112,8 +114,14 @@ there will be called at other points of notmuch execution." :type 'file :group 'notmuch) -(defvar notmuch-query-history nil - "Variable to store minibuffer history for notmuch queries.") +(defcustom notmuch-search-hook '(notmuch-hl-line-mode) + "List of functions to call when notmuch displays the search results." + :type 'hook + :options '(notmuch-hl-line-mode) + :group 'notmuch-search + :group 'notmuch-hooks) + +;;; Mime Utilities (defun notmuch-foreach-mime-part (function mm-handle) (cond ((stringp (car mm-handle)) @@ -151,25 +159,13 @@ there will be called at other points of notmuch execution." (mm-save-part p)))) mm-handle)) -(require 'hl-line) - -(defun notmuch-hl-line-mode () - (prog1 (hl-line-mode) - (when hl-line-overlay - (overlay-put hl-line-overlay 'priority 1)))) - -(defcustom notmuch-search-hook '(notmuch-hl-line-mode) - "List of functions to call when notmuch displays the search results." - :type 'hook - :options '(notmuch-hl-line-mode) - :group 'notmuch-search - :group 'notmuch-hooks) +;;; Keymap (defvar notmuch-search-mode-map (let ((map (make-sparse-keymap))) (set-keymap-parent map notmuch-common-keymap) (define-key map "x" 'notmuch-bury-or-kill-this-buffer) - (define-key map (kbd "") 'notmuch-search-scroll-down) + (define-key map (kbd "DEL") 'notmuch-search-scroll-down) (define-key map "b" 'notmuch-search-scroll-down) (define-key map " " 'notmuch-search-scroll-up) (define-key map "<" 'notmuch-search-first-thread) @@ -194,7 +190,17 @@ there will be called at other points of notmuch execution." (define-key map "U" 'notmuch-unthreaded-from-search-current-query) map) "Keymap for \"notmuch search\" buffers.") -(fset 'notmuch-search-mode-map notmuch-search-mode-map) + +;;; Internal Variables + +(defvar notmuch-query-history nil + "Variable to store minibuffer history for notmuch queries.") + +(defvar-local notmuch-search-query-string nil) +(defvar-local notmuch-search-target-thread nil) +(defvar-local notmuch-search-target-line nil) + +;;; Stashing (defvar notmuch-search-stash-map (let ((map (make-sparse-keymap))) @@ -213,13 +219,9 @@ there will be called at other points of notmuch execution." (defun notmuch-stash-query () "Copy current query to kill-ring." (interactive) - (notmuch-common-do-stash (notmuch-search-get-query))) - -(defvar notmuch-search-query-string) -(defvar notmuch-search-target-thread) -(defvar notmuch-search-target-line) + (notmuch-common-do-stash notmuch-search-query-string)) -(defvar notmuch-search-disjunctive-regexp "\\<[oO][rR]\\>") +;;; Movement (defun notmuch-search-scroll-up () "Move forward through search results by one window's worth." @@ -272,6 +274,8 @@ there will be called at other points of notmuch execution." (interactive) (goto-char (point-min))) +;;; Faces + (defface notmuch-message-summary-face `((((class color) (background light)) ,@(and (>= emacs-major-version 27) '(:extend t)) @@ -343,7 +347,7 @@ there will be called at other points of notmuch execution." "Face used in search mode face for flagged threads. This face is the default value for the \"flagged\" tag in -`notmuch-search-line-faces`." +`notmuch-search-line-faces'." :group 'notmuch-search :group 'notmuch-faces) @@ -353,10 +357,12 @@ This face is the default value for the \"flagged\" tag in "Face used in search mode for unread threads. This face is the default value for the \"unread\" tag in -`notmuch-search-line-faces`." +`notmuch-search-line-faces'." :group 'notmuch-search :group 'notmuch-faces) +;;; Mode + (define-derived-mode notmuch-search-mode fundamental-mode "notmuch-search" "Major mode displaying results of a notmuch search. @@ -387,12 +393,8 @@ new, global search. Complete list of currently available key bindings: \\{notmuch-search-mode-map}" - (make-local-variable 'notmuch-search-query-string) - (make-local-variable 'notmuch-search-oldest-first) - (make-local-variable 'notmuch-search-target-thread) - (make-local-variable 'notmuch-search-target-line) (setq notmuch-buffer-refresh-function #'notmuch-search-refresh-view) - (set (make-local-variable 'scroll-preserve-screen-position) t) + (setq-local scroll-preserve-screen-position t) (add-to-invisibility-spec (cons 'ellipsis t)) (setq truncate-lines t) (setq buffer-read-only t) @@ -401,6 +403,8 @@ Complete list of currently available key bindings: (setq imenu-extract-index-name-function #'notmuch-search-imenu-extract-index-name-function)) +;;; Search Results + (defun notmuch-search-get-result (&optional pos) "Return the result object for the thread at POS (or point). @@ -473,7 +477,7 @@ If BARE is set then do not prefix with \"thread:\"." (defun notmuch-search-find-stable-query () "Return the stable queries for the current thread. -This returns a list (MATCHED-QUERY UNMATCHED-QUERY) for the +Return a list (MATCHED-QUERY UNMATCHED-QUERY) for the matched and unmatched messages in the current thread." (plist-get (notmuch-search-get-result) :query)) @@ -515,17 +519,16 @@ With a prefix argument, invert the default value of `notmuch-show-only-matching-messages' when displaying the thread." (interactive "P") - (let ((thread-id (notmuch-search-find-thread-id)) - (subject (notmuch-search-find-subject))) - (if (> (length thread-id) 0) + (let ((thread-id (notmuch-search-find-thread-id))) + (if thread-id (notmuch-show thread-id elide-toggle (current-buffer) notmuch-search-query-string ;; Name the buffer based on the subject. - (concat "*" - (truncate-string-to-width subject 30 nil nil t) - "*")) + (format "*%s*" (truncate-string-to-width + (notmuch-search-find-subject) + 30 nil nil t))) (message "End of search results.")))) (defun notmuch-tree-from-search-current-query () @@ -550,18 +553,21 @@ thread." (defun notmuch-search-reply-to-thread (&optional prompt-for-sender) "Begin composing a reply-all to the entire current thread in a new buffer." (interactive "P") - (let ((message-id (notmuch-search-find-thread-id))) - (notmuch-mua-new-reply message-id prompt-for-sender t))) + (notmuch-mua-new-reply (notmuch-search-find-thread-id) + prompt-for-sender t)) (defun notmuch-search-reply-to-thread-sender (&optional prompt-for-sender) "Begin composing a reply to the entire current thread in a new buffer." (interactive "P") - (let ((message-id (notmuch-search-find-thread-id))) - (notmuch-mua-new-reply message-id prompt-for-sender nil))) + (notmuch-mua-new-reply (notmuch-search-find-thread-id) + prompt-for-sender nil)) + +;;; Tags (defun notmuch-search-set-tags (tags &optional pos) - (let ((new-result (plist-put (notmuch-search-get-result pos) :tags tags))) - (notmuch-search-update-result new-result pos))) + (notmuch-search-update-result + (plist-put (notmuch-search-get-result pos) :tags tags) + pos)) (defun notmuch-search-get-tags (&optional pos) (plist-get (notmuch-search-get-result pos) :tags)) @@ -571,12 +577,12 @@ thread." (notmuch-search-foreach-result beg end (lambda (pos) (setq output (append output (notmuch-search-get-tags pos))))) - output)) + (delete-dups output))) (defun notmuch-search-interactive-tag-changes (&optional initial-input) "Prompt for tag changes for the current thread or region. -Returns (TAG-CHANGES REGION-BEGIN REGION-END)." +Return (TAG-CHANGES REGION-BEGIN REGION-END)." (pcase-let ((`(,beg ,end) (notmuch-interactive-region))) (list (notmuch-read-tag-changes (notmuch-search-get-tags-region beg end) (if (= beg end) "Tag thread" "Tag region") @@ -640,6 +646,8 @@ This function advances the next thread when finished." (when (eq beg end) (notmuch-search-next-thread))) +;;; Search Results + (defun notmuch-search-update-result (result &optional pos) "Replace the result object of the thread at POS (or point) by RESULT and redraw it. @@ -668,7 +676,7 @@ of the result." (min init-point (- new-end 1))))) (goto-char new-point))))) -(defun notmuch-search-process-sentinel (proc msg) +(defun notmuch-search-process-sentinel (proc _msg) "Add a message to let user know when \"notmuch search\" exits." (let ((buffer (process-buffer proc)) (status (process-status proc)) @@ -694,7 +702,7 @@ of the result." (throw 'return nil)) (when (and atbob (not (string= notmuch-search-target-thread "found"))) - (set 'never-found-target-thread t))))) + (setq never-found-target-thread t))))) (when (and never-found-target-thread notmuch-search-target-line) (goto-char (point-min)) @@ -806,13 +814,13 @@ non-authors is found, assume that all of the authors match." (setq invisible-string (notmuch-search-author-propertize invisible-string))) ;; If there is any invisible text, add it as a tooltip to the ;; visible text. - (unless (string= invisible-string "") + (unless (string-empty-p invisible-string) (setq visible-string (propertize visible-string 'help-echo (concat "..." invisible-string)))) ;; Insert the visible and, if present, invisible author strings. (insert visible-string) - (unless (string= invisible-string "") + (unless (string-empty-p invisible-string) (let ((start (point)) overlay) (insert invisible-string) @@ -871,8 +879,7 @@ sets the :orig-tag property." "Process and filter the output of \"notmuch search\"." (let ((results-buf (process-buffer proc)) (parse-buf (process-get proc 'parse-buf)) - (inhibit-read-only t) - done) + (inhibit-read-only t)) (when (buffer-live-p results-buf) (with-current-buffer parse-buf ;; Insert new data @@ -882,6 +889,8 @@ sets the :orig-tag property." (notmuch-sexp-parse-partial-list 'notmuch-search-append-result results-buf))))) +;;; Commands (and some helper functions used by them) + (defun notmuch-search-tag-all (tag-changes) "Add/remove tags from all messages in current search buffer. @@ -924,40 +933,39 @@ See `notmuch-tag' for information on the format of TAG-CHANGES." "Read a notmuch-query from the minibuffer with completion. PROMPT is the string to prompt with." - (let* - ((all-tags - (mapcar (lambda (tag) (notmuch-escape-boolean-term tag)) - (process-lines notmuch-command "search" "--output=tags" "*"))) - (completions - (append (list "folder:" "path:" "thread:" "id:" "date:" "from:" "to:" - "subject:" "attachment:") - (mapcar (lambda (tag) (concat "tag:" tag)) all-tags) - (mapcar (lambda (tag) (concat "is:" tag)) all-tags) - (mapcar (lambda (mimetype) (concat "mimetype:" mimetype)) - (mailcap-mime-types))))) - (let ((keymap (copy-keymap minibuffer-local-map)) - (current-query (cl-case major-mode - (notmuch-search-mode (notmuch-search-get-query)) - (notmuch-show-mode (notmuch-show-get-query)) - (notmuch-tree-mode (notmuch-tree-get-query)))) - (minibuffer-completion-table - (completion-table-dynamic - (lambda (string) - ;; generate a list of possible completions for the current input - (cond - ;; this ugly regexp is used to get the last word of the input - ;; possibly preceded by a '(' - ((string-match "\\(^\\|.* (?\\)\\([^ ]*\\)$" string) - (mapcar (lambda (compl) - (concat (match-string-no-properties 1 string) compl)) - (all-completions (match-string-no-properties 2 string) - completions))) - (t (list string))))))) - ;; this was simpler than convincing completing-read to accept spaces: - (define-key keymap (kbd "TAB") 'minibuffer-complete) - (let ((history-delete-duplicates t)) - (read-from-minibuffer prompt nil keymap nil - 'notmuch-search-history current-query nil))))) + (let* ((all-tags + (mapcar (lambda (tag) (notmuch-escape-boolean-term tag)) + (process-lines notmuch-command "search" "--output=tags" "*"))) + (completions + (append (list "folder:" "path:" "thread:" "id:" "date:" "from:" "to:" + "subject:" "attachment:") + (mapcar (lambda (tag) (concat "tag:" tag)) all-tags) + (mapcar (lambda (tag) (concat "is:" tag)) all-tags) + (mapcar (lambda (mimetype) (concat "mimetype:" mimetype)) + (mailcap-mime-types)))) + (keymap (copy-keymap minibuffer-local-map)) + (current-query (cl-case major-mode + (notmuch-search-mode (notmuch-search-get-query)) + (notmuch-show-mode (notmuch-show-get-query)) + (notmuch-tree-mode (notmuch-tree-get-query)))) + (minibuffer-completion-table + (completion-table-dynamic + (lambda (string) + ;; Generate a list of possible completions for the current input. + (cond + ;; This ugly regexp is used to get the last word of the input + ;; possibly preceded by a '('. + ((string-match "\\(^\\|.* (?\\)\\([^ ]*\\)$" string) + (mapcar (lambda (compl) + (concat (match-string-no-properties 1 string) compl)) + (all-completions (match-string-no-properties 2 string) + completions))) + (t (list string))))))) + ;; This was simpler than convincing completing-read to accept spaces: + (define-key keymap (kbd "TAB") 'minibuffer-complete) + (let ((history-delete-duplicates t)) + (read-from-minibuffer prompt nil keymap nil + 'notmuch-search-history current-query nil)))) (defun notmuch-search-get-query () "Return the current query in this search buffer." @@ -995,22 +1003,17 @@ the configured default sort order." (if no-display (set-buffer buffer) (pop-to-buffer-same-window buffer)) - ;; avoid wiping out third party buffer-local variables in the case - ;; where we're just refreshing or changing the sort order of an - ;; existing search results buffer - (unless (eq major-mode 'notmuch-search-mode) - (notmuch-search-mode)) + (notmuch-search-mode) ;; Don't track undo information for this buffer - (set 'buffer-undo-list t) - (set 'notmuch-search-query-string query) - (set 'notmuch-search-oldest-first oldest-first) - (set 'notmuch-search-target-thread target-thread) - (set 'notmuch-search-target-line target-line) + (setq buffer-undo-list t) + (setq notmuch-search-query-string query) + (setq notmuch-search-oldest-first oldest-first) + (setq notmuch-search-target-thread target-thread) + (setq notmuch-search-target-line target-line) (notmuch-tag-clear-cache) - (let ((proc (get-buffer-process (current-buffer))) - (inhibit-read-only t)) - (when proc - (error "notmuch search process already running for query `%s'" query)) + (when (get-buffer-process buffer) + (error "notmuch search process already running for query `%s'" query)) + (let ((inhibit-read-only t)) (erase-buffer) (goto-char (point-min)) (save-excursion @@ -1020,12 +1023,12 @@ the configured default sort order." (if oldest-first "--sort=oldest-first" "--sort=newest-first") - query)) - ;; Use a scratch buffer to accumulate partial output. - ;; This buffer will be killed by the sentinel, which - ;; should be called no matter how the process dies. - (parse-buf (generate-new-buffer " *notmuch search parse*"))) - (process-put proc 'parse-buf parse-buf) + query))) + ;; Use a scratch buffer to accumulate partial output. + ;; This buffer will be killed by the sentinel, which + ;; should be called no matter how the process dies. + (process-put proc 'parse-buf + (generate-new-buffer " *notmuch search parse*")) (set-process-filter proc 'notmuch-search-process-filter) (set-process-query-on-exit-flag proc nil)))) (run-hooks 'notmuch-search-hook))) @@ -1039,13 +1042,12 @@ the new search results, then point will be placed on the same thread. Otherwise, point will be moved to attempt to be in the same relative position within the new buffer." (interactive) - (let ((target-line (line-number-at-pos)) - (oldest-first notmuch-search-oldest-first) - (target-thread (notmuch-search-find-thread-id 'bare)) - (query notmuch-search-query-string)) - ;; notmuch-search erases the current buffer. - (notmuch-search query oldest-first target-thread target-line t) - (goto-char (point-min)))) + (notmuch-search notmuch-search-query-string + notmuch-search-oldest-first + (notmuch-search-find-thread-id 'bare) + (line-number-at-pos) + t) + (goto-char (point-min))) (defun notmuch-search-toggle-order () "Toggle the current search order. @@ -1053,15 +1055,13 @@ same relative position within the new buffer." This command toggles the sort order for the current search. The default sort order is defined by `notmuch-search-oldest-first'." (interactive) - (set 'notmuch-search-oldest-first (not notmuch-search-oldest-first)) + (setq notmuch-search-oldest-first (not notmuch-search-oldest-first)) (notmuch-search-refresh-view)) (defun notmuch-group-disjunctive-query-string (query-string) "Group query if it contains a complex expression. - -Enclose QUERY-STRING in parentheses if it matches -`notmuch-search-disjunctive-regexp'." - (if (string-match-p notmuch-search-disjunctive-regexp query-string) +Enclose QUERY-STRING in parentheses if contains \"OR\" operators." + (if (string-match-p "\\<[oO][rR]\\>" query-string) (concat "( " query-string " )") query-string)) @@ -1080,10 +1080,10 @@ current search results AND the additional query string provided." notmuch-search-oldest-first))) (defun notmuch-search-filter-by-tag (tag) - "Filter the current search results based on a single tag. + "Filter the current search results based on a single TAG. -Runs a new search matching only messages that match both the -current search results AND that are tagged with the given tag." +Run a new search matching only messages that match the current +search results and that are also tagged with the given TAG." (interactive (list (notmuch-select-tag-with-completion "Filter by tag: " notmuch-search-query-string))) @@ -1103,7 +1103,7 @@ current search results AND that are tagged with the given tag." (notmuch-hello)) (defun notmuch-interesting-buffer (b) - "Is the current buffer of interest to a notmuch user?" + "Whether the current buffer's major-mode is a notmuch mode." (with-current-buffer b (memq major-mode '(notmuch-show-mode notmuch-search-mode @@ -1115,8 +1115,8 @@ current search results AND that are tagged with the given tag." (defun notmuch-cycle-notmuch-buffers () "Cycle through any existing notmuch buffers (search, show or hello). -If the current buffer is the only notmuch buffer, bury it. If no -notmuch buffers exist, run `notmuch'." +If the current buffer is the only notmuch buffer, bury it. +If no notmuch buffers exist, run `notmuch'." (interactive) (let (start first) ;; If the current buffer is a notmuch buffer, remember it and then @@ -1137,22 +1137,30 @@ notmuch buffers exist, run `notmuch'." (pop-to-buffer-same-window first)) (notmuch)))) +;;; Integrations +;;;; Hl-line Support + +(defun notmuch-hl-line-mode () + (prog1 (hl-line-mode) + (when hl-line-overlay + (overlay-put hl-line-overlay 'priority 1)))) + ;;;; Imenu Support (defun notmuch-search-imenu-prev-index-position-function () "Move point to previous message in notmuch-search buffer. -This function is used as a value for -`imenu-prev-index-position-function'." +Used as`imenu-prev-index-position-function' in notmuch buffers." (notmuch-search-previous-thread)) (defun notmuch-search-imenu-extract-index-name-function () "Return imenu name for line at point. -This function is used as a value for -`imenu-extract-index-name-function'. Point should be at the -beginning of the line." - (let ((subject (notmuch-search-find-subject)) - (author (notmuch-search-find-authors))) - (format "%s (%s)" subject author))) +Used as `imenu-extract-index-name-function' in notmuch buffers. +Point should be at the beginning of the line." + (format "%s (%s)" + (notmuch-search-find-subject) + (notmuch-search-find-authors))) + +;;; _ (setq mail-user-agent 'notmuch-user-agent) diff --git a/emacs/rstdoc.el b/emacs/rstdoc.el index 4221f142..c7c13015 100644 --- a/emacs/rstdoc.el +++ b/emacs/rstdoc.el @@ -1,4 +1,4 @@ -;;; rstdoc.el --- help generate documentation from docstrings -*-lexical-binding: t-*- +;;; rstdoc.el --- help generate documentation from docstrings -*- lexical-binding: t -*- ;; Copyright (C) 2018 David Bremner diff --git a/gmime-filter-reply.h b/gmime-filter-reply.h index 5a1e606e..7cdefcd1 100644 --- a/gmime-filter-reply.h +++ b/gmime-filter-reply.h @@ -24,11 +24,17 @@ G_BEGIN_DECLS #define GMIME_TYPE_FILTER_REPLY (g_mime_filter_reply_get_type ()) -#define GMIME_FILTER_REPLY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GMIME_TYPE_FILTER_REPLY, GMimeFilterReply)) -#define GMIME_FILTER_REPLY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GMIME_TYPE_FILTER_REPLY, GMimeFilterReplyClass)) -#define GMIME_IS_FILTER_REPLY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GMIME_TYPE_FILTER_REPLY)) -#define GMIME_IS_FILTER_REPLY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GMIME_TYPE_FILTER_REPLY)) -#define GMIME_FILTER_REPLY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GMIME_TYPE_FILTER_REPLY, GMimeFilterReplyClass)) +#define GMIME_FILTER_REPLY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), \ + GMIME_TYPE_FILTER_REPLY, \ + GMimeFilterReply)) +#define GMIME_FILTER_REPLY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GMIME_TYPE_FILTER_REPLY, \ + GMimeFilterReplyClass)) +#define GMIME_IS_FILTER_REPLY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \ + GMIME_TYPE_FILTER_REPLY)) +#define GMIME_IS_FILTER_REPLY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), \ + GMIME_TYPE_FILTER_REPLY)) +#define GMIME_FILTER_REPLY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GMIME_TYPE_FILTER_REPLY, \ + GMimeFilterReplyClass)) typedef struct _GMimeFilterReply GMimeFilterReply; typedef struct _GMimeFilterReplyClass GMimeFilterReplyClass; diff --git a/hooks.c b/hooks.c index 59c58070..ec89b22e 100644 --- a/hooks.c +++ b/hooks.c @@ -24,14 +24,15 @@ #include int -notmuch_run_hook (const char *db_path, const char *hook) +notmuch_run_hook (notmuch_database_t *notmuch, const char *hook) { char *hook_path; int status = 0; pid_t pid; - hook_path = talloc_asprintf (NULL, "%s/%s/%s/%s", db_path, ".notmuch", - "hooks", hook); + hook_path = talloc_asprintf (notmuch, "%s/%s", + notmuch_config_get (notmuch, NOTMUCH_CONFIG_HOOK_DIR), + hook); if (hook_path == NULL) { fprintf (stderr, "Out of memory\n"); return 1; diff --git a/lib/Makefile.local b/lib/Makefile.local index a6400126..01cbb3f2 100644 --- a/lib/Makefile.local +++ b/lib/Makefile.local @@ -59,7 +59,10 @@ libnotmuch_cxx_srcs = \ $(dir)/config.cc \ $(dir)/regexp-fields.cc \ $(dir)/thread.cc \ - $(dir)/thread-fp.cc + $(dir)/thread-fp.cc \ + $(dir)/features.cc \ + $(dir)/prefix.cc \ + $(dir)/open.cc libnotmuch_modules := $(libnotmuch_c_srcs:.c=.o) $(libnotmuch_cxx_srcs:.cc=.o) diff --git a/lib/add-message.cc b/lib/add-message.cc index b47aa501..d6e5e73d 100644 --- a/lib/add-message.cc +++ b/lib/add-message.cc @@ -532,7 +532,9 @@ notmuch_database_index_file (notmuch_database_t *notmuch, goto DONE; } - _notmuch_message_add_filename (message, filename); + ret = _notmuch_message_add_filename (message, filename); + if (ret) + goto DONE; if (is_new || is_ghost) { _notmuch_message_add_term (message, "type", "mail"); diff --git a/lib/config.cc b/lib/config.cc index 0b760dbc..368ed669 100644 --- a/lib/config.cc +++ b/lib/config.cc @@ -22,6 +22,9 @@ #include "notmuch-private.h" #include "database-private.h" +#include +#include + static const std::string CONFIG_PREFIX = "C"; struct _notmuch_config_list { @@ -31,6 +34,20 @@ struct _notmuch_config_list { char *current_val; }; +struct _notmuch_config_values { + const char *iterator; + size_t tok_len; + const char *string; + void *children; /* talloc_context */ +}; + +struct _notmuch_config_pairs { + notmuch_string_map_iterator_t *iter; +}; + +static const char *_notmuch_config_key_to_string (notmuch_config_key_t key); +static char *_expand_path (void *ctx, const char *key, const char *val); + static int _notmuch_config_list_destroy (notmuch_config_list_t *list) { @@ -50,6 +67,11 @@ notmuch_database_set_config (notmuch_database_t *notmuch, if (status) return status; + if (! notmuch->config) { + if ((status = _notmuch_config_load_from_database (notmuch))) + return status; + } + try { notmuch->writable_xapian_db->set_metadata (CONFIG_PREFIX + key, value); } catch (const Xapian::Error &error) { @@ -58,7 +80,13 @@ notmuch_database_set_config (notmuch_database_t *notmuch, _notmuch_database_log (notmuch, "Error: A Xapian exception occurred setting metadata: %s\n", error.get_msg ().c_str ()); } - return status; + + if (status) + return status; + + _notmuch_string_map_set (notmuch->config, key, value); + + return NOTMUCH_STATUS_SUCCESS; } static notmuch_status_t @@ -84,17 +112,25 @@ notmuch_database_get_config (notmuch_database_t *notmuch, const char *key, char **value) { - std::string strval; + const char *stored_val; notmuch_status_t status; + if (! notmuch->config) { + if ((status = _notmuch_config_load_from_database (notmuch))) + return status; + } + if (! value) return NOTMUCH_STATUS_NULL_POINTER; - status = _metadata_value (notmuch, key, strval); - if (status) - return status; - - *value = strdup (strval.c_str ()); + stored_val = _notmuch_string_map_get (notmuch->config, key); + if (! stored_val) { + /* XXX in principle this API should be fixed so empty string + * is distinguished from not found */ + *value = strdup (""); + } else { + *value = strdup (stored_val); + } return NOTMUCH_STATUS_SUCCESS; } @@ -124,7 +160,8 @@ notmuch_database_get_config_list (notmuch_database_t *notmuch, talloc_set_destructor (list, _notmuch_config_list_destroy); } catch (const Xapian::Error &error) { - _notmuch_database_log (notmuch, "A Xapian exception occurred getting metadata iterator: %s.\n", + _notmuch_database_log (notmuch, + "A Xapian exception occurred getting metadata iterator: %s.\n", error.get_msg ().c_str ()); notmuch->exception_reported = true; status = NOTMUCH_STATUS_XAPIAN_EXCEPTION; @@ -139,7 +176,7 @@ notmuch_database_get_config_list (notmuch_database_t *notmuch, if (status != NOTMUCH_STATUS_XAPIAN_EXCEPTION) _notmuch_config_list_destroy (list); } - } else { + } else { talloc_set_destructor (list, _notmuch_config_list_destroy); } @@ -155,7 +192,9 @@ notmuch_config_list_valid (notmuch_config_list_t *metadata) return true; } -static inline char * _key_from_iterator (notmuch_config_list_t *list) { +static inline char * +_key_from_iterator (notmuch_config_list_t *list) +{ return talloc_strdup (list, (*list->iterator).c_str () + CONFIG_PREFIX.length ()); } @@ -201,3 +240,459 @@ notmuch_config_list_destroy (notmuch_config_list_t *list) { talloc_free (list); } + +notmuch_status_t +_notmuch_config_load_from_database (notmuch_database_t *notmuch) +{ + notmuch_status_t status = NOTMUCH_STATUS_SUCCESS; + notmuch_config_list_t *list; + + if (notmuch->config == NULL) + notmuch->config = _notmuch_string_map_create (notmuch); + + if (unlikely (notmuch->config == NULL)) + return NOTMUCH_STATUS_OUT_OF_MEMORY; + + status = notmuch_database_get_config_list (notmuch, "", &list); + if (status) + return status; + + for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) { + const char *key = notmuch_config_list_key (list); + char *normalized_val = _expand_path (list, key, notmuch_config_list_value (list)); + _notmuch_string_map_append (notmuch->config, key, normalized_val); + talloc_free (normalized_val); + } + + return status; +} + +notmuch_config_values_t * +notmuch_config_get_values (notmuch_database_t *notmuch, notmuch_config_key_t key) +{ + const char *key_str = _notmuch_config_key_to_string (key); + + if (! key_str) + return NULL; + + return notmuch_config_get_values_string (notmuch, key_str); +} + +notmuch_config_values_t * +notmuch_config_get_values_string (notmuch_database_t *notmuch, const char *key_str) +{ + notmuch_config_values_t *values = NULL; + bool ok = false; + + values = talloc (notmuch, notmuch_config_values_t); + if (unlikely (! values)) + goto DONE; + + values->children = talloc_new (values); + + values->string = _notmuch_string_map_get (notmuch->config, key_str); + if (! values->string) + goto DONE; + + values->iterator = strsplit_len (values->string, ';', &(values->tok_len)); + ok = true; + + DONE: + if (! ok) { + if (values) + talloc_free (values); + return NULL; + } + return values; +} + +notmuch_bool_t +notmuch_config_values_valid (notmuch_config_values_t *values) +{ + if (! values) + return false; + + return (values->iterator != NULL); +} + +const char * +notmuch_config_values_get (notmuch_config_values_t *values) +{ + return talloc_strndup (values->children, values->iterator, values->tok_len); +} + +void +notmuch_config_values_start (notmuch_config_values_t *values) +{ + if (values == NULL) + return; + if (values->children) { + talloc_free (values->children); + } + + values->children = talloc_new (values); + + values->iterator = strsplit_len (values->string, ';', &(values->tok_len)); +} + +void +notmuch_config_values_move_to_next (notmuch_config_values_t *values) +{ + values->iterator += values->tok_len; + values->iterator = strsplit_len (values->iterator, ';', &(values->tok_len)); +} + +void +notmuch_config_values_destroy (notmuch_config_values_t *values) +{ + talloc_free (values); +} + +notmuch_config_pairs_t * +notmuch_config_get_pairs (notmuch_database_t *notmuch, + const char *prefix) +{ + notmuch_config_pairs_t *pairs = talloc (notmuch, notmuch_config_pairs_t); + + pairs->iter = _notmuch_string_map_iterator_create (notmuch->config, prefix, false); + return pairs; +} + +notmuch_bool_t +notmuch_config_pairs_valid (notmuch_config_pairs_t *pairs) +{ + return _notmuch_string_map_iterator_valid (pairs->iter); +} + +void +notmuch_config_pairs_move_to_next (notmuch_config_pairs_t *pairs) +{ + _notmuch_string_map_iterator_move_to_next (pairs->iter); +} + +const char * +notmuch_config_pairs_key (notmuch_config_pairs_t *pairs) +{ + return _notmuch_string_map_iterator_key (pairs->iter); +} + +const char * +notmuch_config_pairs_value (notmuch_config_pairs_t *pairs) +{ + return _notmuch_string_map_iterator_value (pairs->iter); +} + +void +notmuch_config_pairs_destroy (notmuch_config_pairs_t *pairs) +{ + _notmuch_string_map_iterator_destroy (pairs->iter); + talloc_free (pairs); +} + +static char * +_expand_path (void *ctx, const char *key, const char *val) +{ + char *expanded_val; + + if ((strcmp (key, "database.path") == 0 || + strcmp (key, "database.mail_root") == 0 || + strcmp (key, "database.hook_dir") == 0 || + strcmp (key, "database.backup_path") == 0 ) && + val[0] != '/') + expanded_val = talloc_asprintf (ctx, "%s/%s", getenv ("HOME"), val); + else + expanded_val = talloc_strdup (ctx, val); + + return expanded_val; +} + +notmuch_status_t +_notmuch_config_load_from_file (notmuch_database_t *notmuch, + GKeyFile *file) +{ + notmuch_status_t status = NOTMUCH_STATUS_SUCCESS; + gchar **groups = NULL, **keys, *val; + + if (notmuch->config == NULL) + notmuch->config = _notmuch_string_map_create (notmuch); + + if (unlikely (notmuch->config == NULL)) { + status = NOTMUCH_STATUS_OUT_OF_MEMORY; + goto DONE; + } + + groups = g_key_file_get_groups (file, NULL); + for (gchar **grp = groups; *grp; grp++) { + keys = g_key_file_get_keys (file, *grp, NULL, NULL); + for (gchar **keys_p = keys; *keys_p; keys_p++) { + char *absolute_key = talloc_asprintf (notmuch, "%s.%s", *grp, *keys_p); + char *normalized_val; + val = g_key_file_get_value (file, *grp, *keys_p, NULL); + if (! val) { + status = NOTMUCH_STATUS_FILE_ERROR; + goto DONE; + } + normalized_val = _expand_path (notmuch, absolute_key, val); + _notmuch_string_map_set (notmuch->config, absolute_key, normalized_val); + g_free (val); + talloc_free (absolute_key); + talloc_free (normalized_val); + if (status) + goto DONE; + } + g_strfreev (keys); + } + + DONE: + if (groups) + g_strfreev (groups); + + return status; +} + +notmuch_status_t +notmuch_config_get_bool (notmuch_database_t *notmuch, notmuch_config_key_t key, notmuch_bool_t *val) +{ + const char *key_string, *val_string; + + key_string = _notmuch_config_key_to_string (key); + if (! key_string) { + return NOTMUCH_STATUS_ILLEGAL_ARGUMENT; + } + + val_string = _notmuch_string_map_get (notmuch->config, key_string); + if (! val_string) { + *val = FALSE; + return NOTMUCH_STATUS_SUCCESS; + } + + if (strcase_equal (val_string, "false") || strcase_equal (val_string, "no")) + *val = FALSE; + else if (strcase_equal (val_string, "true") || strcase_equal (val_string, "yes")) + *val = TRUE; + else + return NOTMUCH_STATUS_ILLEGAL_ARGUMENT; + + return NOTMUCH_STATUS_SUCCESS; +} + +static const char * +_get_name_from_passwd_file (void *ctx) +{ + long pw_buf_size; + char *pw_buf; + struct passwd passwd, *ignored; + const char *name; + int e; + + pw_buf_size = sysconf (_SC_GETPW_R_SIZE_MAX); + if (pw_buf_size == -1) pw_buf_size = 64; + pw_buf = (char *) talloc_size (ctx, pw_buf_size); + + while ((e = getpwuid_r (getuid (), &passwd, pw_buf, + pw_buf_size, &ignored)) == ERANGE) { + pw_buf_size = pw_buf_size * 2; + pw_buf = (char *) talloc_zero_size (ctx, pw_buf_size); + } + + if (e == 0) { + char *comma = strchr (passwd.pw_gecos, ','); + if (comma) + name = talloc_strndup (ctx, passwd.pw_gecos, + comma - passwd.pw_gecos); + else + name = talloc_strdup (ctx, passwd.pw_gecos); + } else { + name = talloc_strdup (ctx, ""); + } + + talloc_free (pw_buf); + + return name; +} + +static char * +_get_username_from_passwd_file (void *ctx) +{ + long pw_buf_size; + char *pw_buf; + struct passwd passwd, *ignored; + char *name; + int e; + + pw_buf_size = sysconf (_SC_GETPW_R_SIZE_MAX); + if (pw_buf_size == -1) pw_buf_size = 64; + pw_buf = (char *) talloc_zero_size (ctx, pw_buf_size); + + while ((e = getpwuid_r (getuid (), &passwd, pw_buf, + pw_buf_size, &ignored)) == ERANGE) { + pw_buf_size = pw_buf_size * 2; + pw_buf = (char *) talloc_zero_size (ctx, pw_buf_size); + } + + if (e == 0) + name = talloc_strdup (ctx, passwd.pw_name); + else + name = talloc_strdup (ctx, ""); + + talloc_free (pw_buf); + + return name; +} + +static const char * +_get_email_from_passwd_file (void *ctx) +{ + + char hostname[256]; + struct hostent *hostent; + const char *domainname; + char *email; + + char *username = _get_username_from_passwd_file (ctx); + + gethostname (hostname, 256); + hostname[255] = '\0'; + + hostent = gethostbyname (hostname); + if (hostent && (domainname = strchr (hostent->h_name, '.'))) + domainname += 1; + else + domainname = "(none)"; + + email = talloc_asprintf (ctx, "%s@%s.%s", + username, hostname, domainname); + + talloc_free (username); + return email; +} + +static const char * +_notmuch_config_key_to_string (notmuch_config_key_t key) +{ + switch (key) { + case NOTMUCH_CONFIG_DATABASE_PATH: + return "database.path"; + case NOTMUCH_CONFIG_MAIL_ROOT: + return "database.mail_root"; + case NOTMUCH_CONFIG_HOOK_DIR: + return "database.hook_dir"; + case NOTMUCH_CONFIG_BACKUP_DIR: + return "database.backup_dir"; + case NOTMUCH_CONFIG_EXCLUDE_TAGS: + return "search.exclude_tags"; + case NOTMUCH_CONFIG_NEW_TAGS: + return "new.tags"; + case NOTMUCH_CONFIG_NEW_IGNORE: + return "new.ignore"; + case NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS: + return "maildir.synchronize_flags"; + case NOTMUCH_CONFIG_PRIMARY_EMAIL: + return "user.primary_email"; + case NOTMUCH_CONFIG_OTHER_EMAIL: + return "user.other_email"; + case NOTMUCH_CONFIG_USER_NAME: + return "user.name"; + default: + return NULL; + } +} + +static const char * +_notmuch_config_default (notmuch_database_t *notmuch, notmuch_config_key_t key) +{ + char *path; + const char *name, *email; + + switch (key) { + case NOTMUCH_CONFIG_DATABASE_PATH: + path = getenv ("MAILDIR"); + if (path) + path = talloc_strdup (notmuch, path); + else + path = talloc_asprintf (notmuch, "%s/mail", + getenv ("HOME")); + return path; + case NOTMUCH_CONFIG_MAIL_ROOT: + /* by default, mail root is the same as database path */ + return notmuch_database_get_path (notmuch); + case NOTMUCH_CONFIG_EXCLUDE_TAGS: + return ""; + case NOTMUCH_CONFIG_NEW_TAGS: + return "unread;inbox"; + case NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS: + return "true"; + case NOTMUCH_CONFIG_USER_NAME: + name = getenv ("NAME"); + if (name) + name = talloc_strdup (notmuch, name); + else + name = _get_name_from_passwd_file (notmuch); + return name; + case NOTMUCH_CONFIG_PRIMARY_EMAIL: + email = getenv ("EMAIL"); + if (email) + email = talloc_strdup (notmuch, email); + else + email = _get_email_from_passwd_file (notmuch); + return email; + case NOTMUCH_CONFIG_NEW_IGNORE: + return ""; + case NOTMUCH_CONFIG_HOOK_DIR: + case NOTMUCH_CONFIG_BACKUP_DIR: + case NOTMUCH_CONFIG_OTHER_EMAIL: + return NULL; + default: + case NOTMUCH_CONFIG_LAST: + INTERNAL_ERROR ("illegal key enum %d", key); + } +} + +notmuch_status_t +_notmuch_config_load_defaults (notmuch_database_t *notmuch) +{ + notmuch_config_key_t key; + + for (key = NOTMUCH_CONFIG_FIRST; + key < NOTMUCH_CONFIG_LAST; + key = notmuch_config_key_t (key + 1)) { + const char *val = notmuch_config_get (notmuch, key); + const char *key_string = _notmuch_config_key_to_string (key); + + val = _notmuch_string_map_get (notmuch->config, key_string); + if (! val) { + _notmuch_string_map_set (notmuch->config, key_string, _notmuch_config_default (notmuch, + key)); + } + } + return NOTMUCH_STATUS_SUCCESS; +} + +const char * +notmuch_config_get (notmuch_database_t *notmuch, notmuch_config_key_t key) +{ + + return _notmuch_string_map_get (notmuch->config, _notmuch_config_key_to_string (key)); +} + +const char * +notmuch_config_path (notmuch_database_t *notmuch) +{ + return notmuch->config_path; +} + +notmuch_status_t +notmuch_config_set (notmuch_database_t *notmuch, notmuch_config_key_t key, const char *val) +{ + + return notmuch_database_set_config (notmuch, _notmuch_config_key_to_string (key), val); +} + +void +_notmuch_config_cache (notmuch_database_t *notmuch, notmuch_config_key_t key, const char *val) +{ + if (notmuch->config == NULL) + notmuch->config = _notmuch_string_map_create (notmuch); + + _notmuch_string_map_set (notmuch->config, _notmuch_config_key_to_string (key), val); +} diff --git a/lib/database-private.h b/lib/database-private.h index 041602cd..0d12ec1e 100644 --- a/lib/database-private.h +++ b/lib/database-private.h @@ -32,6 +32,8 @@ #include "notmuch-private.h" +#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0])) + #ifdef SILENCE_XAPIAN_DEPRECATION_WARNINGS #define XAPIAN_DEPRECATED(D) D #endif @@ -187,7 +189,11 @@ operator& (notmuch_field_flag_t a, notmuch_field_flag_t b) struct _notmuch_database { bool exception_reported; - char *path; + /* Path to actual database */ + const char *xapian_path; + + /* Path to config loaded, if any */ + const char *config_path; int atomic_nesting; /* true if changes have been made in this atomic section */ @@ -226,6 +232,9 @@ struct _notmuch_database { * here, but at least they are small */ notmuch_string_map_t *user_prefix; notmuch_string_map_t *user_header; + + /* Cached and possibly overridden configuration */ + notmuch_string_map_t *config; }; /* Prior to database version 3, features were implied by the database @@ -263,4 +272,23 @@ _notmuch_database_find_doc_ids (notmuch_database_t *notmuch, const char *value, Xapian::PostingIterator *begin, Xapian::PostingIterator *end); + +#define NOTMUCH_DATABASE_VERSION 3 + +/* features.cc */ + +_notmuch_features +_notmuch_database_parse_features (const void *ctx, const char *features, unsigned int version, + char mode, char **incompat_out); + +char * +_notmuch_database_print_features (const void *ctx, unsigned int features); + +/* prefix.cc */ +notmuch_status_t +_notmuch_database_setup_standard_query_fields (notmuch_database_t *notmuch); + +notmuch_status_t +_notmuch_database_setup_user_query_fields (notmuch_database_t *notmuch); + #endif diff --git a/lib/database.cc b/lib/database.cc index 75189685..96458f6f 100644 --- a/lib/database.cc +++ b/lib/database.cc @@ -19,10 +19,6 @@ */ #include "database-private.h" -#include "parse-time-vrp.h" -#include "query-fp.h" -#include "thread-fp.h" -#include "regexp-fields.h" #include "string-util.h" #include @@ -39,8 +35,6 @@ using namespace std; -#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0])) - typedef struct { const char *name; const char *prefix; @@ -52,16 +46,11 @@ typedef struct { #define STRINGIFY(s) _SUB_STRINGIFY (s) #define _SUB_STRINGIFY(s) #s -#if HAVE_XAPIAN_DB_RETRY_LOCK -#define DB_ACTION (Xapian::DB_CREATE_OR_OPEN | Xapian::DB_RETRY_LOCK) -#else -#define DB_ACTION Xapian::DB_CREATE_OR_OPEN -#endif - #define LOG_XAPIAN_EXCEPTION(message, error) _log_xapian_exception (__location__, message, error) static void -_log_xapian_exception (const char *where, notmuch_database_t *notmuch, const Xapian::Error error) { +_log_xapian_exception (const char *where, notmuch_database_t *notmuch, const Xapian::Error error) +{ _notmuch_database_log (notmuch, "A Xapian exception occurred at %s: %s\n", where, @@ -265,80 +254,6 @@ _notmuch_database_mode (notmuch_database_t *notmuch) * same thread. */ -/* With these prefix values we follow the conventions published here: - * - * https://xapian.org/docs/omega/termprefixes.html - * - * as much as makes sense. Note that I took some liberty in matching - * the reserved prefix values to notmuch concepts, (for example, 'G' - * is documented as "newsGroup (or similar entity - e.g. a web forum - * name)", for which I think the thread is the closest analogue in - * notmuch. This in spite of the fact that we will eventually be - * storing mailing-list messages where 'G' for "mailing list name" - * might be even a closer analogue. I'm treating the single-character - * prefixes preferentially for core notmuch concepts (which will be - * nearly universal to all mail messages). - */ - -static const -prefix_t prefix_table[] = { - /* name term prefix flags */ - { "type", "T", NOTMUCH_FIELD_NO_FLAGS }, - { "reference", "XREFERENCE", NOTMUCH_FIELD_NO_FLAGS }, - { "replyto", "XREPLYTO", NOTMUCH_FIELD_NO_FLAGS }, - { "directory", "XDIRECTORY", NOTMUCH_FIELD_NO_FLAGS }, - { "file-direntry", "XFDIRENTRY", NOTMUCH_FIELD_NO_FLAGS }, - { "directory-direntry", "XDDIRENTRY", NOTMUCH_FIELD_NO_FLAGS }, - { "body", "", NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROBABILISTIC }, - { "thread", "G", NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROCESSOR }, - { "tag", "K", NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROCESSOR }, - { "is", "K", NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROCESSOR }, - { "id", "Q", NOTMUCH_FIELD_EXTERNAL }, - { "mid", "Q", NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROCESSOR }, - { "path", "P", NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROCESSOR }, - { "property", "XPROPERTY", NOTMUCH_FIELD_EXTERNAL }, - /* - * Unconditionally add ':' to reduce potential ambiguity with - * overlapping prefixes and/or terms that start with capital - * letters. See Xapian document termprefixes.html for related - * discussion. - */ - { "folder", "XFOLDER:", NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROCESSOR }, - { "date", NULL, NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROCESSOR }, - { "query", NULL, NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROCESSOR }, - { "from", "XFROM", NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROBABILISTIC | - NOTMUCH_FIELD_PROCESSOR }, - { "to", "XTO", NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROBABILISTIC }, - { "attachment", "XATTACHMENT", NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROBABILISTIC }, - { "mimetype", "XMIMETYPE", NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROBABILISTIC }, - { "subject", "XSUBJECT", NOTMUCH_FIELD_EXTERNAL | - NOTMUCH_FIELD_PROBABILISTIC | - NOTMUCH_FIELD_PROCESSOR }, -}; - -static void -_setup_query_field_default (const prefix_t *prefix, notmuch_database_t *notmuch) -{ - if (prefix->prefix) - notmuch->query_parser->add_prefix ("", prefix->prefix); - if (prefix->flags & NOTMUCH_FIELD_PROBABILISTIC) - notmuch->query_parser->add_prefix (prefix->name, prefix->prefix); - else - notmuch->query_parser->add_boolean_prefix (prefix->name, prefix->prefix); -} notmuch_string_map_iterator_t * _notmuch_database_user_headers (notmuch_database_t *notmuch) @@ -346,153 +261,6 @@ _notmuch_database_user_headers (notmuch_database_t *notmuch) return _notmuch_string_map_iterator_create (notmuch->user_header, "", false); } -const char * -_user_prefix (void *ctx, const char *name) -{ - return talloc_asprintf (ctx, "XU%s:", name); -} - -static notmuch_status_t -_setup_user_query_fields (notmuch_database_t *notmuch) -{ - notmuch_config_list_t *list; - notmuch_status_t status; - - notmuch->user_prefix = _notmuch_string_map_create (notmuch); - if (notmuch->user_prefix == NULL) - return NOTMUCH_STATUS_OUT_OF_MEMORY; - - notmuch->user_header = _notmuch_string_map_create (notmuch); - if (notmuch->user_header == NULL) - return NOTMUCH_STATUS_OUT_OF_MEMORY; - - status = notmuch_database_get_config_list (notmuch, CONFIG_HEADER_PREFIX, &list); - if (status) - return status; - - for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) { - - prefix_t query_field; - - const char *key = notmuch_config_list_key (list) - + sizeof (CONFIG_HEADER_PREFIX) - 1; - - _notmuch_string_map_append (notmuch->user_prefix, - key, - _user_prefix (notmuch, key)); - - _notmuch_string_map_append (notmuch->user_header, - key, - notmuch_config_list_value (list)); - - query_field.name = talloc_strdup (notmuch, key); - query_field.prefix = _user_prefix (notmuch, key); - query_field.flags = NOTMUCH_FIELD_PROBABILISTIC - | NOTMUCH_FIELD_EXTERNAL; - - _setup_query_field_default (&query_field, notmuch); - } - - notmuch_config_list_destroy (list); - - return NOTMUCH_STATUS_SUCCESS; -} - -static void -_setup_query_field (const prefix_t *prefix, notmuch_database_t *notmuch) -{ - if (prefix->flags & NOTMUCH_FIELD_PROCESSOR) { - Xapian::FieldProcessor *fp; - - if (STRNCMP_LITERAL (prefix->name, "date") == 0) - fp = (new DateFieldProcessor(NOTMUCH_VALUE_TIMESTAMP))->release (); - else if (STRNCMP_LITERAL(prefix->name, "query") == 0) - fp = (new QueryFieldProcessor (*notmuch->query_parser, notmuch))->release (); - else if (STRNCMP_LITERAL (prefix->name, "thread") == 0) - fp = (new ThreadFieldProcessor (*notmuch->query_parser, notmuch))->release (); - else - fp = (new RegexpFieldProcessor (prefix->name, prefix->flags, - *notmuch->query_parser, notmuch))->release (); - - /* we treat all field-processor fields as boolean in order to get the raw input */ - if (prefix->prefix) - notmuch->query_parser->add_prefix ("", prefix->prefix); - notmuch->query_parser->add_boolean_prefix (prefix->name, fp); - } else { - _setup_query_field_default (prefix, notmuch); - } -} - -const char * -_find_prefix (const char *name) -{ - unsigned int i; - - for (i = 0; i < ARRAY_SIZE (prefix_table); i++) { - if (strcmp (name, prefix_table[i].name) == 0) - return prefix_table[i].prefix; - } - - INTERNAL_ERROR ("No prefix exists for '%s'\n", name); - - return ""; -} - -/* Like find prefix, but include the possibility of user defined - * prefixes specific to this database */ - -const char * -_notmuch_database_prefix (notmuch_database_t *notmuch, const char *name) -{ - unsigned int i; - - /*XXX TODO: reduce code duplication */ - for (i = 0; i < ARRAY_SIZE (prefix_table); i++) { - if (strcmp (name, prefix_table[i].name) == 0) - return prefix_table[i].prefix; - } - - if (notmuch->user_prefix) - return _notmuch_string_map_get (notmuch->user_prefix, name); - - return NULL; -} - -static const struct { - /* NOTMUCH_FEATURE_* value. */ - _notmuch_features value; - /* Feature name as it appears in the database. This name should - * be appropriate for displaying to the user if an older version - * of notmuch doesn't support this feature. */ - const char *name; - /* Compatibility flags when this feature is declared. */ - const char *flags; -} feature_names[] = { - { NOTMUCH_FEATURE_FILE_TERMS, - "multiple paths per message", "rw" }, - { NOTMUCH_FEATURE_DIRECTORY_DOCS, - "relative directory paths", "rw" }, - /* Header values are not required for reading a database because a - * reader can just refer to the message file. */ - { NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES, - "from/subject/message-ID in database", "w" }, - { NOTMUCH_FEATURE_BOOL_FOLDER, - "exact folder:/path: search", "rw" }, - { NOTMUCH_FEATURE_GHOSTS, - "mail documents for missing messages", "w" }, - /* Knowledge of the index mime-types are not required for reading - * a database because a reader will just be unable to query - * them. */ - { NOTMUCH_FEATURE_INDEXED_MIMETYPES, - "indexed MIME types", "w" }, - { NOTMUCH_FEATURE_LAST_MOD, - "modification tracking", "w" }, - /* Existing databases will work fine for all queries not involving - * 'body:' */ - { NOTMUCH_FEATURE_UNPREFIX_BODY_ONLY, - "index body and headers separately", "w" }, -}; - const char * notmuch_status_to_string (notmuch_status_t status) { @@ -525,12 +293,22 @@ notmuch_status_to_string (notmuch_status_t status) return "Operation requires a database upgrade"; case NOTMUCH_STATUS_PATH_ERROR: return "Path supplied is illegal for this function"; + case NOTMUCH_STATUS_IGNORED: + return "Argument was ignored"; + case NOTMUCH_STATUS_ILLEGAL_ARGUMENT: + return "Illegal argument for function"; case NOTMUCH_STATUS_MALFORMED_CRYPTO_PROTOCOL: return "Crypto protocol missing, malformed, or unintelligible"; case NOTMUCH_STATUS_FAILED_CRYPTO_CONTEXT_CREATION: return "Crypto engine initialization failure"; case NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL: return "Unknown crypto protocol"; + case NOTMUCH_STATUS_NO_CONFIG: + return "No configuration file found"; + case NOTMUCH_STATUS_NO_DATABASE: + return "No database found"; + case NOTMUCH_STATUS_DATABASE_EXISTS: + return "Database exists, not recreated"; default: case NOTMUCH_STATUS_LAST_STATUS: return "Unknown error status value"; @@ -686,109 +464,6 @@ notmuch_database_find_message (notmuch_database_t *notmuch, } } -notmuch_status_t -notmuch_database_create (const char *path, notmuch_database_t **database) -{ - char *status_string = NULL; - notmuch_status_t status; - - status = notmuch_database_create_verbose (path, database, - &status_string); - - if (status_string) { - fputs (status_string, stderr); - free (status_string); - } - - return status; -} - -notmuch_status_t -notmuch_database_create_verbose (const char *path, - notmuch_database_t **database, - char **status_string) -{ - notmuch_status_t status = NOTMUCH_STATUS_SUCCESS; - notmuch_database_t *notmuch = NULL; - char *notmuch_path = NULL; - char *message = NULL; - struct stat st; - int err; - - if (path == NULL) { - message = strdup ("Error: Cannot create a database for a NULL path.\n"); - status = NOTMUCH_STATUS_NULL_POINTER; - goto DONE; - } - - if (path[0] != '/') { - message = strdup ("Error: Database path must be absolute.\n"); - status = NOTMUCH_STATUS_PATH_ERROR; - goto DONE; - } - - err = stat (path, &st); - if (err) { - IGNORE_RESULT (asprintf (&message, "Error: Cannot create database at %s: %s.\n", - path, strerror (errno))); - status = NOTMUCH_STATUS_FILE_ERROR; - goto DONE; - } - - if (! S_ISDIR (st.st_mode)) { - IGNORE_RESULT (asprintf (&message, "Error: Cannot create database at %s: " - "Not a directory.\n", - path)); - status = NOTMUCH_STATUS_FILE_ERROR; - goto DONE; - } - - notmuch_path = talloc_asprintf (NULL, "%s/%s", path, ".notmuch"); - - err = mkdir (notmuch_path, 0755); - - if (err) { - IGNORE_RESULT (asprintf (&message, "Error: Cannot create directory %s: %s.\n", - notmuch_path, strerror (errno))); - status = NOTMUCH_STATUS_FILE_ERROR; - goto DONE; - } - - status = notmuch_database_open_verbose (path, - NOTMUCH_DATABASE_MODE_READ_WRITE, - ¬much, &message); - if (status) - goto DONE; - - /* Upgrade doesn't add these feature to existing databases, but - * new databases have them. */ - notmuch->features |= NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES; - notmuch->features |= NOTMUCH_FEATURE_INDEXED_MIMETYPES; - notmuch->features |= NOTMUCH_FEATURE_UNPREFIX_BODY_ONLY; - - status = notmuch_database_upgrade (notmuch, NULL, NULL); - if (status) { - notmuch_database_close (notmuch); - notmuch = NULL; - } - - DONE: - if (notmuch_path) - talloc_free (notmuch_path); - - if (message) { - if (status_string) - *status_string = message; - else - free (message); - } - if (database) - *database = notmuch; - else - talloc_free (notmuch); - return status; -} - notmuch_status_t _notmuch_database_ensure_writable (notmuch_database_t *notmuch) { @@ -817,291 +492,6 @@ _notmuch_database_new_revision (notmuch_database_t *notmuch) return new_revision; } -/* Parse a database features string from the given database version. - * Returns the feature bit set. - * - * For version < 3, this ignores the features string and returns a - * hard-coded set of features. - * - * If there are unrecognized features that are required to open the - * database in mode (which should be 'r' or 'w'), return a - * comma-separated list of unrecognized but required features in - * *incompat_out suitable for presenting to the user. *incompat_out - * will be allocated from ctx. - */ -static _notmuch_features -_parse_features (const void *ctx, const char *features, unsigned int version, - char mode, char **incompat_out) -{ - _notmuch_features res = static_cast<_notmuch_features>(0); - unsigned int namelen, i; - size_t llen = 0; - const char *flags; - - /* Prior to database version 3, features were implied by the - * version number. */ - if (version == 0) - return NOTMUCH_FEATURES_V0; - else if (version == 1) - return NOTMUCH_FEATURES_V1; - else if (version == 2) - return NOTMUCH_FEATURES_V2; - - /* Parse the features string */ - while ((features = strtok_len_c (features + llen, "\n", &llen)) != NULL) { - flags = strchr (features, '\t'); - if (! flags || flags > features + llen) - continue; - namelen = flags - features; - - for (i = 0; i < ARRAY_SIZE (feature_names); ++i) { - if (strlen (feature_names[i].name) == namelen && - strncmp (feature_names[i].name, features, namelen) == 0) { - res |= feature_names[i].value; - break; - } - } - - if (i == ARRAY_SIZE (feature_names) && incompat_out) { - /* Unrecognized feature */ - const char *have = strchr (flags, mode); - if (have && have < features + llen) { - /* This feature is required to access this database in - * 'mode', but we don't understand it. */ - if (! *incompat_out) - *incompat_out = talloc_strdup (ctx, ""); - *incompat_out = talloc_asprintf_append_buffer ( - *incompat_out, "%s%.*s", **incompat_out ? ", " : "", - namelen, features); - } - } - } - - return res; -} - -static char * -_print_features (const void *ctx, unsigned int features) -{ - unsigned int i; - char *res = talloc_strdup (ctx, ""); - - for (i = 0; i < ARRAY_SIZE (feature_names); ++i) - if (features & feature_names[i].value) - res = talloc_asprintf_append_buffer ( - res, "%s\t%s\n", feature_names[i].name, feature_names[i].flags); - - return res; -} - -notmuch_status_t -notmuch_database_open (const char *path, - notmuch_database_mode_t mode, - notmuch_database_t **database) -{ - char *status_string = NULL; - notmuch_status_t status; - - status = notmuch_database_open_verbose (path, mode, database, - &status_string); - - if (status_string) { - fputs (status_string, stderr); - free (status_string); - } - - return status; -} - -notmuch_status_t -notmuch_database_open_verbose (const char *path, - notmuch_database_mode_t mode, - notmuch_database_t **database, - char **status_string) -{ - notmuch_status_t status = NOTMUCH_STATUS_SUCCESS; - void *local = talloc_new (NULL); - notmuch_database_t *notmuch = NULL; - char *notmuch_path, *xapian_path, *incompat_features; - char *message = NULL; - struct stat st; - int err; - unsigned int i, version; - static int initialized = 0; - - if (path == NULL) { - message = strdup ("Error: Cannot open a database for a NULL path.\n"); - status = NOTMUCH_STATUS_NULL_POINTER; - goto DONE; - } - - if (path[0] != '/') { - message = strdup ("Error: Database path must be absolute.\n"); - status = NOTMUCH_STATUS_PATH_ERROR; - goto DONE; - } - - if (! (notmuch_path = talloc_asprintf (local, "%s/%s", path, ".notmuch"))) { - message = strdup ("Out of memory\n"); - status = NOTMUCH_STATUS_OUT_OF_MEMORY; - goto DONE; - } - - err = stat (notmuch_path, &st); - if (err) { - IGNORE_RESULT (asprintf (&message, "Error opening database at %s: %s\n", - notmuch_path, strerror (errno))); - status = NOTMUCH_STATUS_FILE_ERROR; - goto DONE; - } - - if (! (xapian_path = talloc_asprintf (local, "%s/%s", notmuch_path, "xapian"))) { - message = strdup ("Out of memory\n"); - status = NOTMUCH_STATUS_OUT_OF_MEMORY; - goto DONE; - } - - /* Initialize the GLib type system and threads */ -#if ! GLIB_CHECK_VERSION (2, 35, 1) - g_type_init (); -#endif - - /* Initialize gmime */ - if (! initialized) { - g_mime_init (); - initialized = 1; - } - - notmuch = talloc_zero (NULL, notmuch_database_t); - notmuch->exception_reported = false; - notmuch->status_string = NULL; - notmuch->path = talloc_strdup (notmuch, path); - - strip_trailing (notmuch->path, '/'); - - notmuch->writable_xapian_db = NULL; - notmuch->atomic_nesting = 0; - notmuch->view = 1; - try { - string last_thread_id; - string last_mod; - - if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) { - notmuch->writable_xapian_db = new Xapian::WritableDatabase (xapian_path, - DB_ACTION); - notmuch->xapian_db = notmuch->writable_xapian_db; - } else { - notmuch->xapian_db = new Xapian::Database (xapian_path); - } - - /* Check version. As of database version 3, we represent - * changes in terms of features, so assume a version bump - * means a dramatically incompatible change. */ - version = notmuch_database_get_version (notmuch); - if (version > NOTMUCH_DATABASE_VERSION) { - IGNORE_RESULT (asprintf (&message, - "Error: Notmuch database at %s\n" - " has a newer database format version (%u) than supported by this\n" - " version of notmuch (%u).\n", - notmuch_path, version, NOTMUCH_DATABASE_VERSION)); - notmuch_database_destroy (notmuch); - notmuch = NULL; - status = NOTMUCH_STATUS_FILE_ERROR; - goto DONE; - } - - /* Check features. */ - incompat_features = NULL; - notmuch->features = _parse_features ( - local, notmuch->xapian_db->get_metadata ("features").c_str (), - version, mode == NOTMUCH_DATABASE_MODE_READ_WRITE ? 'w' : 'r', - &incompat_features); - if (incompat_features) { - IGNORE_RESULT (asprintf (&message, - "Error: Notmuch database at %s\n" - " requires features (%s)\n" - " not supported by this version of notmuch.\n", - notmuch_path, incompat_features)); - notmuch_database_destroy (notmuch); - notmuch = NULL; - status = NOTMUCH_STATUS_FILE_ERROR; - goto DONE; - } - - notmuch->last_doc_id = notmuch->xapian_db->get_lastdocid (); - last_thread_id = notmuch->xapian_db->get_metadata ("last_thread_id"); - if (last_thread_id.empty ()) { - notmuch->last_thread_id = 0; - } else { - const char *str; - char *end; - - str = last_thread_id.c_str (); - notmuch->last_thread_id = strtoull (str, &end, 16); - if (*end != '\0') - INTERNAL_ERROR ("Malformed database last_thread_id: %s", str); - } - - /* Get current highest revision number. */ - last_mod = notmuch->xapian_db->get_value_upper_bound ( - NOTMUCH_VALUE_LAST_MOD); - if (last_mod.empty ()) - notmuch->revision = 0; - else - notmuch->revision = Xapian::sortable_unserialise (last_mod); - notmuch->uuid = talloc_strdup ( - notmuch, notmuch->xapian_db->get_uuid ().c_str ()); - - notmuch->query_parser = new Xapian::QueryParser; - notmuch->term_gen = new Xapian::TermGenerator; - notmuch->term_gen->set_stemmer (Xapian::Stem ("english")); - notmuch->value_range_processor = new Xapian::NumberRangeProcessor (NOTMUCH_VALUE_TIMESTAMP); - notmuch->date_range_processor = new ParseTimeRangeProcessor (NOTMUCH_VALUE_TIMESTAMP, "date:"); - notmuch->last_mod_range_processor = new Xapian::NumberRangeProcessor (NOTMUCH_VALUE_LAST_MOD, "lastmod:"); - notmuch->query_parser->set_default_op (Xapian::Query::OP_AND); - notmuch->query_parser->set_database (*notmuch->xapian_db); - notmuch->query_parser->set_stemmer (Xapian::Stem ("english")); - notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME); - notmuch->query_parser->add_rangeprocessor (notmuch->value_range_processor); - notmuch->query_parser->add_rangeprocessor (notmuch->date_range_processor); - notmuch->query_parser->add_rangeprocessor (notmuch->last_mod_range_processor); - - for (i = 0; i < ARRAY_SIZE (prefix_table); i++) { - const prefix_t *prefix = &prefix_table[i]; - if (prefix->flags & NOTMUCH_FIELD_EXTERNAL) { - _setup_query_field (prefix, notmuch); - } - } - status = _setup_user_query_fields (notmuch); - } catch (const Xapian::Error &error) { - IGNORE_RESULT (asprintf (&message, "A Xapian exception occurred opening database: %s\n", - error.get_msg ().c_str ())); - notmuch_database_destroy (notmuch); - notmuch = NULL; - status = NOTMUCH_STATUS_XAPIAN_EXCEPTION; - } - - DONE: - talloc_free (local); - - if (message) { - if (status_string) - *status_string = message; - else - free (message); - } - - if (database) - *database = notmuch; - else - talloc_free (notmuch); - - if (notmuch) - notmuch->open = true; - - return status; -} - notmuch_status_t notmuch_database_close (notmuch_database_t *notmuch) { @@ -1127,7 +517,8 @@ notmuch_database_close (notmuch_database_t *notmuch) } catch (const Xapian::Error &error) { status = NOTMUCH_STATUS_XAPIAN_EXCEPTION; if (! notmuch->exception_reported) { - _notmuch_database_log (notmuch, "Error: A Xapian exception occurred closing database: %s\n", + _notmuch_database_log (notmuch, + "Error: A Xapian exception occurred closing database: %s\n", error.get_msg ().c_str ()); } } @@ -1136,28 +527,6 @@ notmuch_database_close (notmuch_database_t *notmuch) return status; } -notmuch_status_t -_notmuch_database_reopen (notmuch_database_t *notmuch) -{ - if (_notmuch_database_mode (notmuch) != NOTMUCH_DATABASE_MODE_READ_ONLY) - return NOTMUCH_STATUS_UNSUPPORTED_OPERATION; - - try { - notmuch->xapian_db->reopen (); - } catch (const Xapian::Error &error) { - if (! notmuch->exception_reported) { - _notmuch_database_log (notmuch, "Error: A Xapian exception reopening database: %s\n", - error.get_msg ().c_str ()); - notmuch->exception_reported = true; - } - return NOTMUCH_STATUS_XAPIAN_EXCEPTION; - } - - notmuch->view++; - - return NOTMUCH_STATUS_SUCCESS; -} - static int unlink_cb (const char *path, unused (const struct stat *sb), @@ -1223,36 +592,56 @@ notmuch_database_compact (const char *path, notmuch_compact_status_cb_t status_cb, void *closure) { - void *local; - char *notmuch_path, *xapian_path, *compact_xapian_path; notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; notmuch_database_t *notmuch = NULL; - struct stat statbuf; - bool keep_backup; char *message = NULL; - local = talloc_new (NULL); - if (! local) - return NOTMUCH_STATUS_OUT_OF_MEMORY; - ret = notmuch_database_open_verbose (path, NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much, &message); if (ret) { if (status_cb) status_cb (message, closure); - goto DONE; + return ret; } - if (! (notmuch_path = talloc_asprintf (local, "%s/%s", path, ".notmuch"))) { - ret = NOTMUCH_STATUS_OUT_OF_MEMORY; - goto DONE; - } + _notmuch_config_cache (notmuch, NOTMUCH_CONFIG_DATABASE_PATH, path); - if (! (xapian_path = talloc_asprintf (local, "%s/%s", notmuch_path, "xapian"))) { - ret = NOTMUCH_STATUS_OUT_OF_MEMORY; + return notmuch_database_compact_db (notmuch, + backup_path, + status_cb, + closure); +} + +notmuch_status_t +notmuch_database_compact_db (notmuch_database_t *notmuch, + const char *backup_path, + notmuch_compact_status_cb_t status_cb, + void *closure) +{ + void *local; + const char *xapian_path, *compact_xapian_path; + const char *path; + notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; + struct stat statbuf; + bool keep_backup; + char *message; + + ret = _notmuch_database_ensure_writable (notmuch); + if (ret) + return ret; + + path = notmuch_config_get (notmuch, NOTMUCH_CONFIG_DATABASE_PATH); + if (! path) + return NOTMUCH_STATUS_PATH_ERROR; + + local = talloc_new (NULL); + if (! local) + return NOTMUCH_STATUS_OUT_OF_MEMORY; + + ret = _notmuch_choose_xapian_path (local, path, &xapian_path, &message); + if (ret) goto DONE; - } if (! (compact_xapian_path = talloc_asprintf (local, "%s.compact", xapian_path))) { ret = NOTMUCH_STATUS_OUT_OF_MEMORY; @@ -1289,7 +678,8 @@ notmuch_database_compact (const char *path, try { NotmuchCompactor compactor (status_cb, closure); - notmuch->xapian_db->compact (compact_xapian_path, Xapian::DBCOMPACT_NO_RENUMBER, 0, compactor); + notmuch->xapian_db->compact (compact_xapian_path, Xapian::DBCOMPACT_NO_RENUMBER, 0, + compactor); } catch (const Xapian::Error &error) { _notmuch_database_log (notmuch, "Error while compacting: %s\n", error.get_msg ().c_str ()); ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION; @@ -1367,7 +757,7 @@ notmuch_database_destroy (notmuch_database_t *notmuch) const char * notmuch_database_get_path (notmuch_database_t *notmuch) { - return notmuch->path; + return notmuch_config_get (notmuch, NOTMUCH_CONFIG_DATABASE_PATH); } unsigned int @@ -1437,8 +827,8 @@ handle_sigalrm (unused (int signal)) */ notmuch_status_t notmuch_database_upgrade (notmuch_database_t *notmuch, - void (*progress_notify) (void *closure, - double progress), + void (*progress_notify)(void *closure, + double progress), void *closure) { void *local = talloc_new (NULL); @@ -1662,7 +1052,8 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, if (private_status) { _notmuch_database_log (notmuch, "Upgrade failed while creating ghost messages.\n"); - status = COERCE_STATUS (private_status, "Unexpected status from _notmuch_message_initialize_ghost"); + status = COERCE_STATUS (private_status, + "Unexpected status from _notmuch_message_initialize_ghost"); goto DONE; } @@ -1674,7 +1065,7 @@ notmuch_database_upgrade (notmuch_database_t *notmuch, } status = NOTMUCH_STATUS_SUCCESS; - db->set_metadata ("features", _print_features (local, notmuch->features)); + db->set_metadata ("features", _notmuch_database_print_features (local, notmuch->features)); db->set_metadata ("version", STRINGIFY (NOTMUCH_DATABASE_VERSION)); DONE: @@ -1964,7 +1355,7 @@ _notmuch_database_relative_path (notmuch_database_t *notmuch, const char *db_path, *relative; unsigned int db_path_len; - db_path = notmuch_database_get_path (notmuch); + db_path = notmuch_config_get (notmuch, NOTMUCH_CONFIG_MAIL_ROOT); db_path_len = strlen (db_path); relative = path; @@ -2095,7 +1486,8 @@ notmuch_database_find_message_by_filename (notmuch_database_t *notmuch, status = NOTMUCH_STATUS_OUT_OF_MEMORY; } } catch (const Xapian::Error &error) { - _notmuch_database_log (notmuch, "Error: A Xapian exception occurred finding message by filename: %s\n", + _notmuch_database_log (notmuch, + "Error: A Xapian exception occurred finding message by filename: %s\n", error.get_msg ().c_str ()); notmuch->exception_reported = true; status = NOTMUCH_STATUS_XAPIAN_EXCEPTION; diff --git a/lib/directory.cc b/lib/directory.cc index eee8254e..5cf64d7f 100644 --- a/lib/directory.cc +++ b/lib/directory.cc @@ -52,8 +52,10 @@ struct _notmuch_directory { #define LOG_XAPIAN_EXCEPTION(directory, error) _log_xapian_exception (__location__, directory, error) static void -_log_xapian_exception (const char *where, notmuch_directory_t *dir, const Xapian::Error error) { +_log_xapian_exception (const char *where, notmuch_directory_t *dir, const Xapian::Error error) +{ notmuch_database_t *notmuch = dir->notmuch; + _notmuch_database_log (notmuch, "A Xapian exception occurred at %s: %s\n", where, diff --git a/lib/features.cc b/lib/features.cc new file mode 100644 index 00000000..cf0196c8 --- /dev/null +++ b/lib/features.cc @@ -0,0 +1,114 @@ +#include "database-private.h" + +static const struct { + /* NOTMUCH_FEATURE_* value. */ + _notmuch_features value; + /* Feature name as it appears in the database. This name should + * be appropriate for displaying to the user if an older version + * of notmuch doesn't support this feature. */ + const char *name; + /* Compatibility flags when this feature is declared. */ + const char *flags; +} feature_names[] = { + { NOTMUCH_FEATURE_FILE_TERMS, + "multiple paths per message", "rw" }, + { NOTMUCH_FEATURE_DIRECTORY_DOCS, + "relative directory paths", "rw" }, + /* Header values are not required for reading a database because a + * reader can just refer to the message file. */ + { NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES, + "from/subject/message-ID in database", "w" }, + { NOTMUCH_FEATURE_BOOL_FOLDER, + "exact folder:/path: search", "rw" }, + { NOTMUCH_FEATURE_GHOSTS, + "mail documents for missing messages", "w" }, + /* Knowledge of the index mime-types are not required for reading + * a database because a reader will just be unable to query + * them. */ + { NOTMUCH_FEATURE_INDEXED_MIMETYPES, + "indexed MIME types", "w" }, + { NOTMUCH_FEATURE_LAST_MOD, + "modification tracking", "w" }, + /* Existing databases will work fine for all queries not involving + * 'body:' */ + { NOTMUCH_FEATURE_UNPREFIX_BODY_ONLY, + "index body and headers separately", "w" }, +}; + +char * +_notmuch_database_print_features (const void *ctx, unsigned int features) +{ + unsigned int i; + char *res = talloc_strdup (ctx, ""); + + for (i = 0; i < ARRAY_SIZE (feature_names); ++i) + if (features & feature_names[i].value) + res = talloc_asprintf_append_buffer ( + res, "%s\t%s\n", feature_names[i].name, feature_names[i].flags); + + return res; +} + + +/* Parse a database features string from the given database version. + * Returns the feature bit set. + * + * For version < 3, this ignores the features string and returns a + * hard-coded set of features. + * + * If there are unrecognized features that are required to open the + * database in mode (which should be 'r' or 'w'), return a + * comma-separated list of unrecognized but required features in + * *incompat_out suitable for presenting to the user. *incompat_out + * will be allocated from ctx. + */ +_notmuch_features +_notmuch_database_parse_features (const void *ctx, const char *features, unsigned int version, + char mode, char **incompat_out) +{ + _notmuch_features res = static_cast<_notmuch_features>(0); + unsigned int namelen, i; + size_t llen = 0; + const char *flags; + + /* Prior to database version 3, features were implied by the + * version number. */ + if (version == 0) + return NOTMUCH_FEATURES_V0; + else if (version == 1) + return NOTMUCH_FEATURES_V1; + else if (version == 2) + return NOTMUCH_FEATURES_V2; + + /* Parse the features string */ + while ((features = strtok_len_c (features + llen, "\n", &llen)) != NULL) { + flags = strchr (features, '\t'); + if (! flags || flags > features + llen) + continue; + namelen = flags - features; + + for (i = 0; i < ARRAY_SIZE (feature_names); ++i) { + if (strlen (feature_names[i].name) == namelen && + strncmp (feature_names[i].name, features, namelen) == 0) { + res |= feature_names[i].value; + break; + } + } + + if (i == ARRAY_SIZE (feature_names) && incompat_out) { + /* Unrecognized feature */ + const char *have = strchr (flags, mode); + if (have && have < features + llen) { + /* This feature is required to access this database in + * 'mode', but we don't understand it. */ + if (! *incompat_out) + *incompat_out = talloc_strdup (ctx, ""); + *incompat_out = talloc_asprintf_append_buffer ( + *incompat_out, "%s%.*s", **incompat_out ? ", " : "", + namelen, features); + } + } + } + + return res; +} diff --git a/lib/index.cc b/lib/index.cc index 826aa341..55c8372e 100644 --- a/lib/index.cc +++ b/lib/index.cc @@ -260,7 +260,8 @@ notmuch_filter_discard_non_term_new (GMimeContentType *content_type) .value_table = NULL, }; - type = g_type_register_static (GMIME_TYPE_FILTER, "NotmuchFilterDiscardNonTerm", &info, (GTypeFlags) 0); + type = g_type_register_static (GMIME_TYPE_FILTER, "NotmuchFilterDiscardNonTerm", &info, + (GTypeFlags) 0); } filter = (NotmuchFilterDiscardNonTerm *) g_object_new (type, NULL); @@ -455,7 +456,8 @@ _index_mime_part (notmuch_message_t *message, msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL) { toindex = _notmuch_repair_crypto_payload_skip_legacy_display (child); if (toindex != child) - notmuch_message_add_property (message, "index.repaired", "skip-protected-headers-legacy-display"); + notmuch_message_add_property (message, "index.repaired", + "skip-protected-headers-legacy-display"); } _index_mime_part (message, indexopts, toindex, msg_crypto); } @@ -467,7 +469,8 @@ _index_mime_part (notmuch_message_t *message, mime_message = g_mime_message_part_get_message (GMIME_MESSAGE_PART (part)); - _index_mime_part (message, indexopts, g_mime_message_get_mime_part (mime_message), msg_crypto); + _index_mime_part (message, indexopts, g_mime_message_get_mime_part (mime_message), + msg_crypto); goto DONE; } @@ -567,6 +570,7 @@ _index_encrypted_mime_part (notmuch_message_t *message, bool attempted = false; GMimeDecryptResult *decrypt_result = NULL; bool get_sk = (notmuch_indexopts_get_decrypt_policy (indexopts) == NOTMUCH_DECRYPT_TRUE); + clear = _notmuch_crypto_decrypt (&attempted, notmuch_indexopts_get_decrypt_policy (indexopts), message, encrypted_data, get_sk ? &decrypt_result : NULL, &err); if (! attempted) @@ -595,7 +599,8 @@ _index_encrypted_mime_part (notmuch_message_t *message, notmuch_status_to_string (status)); if (get_sk) { status = notmuch_message_add_property (message, "session-key", - g_mime_decrypt_result_get_session_key (decrypt_result)); + g_mime_decrypt_result_get_session_key ( + decrypt_result)); if (status) _notmuch_database_log (notmuch, "failed to add session-key " "property (%d)\n", status); @@ -603,11 +608,14 @@ _index_encrypted_mime_part (notmuch_message_t *message, g_object_unref (decrypt_result); } GMimeObject *toindex = clear; - if (_notmuch_message_crypto_potential_payload (msg_crypto, clear, encrypted_data, GMIME_MULTIPART_ENCRYPTED_CONTENT) && + + if (_notmuch_message_crypto_potential_payload (msg_crypto, clear, encrypted_data, + GMIME_MULTIPART_ENCRYPTED_CONTENT) && msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL) { toindex = _notmuch_repair_crypto_payload_skip_legacy_display (clear); if (toindex != clear) - notmuch_message_add_property (message, "index.repaired", "skip-protected-headers-legacy-display"); + notmuch_message_add_property (message, "index.repaired", + "skip-protected-headers-legacy-display"); } _index_mime_part (message, indexopts, toindex, msg_crypto); g_object_unref (clear); @@ -640,7 +648,8 @@ _index_pkcs7_part (notmuch_message_t *message, if (p7type == GMIME_SECURE_MIME_TYPE_SIGNED_DATA) { sigs = g_mime_application_pkcs7_mime_verify (pkcs7, GMIME_VERIFY_NONE, &mimeobj, &err); if (sigs == NULL) { - _notmuch_database_log (notmuch, "Failed to verify PKCS#7 SignedData during indexing. (%d:%d) [%s]\n", + _notmuch_database_log (notmuch, + "Failed to verify PKCS#7 SignedData during indexing. (%d:%d) [%s]\n", err->domain, err->code, err->message); g_error_free (err); goto DONE; @@ -651,7 +660,8 @@ _index_pkcs7_part (notmuch_message_t *message, msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL) { toindex = _notmuch_repair_crypto_payload_skip_legacy_display (mimeobj); if (toindex != mimeobj) - notmuch_message_add_property (message, "index.repaired", "skip-protected-headers-legacy-display"); + notmuch_message_add_property (message, "index.repaired", + "skip-protected-headers-legacy-display"); } _index_mime_part (message, indexopts, toindex, msg_crypto); } else if (p7type == GMIME_SECURE_MIME_TYPE_ENVELOPED_DATA) { @@ -663,7 +673,7 @@ _index_pkcs7_part (notmuch_message_t *message, _notmuch_database_log (notmuch, "Cannot currently handle PKCS#7 smime-type '%s'\n", g_mime_object_get_content_type_parameter (part, "smime-type")); } - DONE: + DONE: if (mimeobj) g_object_unref (mimeobj); if (sigs) diff --git a/lib/indexopts.c b/lib/indexopts.c index 82a0026f..4a860858 100644 --- a/lib/indexopts.c +++ b/lib/indexopts.c @@ -31,6 +31,7 @@ notmuch_database_get_default_indexopts (notmuch_database_t *db) char *decrypt_policy; notmuch_status_t err = notmuch_database_get_config (db, "index.decrypt", &decrypt_policy); + if (err) return NULL; diff --git a/lib/message-file.c b/lib/message-file.c index 311bd478..9e9b387f 100644 --- a/lib/message-file.c +++ b/lib/message-file.c @@ -64,12 +64,13 @@ _notmuch_message_file_open_ctx (notmuch_database_t *notmuch, if (unlikely (message == NULL)) return NULL; - const char *prefix = notmuch_database_get_path (notmuch); + const char *prefix = notmuch_config_get (notmuch, NOTMUCH_CONFIG_MAIL_ROOT); + if (prefix == NULL) goto FAIL; if (*filename == '/') { - if (strncmp (filename, prefix, strlen(prefix)) != 0) { + if (strncmp (filename, prefix, strlen (prefix)) != 0) { _notmuch_database_log (notmuch, "Error opening %s: path outside mail root\n", filename); errno = 0; @@ -77,7 +78,7 @@ _notmuch_message_file_open_ctx (notmuch_database_t *notmuch, } message->filename = talloc_strdup (message, filename); } else { - message->filename = talloc_asprintf(message, "%s/%s", prefix, filename); + message->filename = talloc_asprintf (message, "%s/%s", prefix, filename); } if (message->filename == NULL) diff --git a/lib/message-property.cc b/lib/message-property.cc index ecf7e140..d5afa30c 100644 --- a/lib/message-property.cc +++ b/lib/message-property.cc @@ -43,11 +43,13 @@ notmuch_message_count_properties (notmuch_message_t *message, const char *key, u return NOTMUCH_STATUS_NULL_POINTER; notmuch_string_map_t *map; + map = _notmuch_message_property_map (message); if (! map) return NOTMUCH_STATUS_NULL_POINTER; notmuch_string_map_iterator_t *matcher = _notmuch_string_map_iterator_create (map, key, true); + if (! matcher) return NOTMUCH_STATUS_OUT_OF_MEMORY; diff --git a/lib/message.cc b/lib/message.cc index fca99082..42d56acb 100644 --- a/lib/message.cc +++ b/lib/message.cc @@ -93,8 +93,10 @@ _notmuch_message_destructor (notmuch_message_t *message) #define LOG_XAPIAN_EXCEPTION(message, error) _log_xapian_exception (__location__, message, error) static void -_log_xapian_exception (const char *where, notmuch_message_t *message, const Xapian::Error error) { +_log_xapian_exception (const char *where, notmuch_message_t *message, const Xapian::Error error) +{ notmuch_database_t *notmuch = notmuch_message_get_database (message); + _notmuch_database_log (notmuch, "A Xapian exception occurred at %s: %s\n", where, @@ -286,7 +288,8 @@ _notmuch_message_create_for_message_id (notmuch_database_t *notmuch, doc_id = _notmuch_database_generate_doc_id (notmuch); } catch (const Xapian::Error &error) { - _notmuch_database_log (notmuch_message_get_database (message), "A Xapian exception occurred creating message: %s\n", + _notmuch_database_log (notmuch_message_get_database (message), + "A Xapian exception occurred creating message: %s\n", error.get_msg ().c_str ()); notmuch->exception_reported = true; *status_ret = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION; @@ -318,6 +321,7 @@ _notmuch_message_get_term (notmuch_message_t *message, return NULL; const std::string &term = *i; + if (strncmp (term.c_str (), prefix, prefix_len)) return NULL; @@ -455,7 +459,8 @@ _notmuch_message_ensure_metadata (notmuch_message_t *message, void *field) /* all the way without an exception */ break; } catch (const Xapian::DatabaseModifiedError &error) { - notmuch_status_t status = _notmuch_database_reopen (message->notmuch); + notmuch_status_t status = notmuch_database_reopen (message->notmuch, + NOTMUCH_DATABASE_MODE_READ_ONLY); if (status != NOTMUCH_STATUS_SUCCESS) INTERNAL_ERROR ("unhandled error from notmuch_database_reopen: %s\n", notmuch_status_to_string (status)); @@ -1097,7 +1102,7 @@ _notmuch_message_ensure_filename_list (notmuch_message_t *message) *colon = '\0'; - db_path = notmuch_database_get_path (message->notmuch); + db_path = notmuch_config_get (message->notmuch, NOTMUCH_CONFIG_MAIL_ROOT); directory = _notmuch_database_get_directory_path (local, message->notmuch, @@ -1351,11 +1356,10 @@ notmuch_status_t _notmuch_message_delete (notmuch_message_t *message) { notmuch_status_t status; - const char *mid, *tid, *query_string; + const char *mid, *tid; notmuch_message_t *ghost; notmuch_private_status_t private_status; notmuch_database_t *notmuch; - notmuch_query_t *query; unsigned int count = 0; bool is_ghost; @@ -1377,16 +1381,33 @@ _notmuch_message_delete (notmuch_message_t *message) if (is_ghost) return NOTMUCH_STATUS_SUCCESS; - query_string = talloc_asprintf (message, "thread:%s", tid); - query = notmuch_query_create (notmuch, query_string); - if (query == NULL) - return NOTMUCH_STATUS_OUT_OF_MEMORY; - status = notmuch_query_count_messages (query, &count); - if (status) { - notmuch_query_destroy (query); - return status; + /* look for a non-ghost message in the same thread */ + try { + Xapian::PostingIterator thread_doc, thread_doc_end; + Xapian::PostingIterator mail_doc, mail_doc_end; + + _notmuch_database_find_doc_ids (message->notmuch, "thread", tid, &thread_doc, + &thread_doc_end); + _notmuch_database_find_doc_ids (message->notmuch, "type", "mail", &mail_doc, &mail_doc_end); + + while (count == 0 && + thread_doc != thread_doc_end && + mail_doc != mail_doc_end) { + thread_doc.skip_to (*mail_doc); + if (thread_doc != thread_doc_end) { + if (*thread_doc == *mail_doc) { + count++; + } else { + mail_doc.skip_to (*thread_doc); + if (mail_doc != mail_doc_end && *thread_doc == *mail_doc) + count++; + } + } + } + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; } - if (count > 0) { /* reintroduce a ghost in its place because there are still * other active messages in this thread: */ @@ -1405,27 +1426,21 @@ _notmuch_message_delete (notmuch_message_t *message) notmuch_message_destroy (ghost); status = COERCE_STATUS (private_status, "Error converting to ghost message"); } else { - /* the thread is empty; drop all ghost messages from it */ - notmuch_messages_t *messages; - status = _notmuch_query_search_documents (query, - "ghost", - &messages); - if (status == NOTMUCH_STATUS_SUCCESS) { - notmuch_status_t last_error = NOTMUCH_STATUS_SUCCESS; - while (notmuch_messages_valid (messages)) { - message = notmuch_messages_get (messages); - status = _notmuch_message_delete (message); - if (status) /* we'll report the last failure we see; - * if there is more than one failure, we - * forget about previous ones */ - last_error = status; - notmuch_message_destroy (message); - notmuch_messages_move_to_next (messages); + /* the thread now contains only ghosts: delete them */ + try { + Xapian::PostingIterator doc, doc_end; + + _notmuch_database_find_doc_ids (message->notmuch, "thread", tid, &doc, &doc_end); + + for (; doc != doc_end; doc++) { + message->notmuch->writable_xapian_db->delete_document (*doc); } - status = last_error; + } catch (Xapian::Error &error) { + LOG_XAPIAN_EXCEPTION (message, error); + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; } + } - notmuch_query_destroy (query); return status; } @@ -1765,6 +1780,7 @@ notmuch_message_has_maildir_flag (notmuch_message_t *message, char flag) { notmuch_status_t status; notmuch_bool_t ret; + status = notmuch_message_has_maildir_flag_st (message, flag, &ret); if (status) return FALSE; @@ -1778,14 +1794,14 @@ notmuch_message_has_maildir_flag_st (notmuch_message_t *message, notmuch_bool_t *is_set) { notmuch_status_t status; - + if (! is_set) return NOTMUCH_STATUS_NULL_POINTER; status = _ensure_maildir_flags (message, false); if (status) return status; - + *is_set = message->maildir_flags && (strchr (message->maildir_flags, flag) != NULL); return NOTMUCH_STATUS_SUCCESS; } @@ -2079,8 +2095,8 @@ notmuch_message_remove_all_tags (notmuch_message_t *message) private_status = _notmuch_message_remove_term (message, "tag", tag); if (private_status) { return COERCE_STATUS (private_status, - "_notmuch_message_remove_term return unexpected value: %d\n", - private_status); + "_notmuch_message_remove_term return unexpected value: %d\n", + private_status); } } @@ -2202,8 +2218,8 @@ notmuch_message_reindex (notmuch_message_t *message, orig_thread_id = notmuch_message_get_thread_id (message); if (! orig_thread_id) { /* the following is correct as long as there is only one reason - n_m_get_thread_id returns NULL - */ + * n_m_get_thread_id returns NULL + */ return NOTMUCH_STATUS_XAPIAN_EXCEPTION; } diff --git a/lib/notmuch-private.h b/lib/notmuch-private.h index 41aff342..10b1b024 100644 --- a/lib/notmuch-private.h +++ b/lib/notmuch-private.h @@ -74,7 +74,7 @@ NOTMUCH_BEGIN_DECLS #define NOTMUCH_CLEAR_BIT(valp, bit) \ (_NOTMUCH_VALID_BIT (bit) ? (*(valp) &= ~(1ull << (bit))) : *(valp)) -#define unused(x) x __attribute__ ((unused)) +#define unused(x) x ## _unused __attribute__ ((unused)) /* Thanks to Andrew Tridgell's (SAMBA's) talloc for this definition of * unlikely. The talloc source code comes to us via the GNU LGPL v. 3. @@ -127,10 +127,23 @@ typedef enum _notmuch_private_status { NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY = NOTMUCH_STATUS_OUT_OF_MEMORY, NOTMUCH_PRIVATE_STATUS_READ_ONLY_DATABASE = NOTMUCH_STATUS_READ_ONLY_DATABASE, NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION = NOTMUCH_STATUS_XAPIAN_EXCEPTION, + NOTMUCH_PRIVATE_STATUS_FILE_ERROR = NOTMUCH_STATUS_FILE_ERROR, NOTMUCH_PRIVATE_STATUS_FILE_NOT_EMAIL = NOTMUCH_STATUS_FILE_NOT_EMAIL, NOTMUCH_PRIVATE_STATUS_NULL_POINTER = NOTMUCH_STATUS_NULL_POINTER, NOTMUCH_PRIVATE_STATUS_TAG_TOO_LONG = NOTMUCH_STATUS_TAG_TOO_LONG, NOTMUCH_PRIVATE_STATUS_UNBALANCED_FREEZE_THAW = NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW, + NOTMUCH_PRIVATE_STATUS_UNBALANCED_ATOMIC = NOTMUCH_STATUS_UNBALANCED_ATOMIC, + NOTMUCH_PRIVATE_STATUS_UNSUPPORTED_OPERATION = NOTMUCH_STATUS_UNSUPPORTED_OPERATION, + NOTMUCH_PRIVATE_STATUS_UPGRADE_REQUIRED = NOTMUCH_STATUS_UPGRADE_REQUIRED, + NOTMUCH_PRIVATE_STATUS_PATH_ERROR = NOTMUCH_STATUS_PATH_ERROR, + NOTMUCH_PRIVATE_STATUS_IGNORED = NOTMUCH_STATUS_IGNORED, + NOTMUCH_PRIVATE_STATUS_ILLEGAL_ARGUMENT = NOTMUCH_STATUS_ILLEGAL_ARGUMENT, + NOTMUCH_PRIVATE_STATUS_MALFORMED_CRYPTO_PROTOCOL = NOTMUCH_STATUS_MALFORMED_CRYPTO_PROTOCOL, + NOTMUCH_PRIVATE_STATUS_FAILED_CRYPTO_CONTEXT_CREATION = NOTMUCH_STATUS_FAILED_CRYPTO_CONTEXT_CREATION, + NOTMUCH_PRIVATE_STATUS_UNKNOWN_CRYPTO_PROTOCOL = NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL, + NOTMUCH_PRIVATE_STATUS_NO_CONFIG = NOTMUCH_STATUS_NO_CONFIG, + NOTMUCH_PRIVATE_STATUS_NO_DATABASE = NOTMUCH_STATUS_NO_DATABASE, + NOTMUCH_PRIVATE_STATUS_DATABASE_EXISTS = NOTMUCH_STATUS_DATABASE_EXISTS, /* Then add our own private values. */ NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG = NOTMUCH_STATUS_LAST_STATUS, @@ -192,9 +205,6 @@ _notmuch_message_id_compressed (void *ctx, const char *message_id); notmuch_status_t _notmuch_database_ensure_writable (notmuch_database_t *notmuch); -notmuch_status_t -_notmuch_database_reopen (notmuch_database_t *notmuch); - void _notmuch_database_log (notmuch_database_t *notmuch, const char *format, ...); @@ -636,6 +646,11 @@ _notmuch_string_map_append (notmuch_string_map_t *map, const char *key, const char *value); +void +_notmuch_string_map_set (notmuch_string_map_t *map, + const char *key, + const char *value); + const char * _notmuch_string_map_get (notmuch_string_map_t *map, const char *key); @@ -697,6 +712,24 @@ struct _notmuch_indexopts { #define EMPTY_STRING(s) ((s)[0] == '\0') +/* config.cc */ +notmuch_status_t +_notmuch_config_load_from_database (notmuch_database_t *db); + +notmuch_status_t +_notmuch_config_load_from_file (notmuch_database_t *db, GKeyFile *file); + +notmuch_status_t +_notmuch_config_load_defaults (notmuch_database_t *db); + +void +_notmuch_config_cache (notmuch_database_t *db, notmuch_config_key_t key, const char *val); + +/* open.cc */ +notmuch_status_t +_notmuch_choose_xapian_path (void *ctx, const char *database_path, const char **xapian_path, + char **message); + NOTMUCH_END_DECLS #ifdef __cplusplus diff --git a/lib/notmuch.h b/lib/notmuch.h index c66e78b1..15390610 100644 --- a/lib/notmuch.h +++ b/lib/notmuch.h @@ -58,7 +58,7 @@ NOTMUCH_BEGIN_DECLS * version in Makefile.local. */ #define LIBNOTMUCH_MAJOR_VERSION 5 -#define LIBNOTMUCH_MINOR_VERSION 3 +#define LIBNOTMUCH_MINOR_VERSION 4 #define LIBNOTMUCH_MICRO_VERSION 0 @@ -208,6 +208,18 @@ typedef enum _notmuch_status { * something that notmuch doesn't know how to handle. */ NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL, + /** + * Unable to load a config file + */ + NOTMUCH_STATUS_NO_CONFIG, + /** + * Unable to load a database + */ + NOTMUCH_STATUS_NO_DATABASE, + /** + * Database exists, so not (re)-created + */ + NOTMUCH_STATUS_DATABASE_EXISTS, /** * Not an actual status value. Just a way to find out how many * valid status values there are. @@ -236,6 +248,8 @@ typedef struct _notmuch_tags notmuch_tags_t; typedef struct _notmuch_directory notmuch_directory_t; typedef struct _notmuch_filenames notmuch_filenames_t; typedef struct _notmuch_config_list notmuch_config_list_t; +typedef struct _notmuch_config_values notmuch_config_values_t; +typedef struct _notmuch_config_pairs notmuch_config_pairs_t; typedef struct _notmuch_indexopts notmuch_indexopts_t; #endif /* __DOXYGEN__ */ @@ -301,52 +315,197 @@ typedef enum { } notmuch_database_mode_t; /** - * Open an existing notmuch database located at 'path'. + * Deprecated alias for notmuch_database_open_with_config with + * config_path="" and error_message=NULL + * @deprecated Deprecated as of libnotmuch 5.4 (notmuch 0.32) + */ +/* NOTMUCH_DEPRECATED(5, 4) */ +notmuch_status_t +notmuch_database_open (const char *path, + notmuch_database_mode_t mode, + notmuch_database_t **database); +/** + * Deprecated alias for notmuch_database_open_with_config with + * config_path="" + * + * @deprecated Deprecated as of libnotmuch 5.4 (notmuch 0.32) + * + */ +/* NOTMUCH_DEPRECATED(5, 4) */ +notmuch_status_t +notmuch_database_open_verbose (const char *path, + notmuch_database_mode_t mode, + notmuch_database_t **database, + char **error_message); + +/** + * Open an existing notmuch database located at 'database_path', using + * configuration in 'config_path'. + * + * @param[in] database_path + * @parblock + * Path to existing database. + * + * A notmuch database is a Xapian database containing appropriate + * metadata. * * The database should have been created at some time in the past, * (not necessarily by this process), by calling - * notmuch_database_create with 'path'. By default the database should be - * opened for reading only. In order to write to the database you need to - * pass the NOTMUCH_DATABASE_MODE_READ_WRITE mode. + * notmuch_database_create. + * + * If 'database_path' is NULL, use the location specified + * + * - in the environment variable NOTMUCH_DATABASE, if non-empty + * + * - in a configuration file, located as described under 'config_path' + * + * - by $XDG_DATA_HOME/notmuch/$PROFILE where XDG_DATA_HOME defaults + * to "$HOME/.local/share" and PROFILE as as discussed in + * 'profile' * - * An existing notmuch database can be identified by the presence of a - * directory named ".notmuch" below 'path'. + * If 'database_path' is non-NULL, but does not appear to be a Xapian + * database, check for a directory '.notmuch/xapian' below + * 'database_path' (this is the behavior of + * notmuch_database_open_verbose pre-0.32). + * + * @endparblock + * @param[in] mode + * @parblock + * Mode to open database. Use one of #NOTMUCH_DATABASE_MODE_READ_WRITE + * or #NOTMUCH_DATABASE_MODE_READ_ONLY + * + * @endparblock + * @param[in] config_path + * @parblock + * Path to config file. + * + * Config file is key-value, with mandatory sections. See + * notmuch-config(5) for more information. The key-value pair + * overrides the corresponding configuration data stored in the + * database (see notmuch_database_get_config) + * + * If config_path is NULL use the path specified + * + * - in environment variable NOTMUCH_CONFIG, if non-empty + * + * - by XDG_CONFIG_HOME/notmuch/ where + * XDG_CONFIG_HOME defaults to "$HOME/.config". + * + * - by $HOME/.notmuch-config + * + * If config_path is "" (empty string) then do not + * open any configuration file. + * @endparblock + * @param[in] profile: + * @parblock + * Name of profile (configuration/database variant). + * + * If non-NULL, append to the directory / file path determined for + * config_path and database_path. + * + * If NULL then use + * - environment variable NOTMUCH_PROFILE if defined, + * - otherwise "default" for directories and "" (empty string) for paths. + * + * @endparblock + * @param[out] database + * @parblock + * Pointer to database object. May not be NULL. * * The caller should call notmuch_database_destroy when finished with * this database. * * In case of any failure, this function returns an error status and - * sets *database to NULL (after printing an error message on stderr). + * sets *database to NULL. * - * Return value: + * @endparblock + * @param[out] error_message + * If non-NULL, store error message from opening the database. + * Any such message is allocated by \a malloc(3) and should be freed + * by the caller. * - * NOTMUCH_STATUS_SUCCESS: Successfully opened the database. + * @retval NOTMUCH_STATUS_SUCCESS: Successfully opened the database. * - * NOTMUCH_STATUS_NULL_POINTER: The given 'path' argument is NULL. + * @retval NOTMUCH_STATUS_NULL_POINTER: The given \a database + * argument is NULL. * - * NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory. + * @retval NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory. * - * NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to open the - * database file (such as permission denied, or file not found, + * @retval NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to open the + * database or config file (such as permission denied, or file not found, * etc.), or the database version is unknown. * - * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred. + * @retval NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred. + * + * @since libnotmuch 5.4 (notmuch 0.32) */ + notmuch_status_t -notmuch_database_open (const char *path, - notmuch_database_mode_t mode, - notmuch_database_t **database); +notmuch_database_open_with_config (const char *database_path, + notmuch_database_mode_t mode, + const char *config_path, + const char *profile, + notmuch_database_t **database, + char **error_message); + + /** - * Like notmuch_database_open, except optionally return an error - * message. This message is allocated by malloc and should be freed by - * the caller. + * Loads configuration from config file, database, and/or defaults + * + * For description of arguments, @see notmuch_database_open_with_config + * + * @retval NOTMUCH_STATUS_SUCCESS: Successfully loaded configuration. + * + * @retval NOTMUCH_STATUS_NO_CONFIG: No config file was loaded. Not fatal. + * + * @retval NOTMUCH_STATUS_NO_DATABASE: No config information was + * loaded from a database. Not fatal. + * + * @retval NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory. + * + * @retval NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to open the + * database or config file (such as permission denied, or file not found, + * etc.) + * + * @retval NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred. + * + * @since libnotmuch 5.4 (notmuch 0.32) */ notmuch_status_t -notmuch_database_open_verbose (const char *path, - notmuch_database_mode_t mode, - notmuch_database_t **database, - char **error_message); +notmuch_database_load_config (const char *database_path, + const char *config_path, + const char *profile, + notmuch_database_t **database, + char **error_message); + +/** + * Create a new notmuch database located at 'database_path', using + * configuration in 'config_path'. + * + * For description of arguments, @see notmuch_database_open_with_config + * + * @retval NOTMUCH_STATUS_SUCCESS: Successfully created the database. + * + * @retval NOTMUCH_STATUS_DATABASE_EXISTS: Database already exists, not created + * + * @retval NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory. + * + * @retval NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to open the + * database or config file (such as permission denied, or file not found, + * etc.) + * + * @retval NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred. + * + * @since libnotmuch 5.4 (notmuch 0.32) + */ + +notmuch_status_t +notmuch_database_create_with_config (const char *database_path, + const char *config_path, + const char *profile, + notmuch_database_t **database, + char **error_message); /** * Retrieve last status string for given database. @@ -410,6 +569,18 @@ notmuch_database_compact (const char *path, notmuch_compact_status_cb_t status_cb, void *closure); +/** + * Like notmuch_database_compact, but take an open database as a + * parameter. + * + * @since libnnotmuch 5.4 (notmuch 0.32) + */ +notmuch_status_t +notmuch_database_compact_db (notmuch_database_t *database, + const char *backup_path, + notmuch_compact_status_cb_t status_cb, + void *closure); + /** * Destroy the notmuch database, closing it if necessary and freeing * all associated resources. @@ -745,6 +916,19 @@ notmuch_database_find_message_by_filename (notmuch_database_t *notmuch, notmuch_tags_t * notmuch_database_get_all_tags (notmuch_database_t *db); +/** + * Reopen an open notmuch database. + * + * @param [in] db open notmuch database + * @param [in] mode mode (read only or read-write) for reopened database. + * + * @retval #NOTMUCH_STATUS_SUCCESS + * @retval #NOTMUCH_STATUS_ILLEGAL_ARGUMENT The passed database was not open. + * @retval #NOTMUCH_STATUS_XAPIAN_EXCEPTION A Xapian exception occured + */ +notmuch_status_t +notmuch_database_reopen (notmuch_database_t *db, notmuch_database_mode_t mode); + /** * Create a new query for 'database'. * @@ -1497,7 +1681,7 @@ typedef enum _notmuch_message_flag { * @deprecated Deprecated as of libnotmuch 5.3 (notmuch 0.31). Please * use notmuch_message_get_flag_st instead. */ -NOTMUCH_DEPRECATED(5,3) +NOTMUCH_DEPRECATED (5, 3) notmuch_bool_t notmuch_message_get_flag (notmuch_message_t *message, notmuch_message_flag_t flag); @@ -1692,7 +1876,7 @@ notmuch_message_maildir_flags_to_tags (notmuch_message_t *message); * @returns FALSE in case of error * @deprecated libnotmuch 5.3 (notmuch 0.31) */ -NOTMUCH_DEPRECATED(5, 3) +NOTMUCH_DEPRECATED (5, 3) notmuch_bool_t notmuch_message_has_maildir_flag (notmuch_message_t *message, char flag); @@ -2239,6 +2423,11 @@ notmuch_filenames_destroy (notmuch_filenames_t *filenames); * set config 'key' to 'value' * * @since libnotmuch 4.4 (notmuch 0.23) + * @retval #NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in + * read-only mode so message cannot be modified. + * @retval #NOTMUCH_STATUS_XAPIAN_EXCEPTION: an exception was thrown + * accessing the database. + * @retval #NOTMUCH_STATUS_SUCCESS */ notmuch_status_t notmuch_database_set_config (notmuch_database_t *db, const char *key, const char *value); @@ -2253,6 +2442,7 @@ notmuch_database_set_config (notmuch_database_t *db, const char *key, const char * caller. * * @since libnotmuch 4.4 (notmuch 0.23) + * */ notmuch_status_t notmuch_database_get_config (notmuch_database_t *db, const char *key, char **value); @@ -2263,7 +2453,8 @@ notmuch_database_get_config (notmuch_database_t *db, const char *key, char **val * @since libnotmuch 4.4 (notmuch 0.23) */ notmuch_status_t -notmuch_database_get_config_list (notmuch_database_t *db, const char *prefix, notmuch_config_list_t **out); +notmuch_database_get_config_list (notmuch_database_t *db, const char *prefix, + notmuch_config_list_t **out); /** * Is 'config_list' iterator valid (i.e. _key, _value, _move_to_next can be called). @@ -2313,6 +2504,252 @@ notmuch_config_list_move_to_next (notmuch_config_list_t *config_list); void notmuch_config_list_destroy (notmuch_config_list_t *config_list); +/** + * Configuration keys known to libnotmuch + */ +typedef enum _notmuch_config_key { + NOTMUCH_CONFIG_FIRST, + NOTMUCH_CONFIG_DATABASE_PATH = NOTMUCH_CONFIG_FIRST, + NOTMUCH_CONFIG_MAIL_ROOT, + NOTMUCH_CONFIG_HOOK_DIR, + NOTMUCH_CONFIG_BACKUP_DIR, + NOTMUCH_CONFIG_EXCLUDE_TAGS, + NOTMUCH_CONFIG_NEW_TAGS, + NOTMUCH_CONFIG_NEW_IGNORE, + NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS, + NOTMUCH_CONFIG_PRIMARY_EMAIL, + NOTMUCH_CONFIG_OTHER_EMAIL, + NOTMUCH_CONFIG_USER_NAME, + NOTMUCH_CONFIG_LAST +} notmuch_config_key_t; + +/** + * get a configuration value from an open database. + * + * This value reflects all configuration information given at the time + * the database was opened. + * + * @param[in] notmuch database + * @param[in] key configuration key + * + * @since libnotmuch 5.4 (notmuch 0.32) + * + * @retval NULL if 'key' unknown or if no value is known for + * 'key'. Otherwise returns a string owned by notmuch which should + * not be modified nor freed by the caller. + */ +const char * +notmuch_config_get (notmuch_database_t *notmuch, notmuch_config_key_t key); + +/** + * set a configuration value from in an open database. + * + * This value reflects all configuration information given at the time + * the database was opened. + * + * @param[in,out] notmuch database open read/write + * @param[in] key configuration key + * @param[in] val configuration value + * + * @since libnotmuch 5.4 (notmuch 0.32) + * + * @retval returns any return value for notmuch_database_set_config. + */ +notmuch_status_t +notmuch_config_set (notmuch_database_t *notmuch, notmuch_config_key_t key, const char *val); + +/** + * Returns an iterator for a ';'-delimited list of configuration values + * + * These values reflect all configuration information given at the + * time the database was opened. + * + * @param[in] notmuch database + * @param[in] key configuration key + * + * @since libnotmuch 5.4 (notmuch 0.32) + * + * @retval NULL in case of error. + */ +notmuch_config_values_t * +notmuch_config_get_values (notmuch_database_t *notmuch, notmuch_config_key_t key); + +/** + * Returns an iterator for a ';'-delimited list of configuration values + * + * These values reflect all configuration information given at the + * time the database was opened. + * + * @param[in] notmuch database + * @param[in] key configuration key + * + * @since libnotmuch 5.4 (notmuch 0.32) + * + * @retval NULL in case of error. + */ +notmuch_config_values_t * +notmuch_config_get_values_string (notmuch_database_t *notmuch, const char *key); + +/** + * Is the given 'config_values' iterator pointing at a valid element. + * + * @param[in] values iterator + * + * @since libnotmuch 5.4 (notmuch 0.32) + * + * @retval FALSE if passed a NULL pointer, or the iterator is exhausted. + * + */ +notmuch_bool_t +notmuch_config_values_valid (notmuch_config_values_t *values); + +/** + * Get the current value from the 'values' iterator + * + * @param[in] values iterator + * + * @since libnotmuch 5.4 (notmuch 0.32) + * + * @retval a string with the same lifetime as the iterator + */ +const char * +notmuch_config_values_get (notmuch_config_values_t *values); + +/** + * Move the 'values' iterator to the next element + * + * @param[in,out] values iterator + * + * @since libnotmuch 5.4 (notmuch 0.32) + * + */ +void +notmuch_config_values_move_to_next (notmuch_config_values_t *values); + + +/** + * reset the 'values' iterator to the first element + * + * @param[in,out] values iterator. A NULL value is ignored. + * + * @since libnotmuch 5.4 (notmuch 0.32) + * + */ +void +notmuch_config_values_start (notmuch_config_values_t *values); + +/** + * Destroy a config values iterator, along with any associated + * resources. + * + * @param[in,out] values iterator + * + * @since libnotmuch 5.4 (notmuch 0.32) + */ +void +notmuch_config_values_destroy (notmuch_config_values_t *values); + + +/** + * Returns an iterator for a (key, value) configuration pairs + * + * @param[in] notmuch database + * @param[in] prefix prefix for keys. Pass "" for all keys. + * + * @since libnotmuch 5.4 (notmuch 0.32) + * + * @retval NULL in case of error. + */ +notmuch_config_pairs_t * +notmuch_config_get_pairs (notmuch_database_t *notmuch, + const char *prefix); + +/** + * Is the given 'config_pairs' iterator pointing at a valid element. + * + * @param[in] pairs iterator + * + * @since libnotmuch 5.4 (notmuch 0.32) + * + * @retval FALSE if passed a NULL pointer, or the iterator is exhausted. + * + */ +notmuch_bool_t +notmuch_config_pairs_valid (notmuch_config_pairs_t *pairs); + +/** + * Move the 'config_pairs' iterator to the next element + * + * @param[in,out] pairs iterator + * + * @since libnotmuch 5.4 (notmuch 0.32) + * + */ +void +notmuch_config_pairs_move_to_next (notmuch_config_pairs_t *pairs); + +/** + * Get the current key from the 'config_pairs' iterator + * + * @param[in] pairs iterator + * + * @since libnotmuch 5.4 (notmuch 0.32) + * + * @retval a string with the same lifetime as the iterator + */ +const char * +notmuch_config_pairs_key (notmuch_config_pairs_t *pairs); + +/** + * Get the current value from the 'config_pairs' iterator + * + * @param[in] pairs iterator + * + * @since libnotmuch 5.4 (notmuch 0.32) + * + * @retval a string with the same lifetime as the iterator + */ +const char * +notmuch_config_pairs_value (notmuch_config_pairs_t *pairs); + +/** + * Destroy a config_pairs iterator, along with any associated + * resources. + * + * @param[in,out] pairs iterator + * + * @since libnotmuch 5.4 (notmuch 0.32) + */ +void +notmuch_config_pairs_destroy (notmuch_config_pairs_t *pairs); + +/** + * get a configuration value from an open database as Boolean + * + * This value reflects all configuration information given at the time + * the database was opened. + * + * @param[in] notmuch database + * @param[in] key configuration key + * @param[out] val configuration value, converted to Boolean + * + * @since libnotmuch 5.4 (notmuch 0.32) + * + * @retval #NOTMUCH_STATUS_ILLEGAL_ARGUMENT if either key is unknown + * or the corresponding value does not convert to Boolean. + */ +notmuch_status_t +notmuch_config_get_bool (notmuch_database_t *notmuch, + notmuch_config_key_t key, + notmuch_bool_t *val); + +/** + * return the path of the config file loaded, if any + * + * @retval NULL if no config file was loaded + */ +const char * +notmuch_config_path (notmuch_database_t *notmuch); /** * get the current default indexing options for a given database. diff --git a/lib/open.cc b/lib/open.cc new file mode 100644 index 00000000..13743459 --- /dev/null +++ b/lib/open.cc @@ -0,0 +1,883 @@ +#include +#include + +#include "database-private.h" +#include "parse-time-vrp.h" +#include "path-util.h" + +#if HAVE_XAPIAN_DB_RETRY_LOCK +#define DB_ACTION (Xapian::DB_CREATE_OR_OPEN | Xapian::DB_RETRY_LOCK) +#else +#define DB_ACTION Xapian::DB_CREATE_OR_OPEN +#endif + +notmuch_status_t +notmuch_database_open (const char *path, + notmuch_database_mode_t mode, + notmuch_database_t **database) +{ + char *status_string = NULL; + notmuch_status_t status; + + status = notmuch_database_open_verbose (path, mode, database, + &status_string); + + if (status_string) { + fputs (status_string, stderr); + free (status_string); + } + + return status; +} + +notmuch_status_t +notmuch_database_open_verbose (const char *path, + notmuch_database_mode_t mode, + notmuch_database_t **database, + char **status_string) +{ + return notmuch_database_open_with_config (path, mode, "", NULL, + database, status_string); +} + +static const char * +_xdg_dir (void *ctx, + const char *xdg_root_variable, + const char *xdg_prefix, + const char *profile_name) +{ + const char *xdg_root = getenv (xdg_root_variable); + + if (! xdg_root) { + const char *home = getenv ("HOME"); + + if (! home) return NULL; + + xdg_root = talloc_asprintf (ctx, + "%s/%s", + home, + xdg_prefix); + } + + if (! profile_name) + profile_name = getenv ("NOTMUCH_PROFILE"); + + if (! profile_name) + profile_name = "default"; + + return talloc_asprintf (ctx, + "%s/notmuch/%s", + xdg_root, + profile_name); +} + +static notmuch_status_t +_choose_dir (notmuch_database_t *notmuch, + const char *profile, + notmuch_config_key_t key, + const char *xdg_var, + const char *xdg_subdir, + const char *subdir, + char **message = NULL) +{ + const char *parent; + const char *dir; + struct stat st; + int err; + + dir = notmuch_config_get (notmuch, key); + + if (dir) + return NOTMUCH_STATUS_SUCCESS; + + parent = _xdg_dir (notmuch, xdg_var, xdg_subdir, profile); + if (! parent) + return NOTMUCH_STATUS_PATH_ERROR; + + dir = talloc_asprintf (notmuch, "%s/%s", parent, subdir); + + err = stat (dir, &st); + if (err) { + if (errno == ENOENT) { + char *notmuch_path = dirname (talloc_strdup (notmuch, notmuch->xapian_path)); + dir = talloc_asprintf (notmuch, "%s/%s", notmuch_path, subdir); + } else { + IGNORE_RESULT (asprintf (message, "Error: Cannot stat %s: %s.\n", + dir, strerror (errno))); + return NOTMUCH_STATUS_FILE_ERROR; + } + } + + _notmuch_config_cache (notmuch, key, dir); + + return NOTMUCH_STATUS_SUCCESS; +} + +static notmuch_status_t +_load_key_file (notmuch_database_t *notmuch, + const char *path, + const char *profile, + GKeyFile **key_file) +{ + notmuch_status_t status = NOTMUCH_STATUS_SUCCESS; + + if (path && EMPTY_STRING (path)) + goto DONE; + + if (! path) + path = getenv ("NOTMUCH_CONFIG"); + + if (path) + path = talloc_strdup (notmuch, path); + else { + const char *dir = _xdg_dir (notmuch, "XDG_CONFIG_HOME", ".config", profile); + + if (dir) { + path = talloc_asprintf (notmuch, "%s/config", dir); + if (access (path, R_OK) != 0) + path = NULL; + } + } + + if (! path) { + const char *home = getenv ("HOME"); + + path = talloc_asprintf (notmuch, "%s/.notmuch-config", home); + + if (! profile) + profile = getenv ("NOTMUCH_PROFILE"); + + if (profile) + path = talloc_asprintf (notmuch, "%s.%s", path, profile); + } + + *key_file = g_key_file_new (); + if (! g_key_file_load_from_file (*key_file, path, G_KEY_FILE_NONE, NULL)) { + status = NOTMUCH_STATUS_NO_CONFIG; + } + + DONE: + if (path) + notmuch->config_path = path; + + return status; +} + +static notmuch_status_t +_db_dir_exists (const char *database_path, char **message) +{ + struct stat st; + int err; + + err = stat (database_path, &st); + if (err) { + IGNORE_RESULT (asprintf (message, "Error: Cannot open database at %s: %s.\n", + database_path, strerror (errno))); + return NOTMUCH_STATUS_FILE_ERROR; + } + + if (! S_ISDIR (st.st_mode)) { + IGNORE_RESULT (asprintf (message, "Error: Cannot open database at %s: " + "Not a directory.\n", + database_path)); + return NOTMUCH_STATUS_FILE_ERROR; + } + + return NOTMUCH_STATUS_SUCCESS; +} + +static notmuch_status_t +_choose_database_path (void *ctx, + const char *profile, + GKeyFile *key_file, + const char **database_path, + bool *split, + char **message) +{ + if (! *database_path) { + *database_path = getenv ("NOTMUCH_DATABASE"); + } + + if (! *database_path && key_file) { + char *path = g_key_file_get_value (key_file, "database", "path", NULL); + if (path) { + if (path[0] == '/') + *database_path = talloc_strdup (ctx, path); + else + *database_path = talloc_asprintf (ctx, "%s/%s", getenv ("HOME"), path); + g_free (path); + } + } + if (! *database_path) { + notmuch_status_t status; + + *database_path = _xdg_dir (ctx, "XDG_DATA_HOME", ".local/share", profile); + status = _db_dir_exists (*database_path, message); + if (status) { + *database_path = NULL; + } else { + *split = true; + } + } + + if (! *database_path) { + *database_path = getenv ("MAILDIR"); + } + + if (! *database_path) { + notmuch_status_t status; + + *database_path = talloc_asprintf (ctx, "%s/mail", getenv ("HOME")); + status = _db_dir_exists (*database_path, message); + if (status) { + *database_path = NULL; + } + } + + if (*database_path == NULL) { + *message = strdup ("Error: could not locate database.\n"); + return NOTMUCH_STATUS_NO_DATABASE; + } + + if (*database_path[0] != '/') { + *message = strdup ("Error: Database path must be absolute.\n"); + return NOTMUCH_STATUS_PATH_ERROR; + } + return NOTMUCH_STATUS_SUCCESS; +} + +notmuch_database_t * +_alloc_notmuch () +{ + notmuch_database_t *notmuch; + + notmuch = talloc_zero (NULL, notmuch_database_t); + if (! notmuch) + return NULL; + + notmuch->exception_reported = false; + notmuch->status_string = NULL; + notmuch->writable_xapian_db = NULL; + notmuch->config_path = NULL; + notmuch->atomic_nesting = 0; + notmuch->view = 1; + return notmuch; +} + +static notmuch_status_t +_trial_open (const char *xapian_path, char **message_ptr) +{ + try { + Xapian::Database db (xapian_path); + } catch (const Xapian::DatabaseOpeningError &error) { + IGNORE_RESULT (asprintf (message_ptr, + "Cannot open Xapian database at %s: %s\n", + xapian_path, + error.get_msg ().c_str ())); + return NOTMUCH_STATUS_PATH_ERROR; + } catch (const Xapian::Error &error) { + IGNORE_RESULT (asprintf (message_ptr, + "A Xapian exception occurred opening database: %s\n", + error.get_msg ().c_str ())); + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } + return NOTMUCH_STATUS_SUCCESS; +} + +notmuch_status_t +_notmuch_choose_xapian_path (void *ctx, const char *database_path, + const char **xapian_path, char **message_ptr) +{ + notmuch_status_t status; + const char *trial_path, *notmuch_path; + + status = _db_dir_exists (database_path, message_ptr); + if (status) + goto DONE; + + trial_path = talloc_asprintf (ctx, "%s/xapian", database_path); + status = _trial_open (trial_path, message_ptr); + if (status != NOTMUCH_STATUS_PATH_ERROR) + goto DONE; + + if (*message_ptr) + free (*message_ptr); + + notmuch_path = talloc_asprintf (ctx, "%s/.notmuch", database_path); + status = _db_dir_exists (notmuch_path, message_ptr); + if (status) + goto DONE; + + trial_path = talloc_asprintf (ctx, "%s/xapian", notmuch_path); + status = _trial_open (trial_path, message_ptr); + + DONE: + if (status == NOTMUCH_STATUS_SUCCESS) + *xapian_path = trial_path; + return status; +} + +static void +_set_database_path (notmuch_database_t *notmuch, + const char *database_path) +{ + char *path = talloc_strdup (notmuch, database_path); + + strip_trailing (path, '/'); + + _notmuch_config_cache (notmuch, NOTMUCH_CONFIG_DATABASE_PATH, path); +} + +static void +_init_libs () +{ + + static int initialized = 0; + + /* Initialize the GLib type system and threads */ +#if ! GLIB_CHECK_VERSION (2, 35, 1) + g_type_init (); +#endif + + /* Initialize gmime */ + if (! initialized) { + g_mime_init (); + initialized = 1; + } +} + +static void +_load_database_state (notmuch_database_t *notmuch) +{ + std::string last_thread_id; + std::string last_mod; + + notmuch->last_doc_id = notmuch->xapian_db->get_lastdocid (); + last_thread_id = notmuch->xapian_db->get_metadata ("last_thread_id"); + if (last_thread_id.empty ()) { + notmuch->last_thread_id = 0; + } else { + const char *str; + char *end; + + str = last_thread_id.c_str (); + notmuch->last_thread_id = strtoull (str, &end, 16); + if (*end != '\0') + INTERNAL_ERROR ("Malformed database last_thread_id: %s", str); + } + + /* Get current highest revision number. */ + last_mod = notmuch->xapian_db->get_value_upper_bound ( + NOTMUCH_VALUE_LAST_MOD); + if (last_mod.empty ()) + notmuch->revision = 0; + else + notmuch->revision = Xapian::sortable_unserialise (last_mod); + notmuch->uuid = talloc_strdup ( + notmuch, notmuch->xapian_db->get_uuid ().c_str ()); +} + +static notmuch_status_t +_finish_open (notmuch_database_t *notmuch, + const char *profile, + notmuch_database_mode_t mode, + GKeyFile *key_file, + char **message_ptr) +{ + notmuch_status_t status = NOTMUCH_STATUS_SUCCESS; + char *incompat_features; + char *message = NULL; + unsigned int version; + const char *database_path = notmuch_database_get_path (notmuch); + + try { + + if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) { + notmuch->writable_xapian_db = new Xapian::WritableDatabase (notmuch->xapian_path, + DB_ACTION); + notmuch->xapian_db = notmuch->writable_xapian_db; + } else { + notmuch->xapian_db = new Xapian::Database (notmuch->xapian_path); + } + + /* Check version. As of database version 3, we represent + * changes in terms of features, so assume a version bump + * means a dramatically incompatible change. */ + version = notmuch_database_get_version (notmuch); + if (version > NOTMUCH_DATABASE_VERSION) { + IGNORE_RESULT (asprintf (&message, + "Error: Notmuch database at %s\n" + " has a newer database format version (%u) than supported by this\n" + " version of notmuch (%u).\n", + database_path, version, NOTMUCH_DATABASE_VERSION)); + notmuch_database_destroy (notmuch); + notmuch = NULL; + status = NOTMUCH_STATUS_FILE_ERROR; + goto DONE; + } + + /* Check features. */ + incompat_features = NULL; + notmuch->features = _notmuch_database_parse_features ( + notmuch, notmuch->xapian_db->get_metadata ("features").c_str (), + version, mode == NOTMUCH_DATABASE_MODE_READ_WRITE ? 'w' : 'r', + &incompat_features); + if (incompat_features) { + IGNORE_RESULT (asprintf (&message, + "Error: Notmuch database at %s\n" + " requires features (%s)\n" + " not supported by this version of notmuch.\n", + database_path, incompat_features)); + notmuch_database_destroy (notmuch); + notmuch = NULL; + status = NOTMUCH_STATUS_FILE_ERROR; + goto DONE; + } + + _load_database_state (notmuch); + + notmuch->query_parser = new Xapian::QueryParser; + notmuch->term_gen = new Xapian::TermGenerator; + notmuch->term_gen->set_stemmer (Xapian::Stem ("english")); + notmuch->value_range_processor = new Xapian::NumberRangeProcessor (NOTMUCH_VALUE_TIMESTAMP); + notmuch->date_range_processor = new ParseTimeRangeProcessor (NOTMUCH_VALUE_TIMESTAMP, + "date:"); + notmuch->last_mod_range_processor = new Xapian::NumberRangeProcessor (NOTMUCH_VALUE_LAST_MOD, + "lastmod:"); + notmuch->query_parser->set_default_op (Xapian::Query::OP_AND); + notmuch->query_parser->set_database (*notmuch->xapian_db); + notmuch->query_parser->set_stemmer (Xapian::Stem ("english")); + notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME); + notmuch->query_parser->add_rangeprocessor (notmuch->value_range_processor); + notmuch->query_parser->add_rangeprocessor (notmuch->date_range_processor); + notmuch->query_parser->add_rangeprocessor (notmuch->last_mod_range_processor); + + /* Configuration information is needed to set up query parser */ + status = _notmuch_config_load_from_database (notmuch); + if (status) + goto DONE; + + if (key_file) + status = _notmuch_config_load_from_file (notmuch, key_file); + if (status) + goto DONE; + + status = _choose_dir (notmuch, profile, + NOTMUCH_CONFIG_HOOK_DIR, + "XDG_CONFIG_HOME", + ".config", + "hooks", + &message); + if (status) + goto DONE; + + status = _choose_dir (notmuch, profile, + NOTMUCH_CONFIG_BACKUP_DIR, + "XDG_DATA_HOME", + ".local/share", + "backups", + &message); + if (status) + goto DONE; + status = _notmuch_config_load_defaults (notmuch); + if (status) + goto DONE; + + status = _notmuch_database_setup_standard_query_fields (notmuch); + if (status) + goto DONE; + + status = _notmuch_database_setup_user_query_fields (notmuch); + if (status) + goto DONE; + + } catch (const Xapian::Error &error) { + IGNORE_RESULT (asprintf (&message, "A Xapian exception occurred opening database: %s\n", + error.get_msg ().c_str ())); + notmuch_database_destroy (notmuch); + notmuch = NULL; + status = NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } + DONE: + if (message_ptr) + *message_ptr = message; + return status; +} + +notmuch_status_t +notmuch_database_open_with_config (const char *database_path, + notmuch_database_mode_t mode, + const char *config_path, + const char *profile, + notmuch_database_t **database, + char **status_string) +{ + notmuch_status_t status = NOTMUCH_STATUS_SUCCESS; + void *local = talloc_new (NULL); + notmuch_database_t *notmuch = NULL; + char *message = NULL; + GKeyFile *key_file = NULL; + bool split = false; + + _init_libs (); + + notmuch = _alloc_notmuch (); + if (! notmuch) { + status = NOTMUCH_STATUS_OUT_OF_MEMORY; + goto DONE; + } + + status = _load_key_file (notmuch, config_path, profile, &key_file); + if (status) { + message = strdup ("Error: cannot load config file.\n"); + goto DONE; + } + + if ((status = _choose_database_path (local, profile, key_file, + &database_path, &split, + &message))) + goto DONE; + + status = _db_dir_exists (database_path, &message); + if (status) + goto DONE; + + _set_database_path (notmuch, database_path); + + status = _notmuch_choose_xapian_path (notmuch, database_path, + ¬much->xapian_path, &message); + if (status) + goto DONE; + + status = _finish_open (notmuch, profile, mode, key_file, &message); + + DONE: + talloc_free (local); + + if (key_file) + g_key_file_free (key_file); + + if (message) { + if (status_string) + *status_string = message; + else + free (message); + } + + if (database) + *database = notmuch; + else + talloc_free (notmuch); + + if (notmuch) + notmuch->open = true; + + return status; +} + +notmuch_status_t +notmuch_database_create (const char *path, notmuch_database_t **database) +{ + char *status_string = NULL; + notmuch_status_t status; + + status = notmuch_database_create_verbose (path, database, + &status_string); + + if (status_string) { + fputs (status_string, stderr); + free (status_string); + } + + return status; +} + +notmuch_status_t +notmuch_database_create_verbose (const char *path, + notmuch_database_t **database, + char **status_string) +{ + return notmuch_database_create_with_config (path, "", NULL, database, status_string); +} + +notmuch_status_t +notmuch_database_create_with_config (const char *database_path, + const char *config_path, + const char *profile, + notmuch_database_t **database, + char **status_string) +{ + notmuch_status_t status = NOTMUCH_STATUS_SUCCESS; + notmuch_database_t *notmuch = NULL; + const char *notmuch_path = NULL; + char *message = NULL; + GKeyFile *key_file = NULL; + void *local = talloc_new (NULL); + int err; + bool split = false; + + _init_libs (); + + notmuch = _alloc_notmuch (); + if (! notmuch) { + status = NOTMUCH_STATUS_OUT_OF_MEMORY; + goto DONE; + } + + status = _load_key_file (notmuch, config_path, profile, &key_file); + if (status) { + message = strdup ("Error: cannot load config file.\n"); + goto DONE; + } + + if ((status = _choose_database_path (local, profile, key_file, + &database_path, &split, &message))) + goto DONE; + + status = _db_dir_exists (database_path, &message); + if (status) + goto DONE; + + _set_database_path (notmuch, database_path); + + if (key_file && ! split) { + char *mail_root = notmuch_canonicalize_file_name ( + g_key_file_get_value (key_file, "database", "mail_root", NULL)); + char *db_path = notmuch_canonicalize_file_name (database_path); + + split = (mail_root && (0 != strcmp (mail_root, db_path))); + + free (mail_root); + free (db_path); + } + + if (split) { + notmuch_path = database_path; + } else { + if (! (notmuch_path = talloc_asprintf (local, "%s/%s", database_path, ".notmuch"))) { + status = NOTMUCH_STATUS_OUT_OF_MEMORY; + goto DONE; + } + + err = mkdir (notmuch_path, 0755); + if (err) { + if (errno == EEXIST) { + status = NOTMUCH_STATUS_DATABASE_EXISTS; + talloc_free (notmuch); + notmuch = NULL; + } else { + IGNORE_RESULT (asprintf (&message, "Error: Cannot create directory %s: %s.\n", + notmuch_path, strerror (errno))); + status = NOTMUCH_STATUS_FILE_ERROR; + } + goto DONE; + } + } + + if (! (notmuch->xapian_path = talloc_asprintf (notmuch, "%s/%s", notmuch_path, "xapian"))) { + status = NOTMUCH_STATUS_OUT_OF_MEMORY; + goto DONE; + } + + status = _trial_open (notmuch->xapian_path, &message); + if (status == NOTMUCH_STATUS_SUCCESS) { + notmuch_database_destroy (notmuch); + notmuch = NULL; + status = NOTMUCH_STATUS_DATABASE_EXISTS; + goto DONE; + } + + if (message) + free (message); + + status = _finish_open (notmuch, + profile, + NOTMUCH_DATABASE_MODE_READ_WRITE, + key_file, + &message); + if (status) + goto DONE; + + /* Upgrade doesn't add these feature to existing databases, but + * new databases have them. */ + notmuch->features |= NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES; + notmuch->features |= NOTMUCH_FEATURE_INDEXED_MIMETYPES; + notmuch->features |= NOTMUCH_FEATURE_UNPREFIX_BODY_ONLY; + + status = notmuch_database_upgrade (notmuch, NULL, NULL); + if (status) { + notmuch_database_close (notmuch); + notmuch = NULL; + } + + DONE: + talloc_free (local); + + if (key_file) + g_key_file_free (key_file); + + if (message) { + if (status_string) + *status_string = message; + else + free (message); + } + if (database) + *database = notmuch; + else + talloc_free (notmuch); + return status; +} + +notmuch_status_t +notmuch_database_reopen (notmuch_database_t *notmuch, + notmuch_database_mode_t new_mode) +{ + notmuch_database_mode_t cur_mode = _notmuch_database_mode (notmuch); + + if (notmuch->xapian_db == NULL) { + _notmuch_database_log (notmuch, "Cannot reopen closed or nonexistent database\n"); + return NOTMUCH_STATUS_ILLEGAL_ARGUMENT; + } + + try { + if (cur_mode == new_mode && + new_mode == NOTMUCH_DATABASE_MODE_READ_ONLY) { + notmuch->xapian_db->reopen (); + } else { + notmuch->xapian_db->close (); + + delete notmuch->xapian_db; + notmuch->xapian_db = NULL; + /* no need to free the same object twice */ + notmuch->writable_xapian_db = NULL; + + if (new_mode == NOTMUCH_DATABASE_MODE_READ_WRITE) { + notmuch->writable_xapian_db = new Xapian::WritableDatabase (notmuch->xapian_path, + DB_ACTION); + notmuch->xapian_db = notmuch->writable_xapian_db; + } else { + notmuch->xapian_db = new Xapian::Database (notmuch->xapian_path, + DB_ACTION); + } + } + + _load_database_state (notmuch); + } catch (const Xapian::Error &error) { + if (! notmuch->exception_reported) { + _notmuch_database_log (notmuch, "Error: A Xapian exception reopening database: %s\n", + error.get_msg ().c_str ()); + notmuch->exception_reported = true; + } + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } + + notmuch->view++; + notmuch->open = true; + return NOTMUCH_STATUS_SUCCESS; +} + +notmuch_status_t +_maybe_load_config_from_database (notmuch_database_t *notmuch, + GKeyFile *key_file, + const char *database_path, + const char *profile) +{ + char *message; /* ignored */ + + if (_db_dir_exists (database_path, &message)) + return NOTMUCH_STATUS_NO_DATABASE; + + _set_database_path (notmuch, database_path); + + if (_notmuch_choose_xapian_path (notmuch, database_path, ¬much->xapian_path, &message)) + return NOTMUCH_STATUS_NO_DATABASE; + + (void) _finish_open (notmuch, profile, NOTMUCH_DATABASE_MODE_READ_ONLY, key_file, &message); + + return NOTMUCH_STATUS_SUCCESS; +} + +notmuch_status_t +notmuch_database_load_config (const char *database_path, + const char *config_path, + const char *profile, + notmuch_database_t **database, + char **status_string) +{ + notmuch_status_t status = NOTMUCH_STATUS_SUCCESS, warning = NOTMUCH_STATUS_SUCCESS; + void *local = talloc_new (NULL); + notmuch_database_t *notmuch = NULL; + char *message = NULL; + GKeyFile *key_file = NULL; + bool split = false; + + _init_libs (); + + notmuch = _alloc_notmuch (); + if (! notmuch) { + status = NOTMUCH_STATUS_OUT_OF_MEMORY; + goto DONE; + } + + status = _load_key_file (notmuch, config_path, profile, &key_file); + switch (status) { + case NOTMUCH_STATUS_SUCCESS: + break; + case NOTMUCH_STATUS_NO_CONFIG: + warning = status; + break; + default: + message = strdup ("Error: cannot load config file.\n"); + goto DONE; + } + + status = _choose_database_path (local, profile, key_file, + &database_path, &split, &message); + switch (status) { + case NOTMUCH_STATUS_NO_DATABASE: + case NOTMUCH_STATUS_SUCCESS: + if (! warning) + warning = status; + break; + default: + goto DONE; + } + + + if (database_path) { + status = _maybe_load_config_from_database (notmuch, key_file, database_path, profile); + switch (status) { + case NOTMUCH_STATUS_NO_DATABASE: + case NOTMUCH_STATUS_SUCCESS: + if (! warning) + warning = status; + break; + default: + goto DONE; + } + } + + if (key_file) { + status = _notmuch_config_load_from_file (notmuch, key_file); + if (status) + goto DONE; + } + status = _notmuch_config_load_defaults (notmuch); + if (status) + goto DONE; + + DONE: + talloc_free (local); + + if (status_string) + *status_string = message; + + if (database) + *database = notmuch; + + if (status) + return status; + else + return warning; +} diff --git a/lib/parse-time-vrp.cc b/lib/parse-time-vrp.cc index 10809aa3..22bf2ab5 100644 --- a/lib/parse-time-vrp.cc +++ b/lib/parse-time-vrp.cc @@ -35,14 +35,14 @@ ParseTimeRangeProcessor::operator() (const std::string &begin, const std::string if (time (&now) == (time_t) -1) throw Xapian::QueryParserError ("unable to get current time"); - if (!begin.empty ()) { + if (! begin.empty ()) { if (parse_time_string (begin.c_str (), &parsed_time, &now, PARSE_TIME_ROUND_DOWN)) throw Xapian::QueryParserError ("Didn't understand date specification '" + begin + "'"); else from = (double) parsed_time; } - if (!end.empty ()) { + if (! end.empty ()) { if (end == "!" && ! begin.empty ()) str = begin; else diff --git a/lib/prefix.cc b/lib/prefix.cc new file mode 100644 index 00000000..0d92bdd7 --- /dev/null +++ b/lib/prefix.cc @@ -0,0 +1,210 @@ +#include "database-private.h" +#include "query-fp.h" +#include "thread-fp.h" +#include "regexp-fields.h" +#include "parse-time-vrp.h" + +typedef struct { + const char *name; + const char *prefix; + notmuch_field_flag_t flags; +} prefix_t; + +/* With these prefix values we follow the conventions published here: + * + * https://xapian.org/docs/omega/termprefixes.html + * + * as much as makes sense. Note that I took some liberty in matching + * the reserved prefix values to notmuch concepts, (for example, 'G' + * is documented as "newsGroup (or similar entity - e.g. a web forum + * name)", for which I think the thread is the closest analogue in + * notmuch. This in spite of the fact that we will eventually be + * storing mailing-list messages where 'G' for "mailing list name" + * might be even a closer analogue. I'm treating the single-character + * prefixes preferentially for core notmuch concepts (which will be + * nearly universal to all mail messages). + */ + +static const +prefix_t prefix_table[] = { + /* name term prefix flags */ + { "type", "T", NOTMUCH_FIELD_NO_FLAGS }, + { "reference", "XREFERENCE", NOTMUCH_FIELD_NO_FLAGS }, + { "replyto", "XREPLYTO", NOTMUCH_FIELD_NO_FLAGS }, + { "directory", "XDIRECTORY", NOTMUCH_FIELD_NO_FLAGS }, + { "file-direntry", "XFDIRENTRY", NOTMUCH_FIELD_NO_FLAGS }, + { "directory-direntry", "XDDIRENTRY", NOTMUCH_FIELD_NO_FLAGS }, + { "body", "", NOTMUCH_FIELD_EXTERNAL | + NOTMUCH_FIELD_PROBABILISTIC }, + { "thread", "G", NOTMUCH_FIELD_EXTERNAL | + NOTMUCH_FIELD_PROCESSOR }, + { "tag", "K", NOTMUCH_FIELD_EXTERNAL | + NOTMUCH_FIELD_PROCESSOR }, + { "is", "K", NOTMUCH_FIELD_EXTERNAL | + NOTMUCH_FIELD_PROCESSOR }, + { "id", "Q", NOTMUCH_FIELD_EXTERNAL }, + { "mid", "Q", NOTMUCH_FIELD_EXTERNAL | + NOTMUCH_FIELD_PROCESSOR }, + { "path", "P", NOTMUCH_FIELD_EXTERNAL | + NOTMUCH_FIELD_PROCESSOR }, + { "property", "XPROPERTY", NOTMUCH_FIELD_EXTERNAL }, + /* + * Unconditionally add ':' to reduce potential ambiguity with + * overlapping prefixes and/or terms that start with capital + * letters. See Xapian document termprefixes.html for related + * discussion. + */ + { "folder", "XFOLDER:", NOTMUCH_FIELD_EXTERNAL | + NOTMUCH_FIELD_PROCESSOR }, + { "date", NULL, NOTMUCH_FIELD_EXTERNAL | + NOTMUCH_FIELD_PROCESSOR }, + { "query", NULL, NOTMUCH_FIELD_EXTERNAL | + NOTMUCH_FIELD_PROCESSOR }, + { "from", "XFROM", NOTMUCH_FIELD_EXTERNAL | + NOTMUCH_FIELD_PROBABILISTIC | + NOTMUCH_FIELD_PROCESSOR }, + { "to", "XTO", NOTMUCH_FIELD_EXTERNAL | + NOTMUCH_FIELD_PROBABILISTIC }, + { "attachment", "XATTACHMENT", NOTMUCH_FIELD_EXTERNAL | + NOTMUCH_FIELD_PROBABILISTIC }, + { "mimetype", "XMIMETYPE", NOTMUCH_FIELD_EXTERNAL | + NOTMUCH_FIELD_PROBABILISTIC }, + { "subject", "XSUBJECT", NOTMUCH_FIELD_EXTERNAL | + NOTMUCH_FIELD_PROBABILISTIC | + NOTMUCH_FIELD_PROCESSOR }, +}; + +static const char * +_user_prefix (void *ctx, const char *name) +{ + return talloc_asprintf (ctx, "XU%s:", name); +} + +const char * +_find_prefix (const char *name) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE (prefix_table); i++) { + if (strcmp (name, prefix_table[i].name) == 0) + return prefix_table[i].prefix; + } + + INTERNAL_ERROR ("No prefix exists for '%s'\n", name); + + return ""; +} + +/* Like find prefix, but include the possibility of user defined + * prefixes specific to this database */ + +const char * +_notmuch_database_prefix (notmuch_database_t *notmuch, const char *name) +{ + unsigned int i; + + /*XXX TODO: reduce code duplication */ + for (i = 0; i < ARRAY_SIZE (prefix_table); i++) { + if (strcmp (name, prefix_table[i].name) == 0) + return prefix_table[i].prefix; + } + + if (notmuch->user_prefix) + return _notmuch_string_map_get (notmuch->user_prefix, name); + + return NULL; +} + +static void +_setup_query_field_default (const prefix_t *prefix, notmuch_database_t *notmuch) +{ + if (prefix->prefix) + notmuch->query_parser->add_prefix ("", prefix->prefix); + if (prefix->flags & NOTMUCH_FIELD_PROBABILISTIC) + notmuch->query_parser->add_prefix (prefix->name, prefix->prefix); + else + notmuch->query_parser->add_boolean_prefix (prefix->name, prefix->prefix); +} + +static void +_setup_query_field (const prefix_t *prefix, notmuch_database_t *notmuch) +{ + if (prefix->flags & NOTMUCH_FIELD_PROCESSOR) { + Xapian::FieldProcessor *fp; + + if (STRNCMP_LITERAL (prefix->name, "date") == 0) + fp = (new DateFieldProcessor (NOTMUCH_VALUE_TIMESTAMP))->release (); + else if (STRNCMP_LITERAL (prefix->name, "query") == 0) + fp = (new QueryFieldProcessor (*notmuch->query_parser, notmuch))->release (); + else if (STRNCMP_LITERAL (prefix->name, "thread") == 0) + fp = (new ThreadFieldProcessor (*notmuch->query_parser, notmuch))->release (); + else + fp = (new RegexpFieldProcessor (prefix->name, prefix->flags, + *notmuch->query_parser, notmuch))->release (); + + /* we treat all field-processor fields as boolean in order to get the raw input */ + if (prefix->prefix) + notmuch->query_parser->add_prefix ("", prefix->prefix); + notmuch->query_parser->add_boolean_prefix (prefix->name, fp); + } else { + _setup_query_field_default (prefix, notmuch); + } +} + +notmuch_status_t +_notmuch_database_setup_standard_query_fields (notmuch_database_t *notmuch) +{ + for (unsigned int i = 0; i < ARRAY_SIZE (prefix_table); i++) { + const prefix_t *prefix = &prefix_table[i]; + if (prefix->flags & NOTMUCH_FIELD_EXTERNAL) { + _setup_query_field (prefix, notmuch); + } + } + return NOTMUCH_STATUS_SUCCESS; +} + +notmuch_status_t +_notmuch_database_setup_user_query_fields (notmuch_database_t *notmuch) +{ + notmuch_string_map_iterator_t *list; + + notmuch->user_prefix = _notmuch_string_map_create (notmuch); + if (notmuch->user_prefix == NULL) + return NOTMUCH_STATUS_OUT_OF_MEMORY; + + notmuch->user_header = _notmuch_string_map_create (notmuch); + if (notmuch->user_header == NULL) + return NOTMUCH_STATUS_OUT_OF_MEMORY; + + list = _notmuch_string_map_iterator_create (notmuch->config, CONFIG_HEADER_PREFIX, FALSE); + if (! list) + INTERNAL_ERROR ("unable to read headers from configuration"); + + for (; _notmuch_string_map_iterator_valid (list); + _notmuch_string_map_iterator_move_to_next (list)) { + + prefix_t query_field; + + const char *key = _notmuch_string_map_iterator_key (list) + + sizeof (CONFIG_HEADER_PREFIX) - 1; + + _notmuch_string_map_append (notmuch->user_prefix, + key, + _user_prefix (notmuch, key)); + + _notmuch_string_map_append (notmuch->user_header, + key, + _notmuch_string_map_iterator_value (list)); + + query_field.name = talloc_strdup (notmuch, key); + query_field.prefix = _user_prefix (notmuch, key); + query_field.flags = NOTMUCH_FIELD_PROBABILISTIC + | NOTMUCH_FIELD_EXTERNAL; + + _setup_query_field_default (&query_field, notmuch); + } + + _notmuch_string_map_iterator_destroy (list); + + return NOTMUCH_STATUS_SUCCESS; +} diff --git a/lib/string-map.c b/lib/string-map.c index a88404c7..e3a81b4f 100644 --- a/lib/string-map.c +++ b/lib/string-map.c @@ -143,6 +143,24 @@ bsearch_first (notmuch_string_pair_t *array, size_t len, const char *key, bool e } +void +_notmuch_string_map_set (notmuch_string_map_t *map, + const char *key, + const char *val) +{ + notmuch_string_pair_t *pair; + + /* this means that calling string_map_set invalidates iterators */ + _notmuch_string_map_sort (map); + pair = bsearch_first (map->pairs, map->length, key, true); + if (! pair) + _notmuch_string_map_append (map, key, val); + else { + talloc_free (pair->value); + pair->value = talloc_strdup (map->pairs, val); + } +} + const char * _notmuch_string_map_get (notmuch_string_map_t *map, const char *key) { diff --git a/lib/thread-fp.cc b/lib/thread-fp.cc index 97a65211..06708ef2 100644 --- a/lib/thread-fp.cc +++ b/lib/thread-fp.cc @@ -40,11 +40,13 @@ ThreadFieldProcessor::operator() (const std::string & str) std::set terms; if (! subquery) - throw Xapian::QueryParserError ("failed to create subquery for '" + subquery_str + "'"); + throw Xapian::QueryParserError ("failed to create subquery for '" + subquery_str + + "'"); status = notmuch_query_search_messages (subquery, &messages); if (status) - throw Xapian::QueryParserError ("failed to search messages for '" + subquery_str + "'"); + throw Xapian::QueryParserError ("failed to search messages for '" + subquery_str + + "'"); for (; notmuch_messages_valid (messages); notmuch_messages_move_to_next (messages)) { std::string term = thread_prefix; diff --git a/lib/thread.cc b/lib/thread.cc index 17346008..46a50e80 100644 --- a/lib/thread.cc +++ b/lib/thread.cc @@ -505,7 +505,8 @@ _resolve_thread_relationships (notmuch_thread_t *thread) notmuch_messages_valid (roots); notmuch_messages_move_to_next (roots)) { notmuch_message_t *message = notmuch_messages_get (roots); - if (_notmuch_messages_has_next (roots) || ! _notmuch_message_list_empty (thread->toplevel_list)) + if (_notmuch_messages_has_next (roots) || ! _notmuch_message_list_empty ( + thread->toplevel_list)) _parent_or_toplevel (thread, message); else _notmuch_message_list_add_message (thread->toplevel_list, message); diff --git a/mime-node.c b/mime-node.c index f552e03a..d29c4e48 100644 --- a/mime-node.c +++ b/mime-node.c @@ -243,7 +243,8 @@ node_verify (mime_node_t *node, GMimeObject *part) status = _notmuch_message_crypto_potential_sig_list (node->ctx->msg_crypto, node->sig_list); if (status) /* this is a warning, not an error */ - fprintf (stderr, "Warning: failed to note signature status: %s.\n", notmuch_status_to_string (status)); + fprintf (stderr, "Warning: failed to note signature status: %s.\n", notmuch_status_to_string ( + status)); } /* Decrypt and optionally verify an encrypted mime node */ @@ -278,7 +279,8 @@ node_decrypt_and_verify (mime_node_t *node, GMimeObject *part) node->decrypt_success = true; status = _notmuch_message_crypto_successful_decryption (node->ctx->msg_crypto); if (status) /* this is a warning, not an error */ - fprintf (stderr, "Warning: failed to note decryption status: %s.\n", notmuch_status_to_string (status)); + fprintf (stderr, "Warning: failed to note decryption status: %s.\n", + notmuch_status_to_string (status)); if (decrypt_result) { /* This may be NULL if the part is not signed. */ @@ -287,9 +289,11 @@ node_decrypt_and_verify (mime_node_t *node, GMimeObject *part) node->verify_attempted = true; g_object_ref (node->sig_list); set_signature_list_destructor (node); - status = _notmuch_message_crypto_potential_sig_list (node->ctx->msg_crypto, node->sig_list); + status = _notmuch_message_crypto_potential_sig_list (node->ctx->msg_crypto, + node->sig_list); if (status) /* this is a warning, not an error */ - fprintf (stderr, "Warning: failed to note signature status: %s.\n", notmuch_status_to_string (status)); + fprintf (stderr, "Warning: failed to note signature status: %s.\n", + notmuch_status_to_string (status)); } if (node->ctx->crypto->decrypt == NOTMUCH_DECRYPT_TRUE && message) { @@ -366,7 +370,8 @@ _mime_node_set_up_part (mime_node_t *node, GMimeObject *part, int numchild) } /* Handle PGP/MIME parts (by definition not cryptographic payload parts) */ - if (GMIME_IS_MULTIPART_ENCRYPTED (part) && (node->ctx->crypto->decrypt != NOTMUCH_DECRYPT_FALSE)) { + if (GMIME_IS_MULTIPART_ENCRYPTED (part) && (node->ctx->crypto->decrypt != + NOTMUCH_DECRYPT_FALSE)) { if (node->nchildren != 2) { /* this violates RFC 3156 section 4, so we won't bother with it. */ fprintf (stderr, "Error: %d part(s) for a multipart/encrypted " @@ -385,19 +390,22 @@ _mime_node_set_up_part (mime_node_t *node, GMimeObject *part, int numchild) node_verify (node, part); } } else if (GMIME_IS_APPLICATION_PKCS7_MIME (part) && - GMIME_SECURE_MIME_TYPE_SIGNED_DATA == g_mime_application_pkcs7_mime_get_smime_type (GMIME_APPLICATION_PKCS7_MIME (part))) { + GMIME_SECURE_MIME_TYPE_SIGNED_DATA == g_mime_application_pkcs7_mime_get_smime_type ( + GMIME_APPLICATION_PKCS7_MIME (part))) { /* If node->ctx->crypto->verify is false, it would be better * to just unwrap (instead of verifying), but * https://github.com/jstedfast/gmime/issues/67 */ node_verify (node, part); } else if (GMIME_IS_APPLICATION_PKCS7_MIME (part) && - GMIME_SECURE_MIME_TYPE_ENVELOPED_DATA == g_mime_application_pkcs7_mime_get_smime_type (GMIME_APPLICATION_PKCS7_MIME (part)) && + GMIME_SECURE_MIME_TYPE_ENVELOPED_DATA == g_mime_application_pkcs7_mime_get_smime_type ( + GMIME_APPLICATION_PKCS7_MIME (part)) && (node->ctx->crypto->decrypt != NOTMUCH_DECRYPT_FALSE)) { node_decrypt_and_verify (node, part); if (node->unwrapped_child && node->nchildren == 0) node->nchildren = 1; } else { - if (_notmuch_message_crypto_potential_payload (node->ctx->msg_crypto, part, node->parent ? node->parent->part : NULL, numchild) && + if (_notmuch_message_crypto_potential_payload (node->ctx->msg_crypto, part, node->parent ? + node->parent->part : NULL, numchild) && node->ctx->msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL) { GMimeObject *clean_payload = _notmuch_repair_crypto_payload_skip_legacy_display (part); if (clean_payload != part) { diff --git a/notmuch-client.h b/notmuch-client.h index ebd43e8d..270553ad 100644 --- a/notmuch-client.h +++ b/notmuch-client.h @@ -156,7 +156,7 @@ chomp_newline (char *str) */ extern int notmuch_format_version; -typedef struct _notmuch_config notmuch_config_t; +typedef struct _notmuch_conffile notmuch_conffile_t; /* Commands that support structured output should support the * following argument @@ -171,46 +171,46 @@ void notmuch_exit_if_unsupported_format (void); int -notmuch_count_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_count_command (notmuch_database_t *notmuch, int argc, char *argv[]); int -notmuch_dump_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_dump_command (notmuch_database_t *notmuch, int argc, char *argv[]); int -notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_new_command (notmuch_database_t *notmuch, int argc, char *argv[]); int -notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_insert_command (notmuch_database_t *notmuch, int argc, char *argv[]); int -notmuch_reindex_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_reindex_command (notmuch_database_t *notmuch, int argc, char *argv[]); int -notmuch_reply_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_reply_command (notmuch_database_t *notmuch, int argc, char *argv[]); int -notmuch_restore_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_restore_command (notmuch_database_t *notmuch, int argc, char *argv[]); int -notmuch_search_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_search_command (notmuch_database_t *notmuch, int argc, char *argv[]); int -notmuch_address_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_address_command (notmuch_database_t *notmuch, int argc, char *argv[]); int -notmuch_setup_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_setup_command (notmuch_database_t *notmuch, int argc, char *argv[]); int -notmuch_show_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[]); int -notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_tag_command (notmuch_database_t *notmuch, int argc, char *argv[]); int -notmuch_config_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_config_command (notmuch_database_t *notmuch, int argc, char *argv[]); int -notmuch_compact_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_compact_command (notmuch_database_t *notmuch, int argc, char *argv[]); const char * notmuch_time_relative_date (const void *ctx, time_t then); @@ -253,88 +253,64 @@ json_quote_str (const void *ctx, const char *str); /* notmuch-config.c */ typedef enum { - NOTMUCH_CONFIG_OPEN = 1 << 0, - NOTMUCH_CONFIG_CREATE = 1 << 1, -} notmuch_config_mode_t; - -notmuch_config_t * -notmuch_config_open (void *ctx, - const char *filename, - notmuch_config_mode_t config_mode); + NOTMUCH_COMMAND_CONFIG_CREATE = 1 << 1, + NOTMUCH_COMMAND_DATABASE_EARLY = 1 << 2, + NOTMUCH_COMMAND_DATABASE_WRITE = 1 << 3, + NOTMUCH_COMMAND_DATABASE_CREATE = 1 << 4, + NOTMUCH_COMMAND_CONFIG_LOAD = 1 << 5, +} notmuch_command_mode_t; + +notmuch_conffile_t * +notmuch_conffile_open (notmuch_database_t *notmuch, + const char *filename, + bool create); void -notmuch_config_close (notmuch_config_t *config); +notmuch_conffile_close (notmuch_conffile_t *config); int -notmuch_config_save (notmuch_config_t *config); +notmuch_conffile_save (notmuch_conffile_t *config); bool -notmuch_config_is_new (notmuch_config_t *config); - -const char * -notmuch_config_get_database_path (notmuch_config_t *config); +notmuch_conffile_is_new (notmuch_conffile_t *config); void -notmuch_config_set_database_path (notmuch_config_t *config, - const char *database_path); - -const char * -notmuch_config_get_user_name (notmuch_config_t *config); +notmuch_conffile_set_database_path (notmuch_conffile_t *config, + const char *database_path); void -notmuch_config_set_user_name (notmuch_config_t *config, - const char *user_name); - -const char * -notmuch_config_get_user_primary_email (notmuch_config_t *config); +notmuch_conffile_set_user_name (notmuch_conffile_t *config, + const char *user_name); void -notmuch_config_set_user_primary_email (notmuch_config_t *config, - const char *primary_email); - -const char ** -notmuch_config_get_user_other_email (notmuch_config_t *config, - size_t *length); +notmuch_conffile_set_user_primary_email (notmuch_conffile_t *config, + const char *primary_email); void -notmuch_config_set_user_other_email (notmuch_config_t *config, - const char *other_email[], - size_t length); +notmuch_conffile_set_user_other_email (notmuch_conffile_t *config, + const char *other_email[], + size_t length); -const char ** -notmuch_config_get_new_tags (notmuch_config_t *config, - size_t *length); void -notmuch_config_set_new_tags (notmuch_config_t *config, - const char *new_tags[], - size_t length); - -const char ** -notmuch_config_get_new_ignore (notmuch_config_t *config, - size_t *length); - -void -notmuch_config_set_new_ignore (notmuch_config_t *config, - const char *new_ignore[], +notmuch_conffile_set_new_tags (notmuch_conffile_t *config, + const char *new_tags[], size_t length); -bool -notmuch_config_get_maildir_synchronize_flags (notmuch_config_t *config); - void -notmuch_config_set_maildir_synchronize_flags (notmuch_config_t *config, - bool synchronize_flags); - -const char ** -notmuch_config_get_search_exclude_tags (notmuch_config_t *config, size_t *length); +notmuch_conffile_set_new_ignore (notmuch_conffile_t *config, + const char *new_ignore[], + size_t length); void -notmuch_config_set_search_exclude_tags (notmuch_config_t *config, - const char *list[], - size_t length); +notmuch_conffile_set_maildir_synchronize_flags (notmuch_conffile_t *config, + bool synchronize_flags); +void +notmuch_conffile_set_search_exclude_tags (notmuch_conffile_t *config, + const char *list[], + size_t length); int -notmuch_run_hook (const char *db_path, const char *hook); +notmuch_run_hook (notmuch_database_t *notmuch, const char *hook); bool debugger_is_active (void); @@ -498,9 +474,10 @@ print_status_gzbytes (const char *loc, int bytes); /* the __location__ macro is defined in talloc.h */ -#define ASSERT_GZBYTES(file, bytes) ((print_status_gzbytes (__location__, file, bytes)) ? exit (1) : 0) +#define ASSERT_GZBYTES(file, bytes) ((print_status_gzbytes (__location__, file, bytes)) ? exit (1) : \ + 0) #define GZPRINTF(file, fmt, ...) ASSERT_GZBYTES (file, gzprintf (file, fmt, ##__VA_ARGS__)); -#define GZPUTS(file, str) ASSERT_GZBYTES(file, gzputs (file, str)); +#define GZPUTS(file, str) ASSERT_GZBYTES (file, gzputs (file, str)); #include "command-line-arguments.h" diff --git a/notmuch-compact.c b/notmuch-compact.c index f8996cf4..2648434e 100644 --- a/notmuch-compact.c +++ b/notmuch-compact.c @@ -27,9 +27,8 @@ status_update_cb (const char *msg, unused (void *closure)) } int -notmuch_compact_command (notmuch_config_t *config, int argc, char *argv[]) +notmuch_compact_command (notmuch_database_t *notmuch, int argc, char *argv[]) { - const char *path = notmuch_config_get_database_path (config); const char *backup_path = NULL; notmuch_status_t ret; bool quiet = false; @@ -55,8 +54,8 @@ notmuch_compact_command (notmuch_config_t *config, int argc, char *argv[]) if (! quiet) printf ("Compacting database...\n"); - ret = notmuch_database_compact (path, backup_path, - quiet ? NULL : status_update_cb, NULL); + ret = notmuch_database_compact_db (notmuch, backup_path, + quiet ? NULL : status_update_cb, NULL); if (ret) { fprintf (stderr, "Compaction failed: %s\n", notmuch_status_to_string (ret)); return EXIT_FAILURE; diff --git a/notmuch-config.c b/notmuch-config.c index 19c2ddb3..e5b4db45 100644 --- a/notmuch-config.c +++ b/notmuch-config.c @@ -24,6 +24,7 @@ #include #include +#include "path-util.h" #include "unicode-util.h" static const char toplevel_config_comment[] = @@ -31,109 +32,97 @@ static const char toplevel_config_comment[] = "\n" " For more information about notmuch, see https://notmuchmail.org"; -static const char database_config_comment[] = - " Database configuration\n" - "\n" - " The only value supported here is 'path' which should be the top-level\n" - " directory where your mail currently exists and to where mail will be\n" - " delivered in the future. Files should be individual email messages.\n" - " Notmuch will store its database within a sub-directory of the path\n" - " configured here named \".notmuch\".\n"; - -static const char new_config_comment[] = - " Configuration for \"notmuch new\"\n" - "\n" - " The following options are supported here:\n" - "\n" - "\ttags A list (separated by ';') of the tags that will be\n" - "\t added to all messages incorporated by \"notmuch new\".\n" - "\n" - "\tignore A list (separated by ';') of file and directory names\n" - "\t that will not be searched for messages by \"notmuch new\".\n" - "\n" - "\t NOTE: *Every* file/directory that goes by one of those\n" - "\t names will be ignored, independent of its depth/location\n" - "\t in the mail store.\n"; - -static const char user_config_comment[] = - " User configuration\n" - "\n" - " Here is where you can let notmuch know how you would like to be\n" - " addressed. Valid settings are\n" - "\n" - "\tname Your full name.\n" - "\tprimary_email Your primary email address.\n" - "\tother_email A list (separated by ';') of other email addresses\n" - "\t at which you receive email.\n" - "\n" - " Notmuch will use the various email addresses configured here when\n" - " formatting replies. It will avoid including your own addresses in the\n" - " recipient list of replies, and will set the From address based on the\n" - " address to which the original email was addressed.\n"; - -static const char maildir_config_comment[] = - " Maildir compatibility configuration\n" - "\n" - " The following option is supported here:\n" - "\n" - "\tsynchronize_flags Valid values are true and false.\n" - "\n" - "\tIf true, then the following maildir flags (in message filenames)\n" - "\twill be synchronized with the corresponding notmuch tags:\n" - "\n" - "\t\tFlag Tag\n" - "\t\t---- -------\n" - "\t\tD draft\n" - "\t\tF flagged\n" - "\t\tP passed\n" - "\t\tR replied\n" - "\t\tS unread (added when 'S' flag is not present)\n" - "\n" - "\tThe \"notmuch new\" command will notice flag changes in filenames\n" - "\tand update tags, while the \"notmuch tag\" and \"notmuch restore\"\n" - "\tcommands will notice tag changes and update flags in filenames\n"; - -static const char search_config_comment[] = - " Search configuration\n" - "\n" - " The following option is supported here:\n" - "\n" - "\texclude_tags\n" - "\t\tA ;-separated list of tags that will be excluded from\n" - "\t\tsearch results by default. Using an excluded tag in a\n" - "\t\tquery will override that exclusion.\n"; - -static const char crypto_config_comment[] = - " Cryptography related configuration\n" - "\n" - " The following old option is now ignored:\n" - "\n" - "\tgpgpath\n" - "\t\tThis option was used by older builds of notmuch to choose\n" - "\t\tthe version of gpg to use.\n" - "\t\tSetting $PATH is a better approach.\n"; +struct config_group { + const char *group_name; + const char *comment; +} group_comment_table [] = { + { + "database", + " Database configuration\n" + "\n" + " The only value supported here is 'path' which should be the top-level\n" + " directory where your mail currently exists and to where mail will be\n" + " delivered in the future. Files should be individual email messages.\n" + " Notmuch will store its database within a sub-directory of the path\n" + " configured here named \".notmuch\".\n" + }, + { + "user", + " User configuration\n" + "\n" + " Here is where you can let notmuch know how you would like to be\n" + " addressed. Valid settings are\n" + "\n" + "\tname Your full name.\n" + "\tprimary_email Your primary email address.\n" + "\tother_email A list (separated by ';') of other email addresses\n" + "\t at which you receive email.\n" + "\n" + " Notmuch will use the various email addresses configured here when\n" + " formatting replies. It will avoid including your own addresses in the\n" + " recipient list of replies, and will set the From address based on the\n" + " address to which the original email was addressed.\n" + }, + { + "new", + " Configuration for \"notmuch new\"\n" + "\n" + " The following options are supported here:\n" + "\n" + "\ttags A list (separated by ';') of the tags that will be\n" + "\t added to all messages incorporated by \"notmuch new\".\n" + "\n" + "\tignore A list (separated by ';') of file and directory names\n" + "\t that will not be searched for messages by \"notmuch new\".\n" + "\n" + "\t NOTE: *Every* file/directory that goes by one of those\n" + "\t names will be ignored, independent of its depth/location\n" + "\t in the mail store.\n" + }, + { + "search", + " Search configuration\n" + "\n" + " The following option is supported here:\n" + "\n" + "\texclude_tags\n" + "\t\tA ;-separated list of tags that will be excluded from\n" + "\t\tsearch results by default. Using an excluded tag in a\n" + "\t\tquery will override that exclusion.\n" + }, + { + "maildir", + " Maildir compatibility configuration\n" + "\n" + " The following option is supported here:\n" + "\n" + "\tsynchronize_flags Valid values are true and false.\n" + "\n" + "\tIf true, then the following maildir flags (in message filenames)\n" + "\twill be synchronized with the corresponding notmuch tags:\n" + "\n" + "\t\tFlag Tag\n" + "\t\t---- -------\n" + "\t\tD draft\n" + "\t\tF flagged\n" + "\t\tP passed\n" + "\t\tR replied\n" + "\t\tS unread (added when 'S' flag is not present)\n" + "\n" + "\tThe \"notmuch new\" command will notice flag changes in filenames\n" + "\tand update tags, while the \"notmuch tag\" and \"notmuch restore\"\n" + "\tcommands will notice tag changes and update flags in filenames\n" + }, +}; -struct _notmuch_config { +struct _notmuch_conffile { char *filename; GKeyFile *key_file; bool is_new; - - char *database_path; - char *user_name; - char *user_primary_email; - const char **user_other_email; - size_t user_other_email_length; - const char **new_tags; - size_t new_tags_length; - const char **new_ignore; - size_t new_ignore_length; - bool maildir_synchronize_flags; - const char **search_exclude_tags; - size_t search_exclude_tags_length; }; static int -notmuch_config_destructor (notmuch_config_t *config) +notmuch_conffile_destructor (notmuch_conffile_t *config) { if (config->key_file) g_key_file_free (config->key_file); @@ -141,72 +130,8 @@ notmuch_config_destructor (notmuch_config_t *config) return 0; } -static char * -get_name_from_passwd_file (void *ctx) -{ - long pw_buf_size; - char *pw_buf; - struct passwd passwd, *ignored; - char *name; - int e; - - pw_buf_size = sysconf (_SC_GETPW_R_SIZE_MAX); - if (pw_buf_size == -1) pw_buf_size = 64; - pw_buf = talloc_size (ctx, pw_buf_size); - - while ((e = getpwuid_r (getuid (), &passwd, pw_buf, - pw_buf_size, &ignored)) == ERANGE) { - pw_buf_size = pw_buf_size * 2; - pw_buf = talloc_zero_size (ctx, pw_buf_size); - } - - if (e == 0) { - char *comma = strchr (passwd.pw_gecos, ','); - if (comma) - name = talloc_strndup (ctx, passwd.pw_gecos, - comma - passwd.pw_gecos); - else - name = talloc_strdup (ctx, passwd.pw_gecos); - } else { - name = talloc_strdup (ctx, ""); - } - - talloc_free (pw_buf); - - return name; -} - -static char * -get_username_from_passwd_file (void *ctx) -{ - long pw_buf_size; - char *pw_buf; - struct passwd passwd, *ignored; - char *name; - int e; - - pw_buf_size = sysconf (_SC_GETPW_R_SIZE_MAX); - if (pw_buf_size == -1) pw_buf_size = 64; - pw_buf = talloc_zero_size (ctx, pw_buf_size); - - while ((e = getpwuid_r (getuid (), &passwd, pw_buf, - pw_buf_size, &ignored)) == ERANGE) { - pw_buf_size = pw_buf_size * 2; - pw_buf = talloc_zero_size (ctx, pw_buf_size); - } - - if (e == 0) - name = talloc_strdup (ctx, passwd.pw_name); - else - name = talloc_strdup (ctx, ""); - - talloc_free (pw_buf); - - return name; -} - static bool -get_config_from_file (notmuch_config_t *config, bool create_new) +get_config_from_file (notmuch_conffile_t *config, bool create_new) { #define BUF_SIZE 4096 char *config_str = NULL; @@ -321,32 +246,21 @@ get_config_from_file (notmuch_config_t *config, bool create_new) * The default configuration also contains comments to guide the * user in editing the file directly. */ -notmuch_config_t * -notmuch_config_open (void *ctx, - const char *filename, - notmuch_config_mode_t config_mode) +notmuch_conffile_t * +notmuch_conffile_open (notmuch_database_t *notmuch, + const char *filename, + bool create) { - GError *error = NULL; - size_t tmp; char *notmuch_config_env = NULL; - int file_had_database_group; - int file_had_new_group; - int file_had_user_group; - int file_had_maildir_group; - int file_had_search_group; - int file_had_crypto_group; - notmuch_config_t *config = talloc_zero (ctx, notmuch_config_t); + notmuch_conffile_t *config = talloc_zero (notmuch, notmuch_conffile_t); if (config == NULL) { fprintf (stderr, "Out of memory.\n"); return NULL; } - talloc_set_destructor (config, notmuch_config_destructor); - - /* non-zero defaults */ - config->maildir_synchronize_flags = true; + talloc_set_destructor (config, notmuch_conffile_destructor); if (filename) { config->filename = talloc_strdup (config, filename); @@ -359,153 +273,36 @@ notmuch_config_open (void *ctx, config->key_file = g_key_file_new (); - if (config_mode & NOTMUCH_CONFIG_OPEN) { - bool create_new = (config_mode & NOTMUCH_CONFIG_CREATE) != 0; - - if (! get_config_from_file (config, create_new)) { - talloc_free (config); - return NULL; - } - } - - /* Whenever we know of configuration sections that don't appear in - * the configuration file, we add some comments to help the user - * understand what can be done. - * - * It would be convenient to just add those comments now, but - * apparently g_key_file will clear any comments when keys are - * added later that create the groups. So we have to check for the - * groups now, but add the comments only after setting all of our - * values. - */ - file_had_database_group = g_key_file_has_group (config->key_file, - "database"); - file_had_new_group = g_key_file_has_group (config->key_file, "new"); - file_had_user_group = g_key_file_has_group (config->key_file, "user"); - file_had_maildir_group = g_key_file_has_group (config->key_file, "maildir"); - file_had_search_group = g_key_file_has_group (config->key_file, "search"); - file_had_crypto_group = g_key_file_has_group (config->key_file, "crypto"); - - if (notmuch_config_get_database_path (config) == NULL) { - char *path = getenv ("MAILDIR"); - if (path) - path = talloc_strdup (config, path); - else - path = talloc_asprintf (config, "%s/mail", - getenv ("HOME")); - notmuch_config_set_database_path (config, path); - talloc_free (path); - } - - if (notmuch_config_get_user_name (config) == NULL) { - char *name = getenv ("NAME"); - if (name) - name = talloc_strdup (config, name); - else - name = get_name_from_passwd_file (config); - notmuch_config_set_user_name (config, name); - talloc_free (name); - } - - if (notmuch_config_get_user_primary_email (config) == NULL) { - char *email = getenv ("EMAIL"); - if (email) { - notmuch_config_set_user_primary_email (config, email); - } else { - char hostname[256]; - struct hostent *hostent; - const char *domainname; - - char *username = get_username_from_passwd_file (config); - - gethostname (hostname, 256); - hostname[255] = '\0'; - - hostent = gethostbyname (hostname); - if (hostent && (domainname = strchr (hostent->h_name, '.'))) - domainname += 1; - else - domainname = "(none)"; - - email = talloc_asprintf (config, "%s@%s.%s", - username, hostname, domainname); - - notmuch_config_set_user_primary_email (config, email); - - talloc_free (username); - talloc_free (email); - } - } - - if (notmuch_config_get_new_tags (config, &tmp) == NULL) { - const char *tags[] = { "unread", "inbox" }; - notmuch_config_set_new_tags (config, tags, 2); - } - - if (notmuch_config_get_new_ignore (config, &tmp) == NULL) { - notmuch_config_set_new_ignore (config, NULL, 0); - } - - if (notmuch_config_get_search_exclude_tags (config, &tmp) == NULL) { - if (config->is_new) { - const char *tags[] = { "deleted", "spam" }; - notmuch_config_set_search_exclude_tags (config, tags, 2); - } else { - notmuch_config_set_search_exclude_tags (config, NULL, 0); - } - } - - error = NULL; - config->maildir_synchronize_flags = - g_key_file_get_boolean (config->key_file, - "maildir", "synchronize_flags", &error); - if (error) { - notmuch_config_set_maildir_synchronize_flags (config, true); - g_error_free (error); + if (! get_config_from_file (config, create)) { + talloc_free (config); + return NULL; } - /* Whenever we know of configuration sections that don't appear in - * the configuration file, we add some comments to help the user - * understand what can be done. */ if (config->is_new) g_key_file_set_comment (config->key_file, NULL, NULL, toplevel_config_comment, NULL); - if (! file_had_database_group) - g_key_file_set_comment (config->key_file, "database", NULL, - database_config_comment, NULL); - - if (! file_had_new_group) - g_key_file_set_comment (config->key_file, "new", NULL, - new_config_comment, NULL); - - if (! file_had_user_group) - g_key_file_set_comment (config->key_file, "user", NULL, - user_config_comment, NULL); - - if (! file_had_maildir_group) - g_key_file_set_comment (config->key_file, "maildir", NULL, - maildir_config_comment, NULL); - - if (! file_had_search_group) - g_key_file_set_comment (config->key_file, "search", NULL, - search_config_comment, NULL); - - if (! file_had_crypto_group) - g_key_file_set_comment (config->key_file, "crypto", NULL, - crypto_config_comment, NULL); - + for (size_t i = 0; i < ARRAY_SIZE (group_comment_table); i++) { + const char *name = group_comment_table[i].group_name; + if (! g_key_file_has_group (config->key_file, name)) { + /* Force group to exist before adding comment */ + g_key_file_set_value (config->key_file, name, "dummy_key", "dummy_val"); + g_key_file_remove_key (config->key_file, name, "dummy_key", NULL); + g_key_file_set_comment (config->key_file, name, NULL, + group_comment_table[i].comment, NULL); + } + } return config; } -/* Close the given notmuch_config_t object, freeing all resources. +/* Close the given notmuch_conffile_t object, freeing all resources. * * Note: Any changes made to the configuration are *not* saved by this - * function. To save changes, call notmuch_config_save before - * notmuch_config_close. + * function. To save changes, call notmuch_conffile_save before + * notmuch_conffile_close. */ void -notmuch_config_close (notmuch_config_t *config) +notmuch_conffile_close (notmuch_conffile_t *config) { talloc_free (config); } @@ -518,7 +315,7 @@ notmuch_config_close (notmuch_config_t *config) * printing a description of the error to stderr). */ int -notmuch_config_save (notmuch_config_t *config) +notmuch_conffile_save (notmuch_conffile_t *config) { size_t length; char *data, *filename; @@ -531,7 +328,7 @@ notmuch_config_save (notmuch_config_t *config) } /* Try not to overwrite symlinks. */ - filename = canonicalize_file_name (config->filename); + filename = notmuch_canonicalize_file_name (config->filename); if (! filename) { if (errno == ENOENT) { filename = strdup (config->filename); @@ -568,200 +365,78 @@ notmuch_config_save (notmuch_config_t *config) } bool -notmuch_config_is_new (notmuch_config_t *config) +notmuch_conffile_is_new (notmuch_conffile_t *config) { return config->is_new; } -static const char * -_config_get (notmuch_config_t *config, char **field, - const char *group, const char *key) -{ - /* read from config file and cache value, if not cached already */ - if (*field == NULL) { - char *value; - value = g_key_file_get_string (config->key_file, group, key, NULL); - if (value) { - *field = talloc_strdup (config, value); - free (value); - } - } - return *field; -} - static void -_config_set (notmuch_config_t *config, char **field, +_config_set (notmuch_conffile_t *config, const char *group, const char *key, const char *value) { g_key_file_set_string (config->key_file, group, key, value); - - /* drop the cached value */ - talloc_free (*field); - *field = NULL; -} - -static const char ** -_config_get_list (notmuch_config_t *config, - const char *section, const char *key, - const char ***outlist, size_t *list_length, size_t *ret_length) -{ - assert (outlist); - - /* read from config file and cache value, if not cached already */ - if (*outlist == NULL) { - - char **inlist = g_key_file_get_string_list (config->key_file, - section, key, list_length, NULL); - if (inlist) { - unsigned int i; - - *outlist = talloc_size (config, sizeof (char *) * (*list_length + 1)); - - for (i = 0; i < *list_length; i++) - (*outlist)[i] = talloc_strdup (*outlist, inlist[i]); - - (*outlist)[i] = NULL; - - g_strfreev (inlist); - } - } - - if (ret_length) - *ret_length = *list_length; - - return *outlist; } static void -_config_set_list (notmuch_config_t *config, +_config_set_list (notmuch_conffile_t *config, const char *group, const char *key, const char *list[], - size_t length, const char ***config_var ) + size_t length) { g_key_file_set_string_list (config->key_file, group, key, list, length); - - /* drop the cached value */ - talloc_free (*config_var); - *config_var = NULL; -} - -const char * -notmuch_config_get_database_path (notmuch_config_t *config) -{ - char *db_path = (char *) _config_get (config, &config->database_path, "database", "path"); - - if (db_path && *db_path != '/') { - /* If the path in the configuration file begins with any - * character other than /, presume that it is relative to - * $HOME and update as appropriate. - */ - char *abs_path = talloc_asprintf (config, "%s/%s", getenv ("HOME"), db_path); - talloc_free (db_path); - db_path = config->database_path = abs_path; - } - - return db_path; } void -notmuch_config_set_database_path (notmuch_config_t *config, - const char *database_path) +notmuch_conffile_set_database_path (notmuch_conffile_t *config, + const char *database_path) { - _config_set (config, &config->database_path, "database", "path", database_path); -} - -const char * -notmuch_config_get_user_name (notmuch_config_t *config) -{ - return _config_get (config, &config->user_name, "user", "name"); + _config_set (config, "database", "path", database_path); } void -notmuch_config_set_user_name (notmuch_config_t *config, - const char *user_name) -{ - _config_set (config, &config->user_name, "user", "name", user_name); -} - -const char * -notmuch_config_get_user_primary_email (notmuch_config_t *config) -{ - return _config_get (config, &config->user_primary_email, "user", "primary_email"); -} - -void -notmuch_config_set_user_primary_email (notmuch_config_t *config, - const char *primary_email) -{ - _config_set (config, &config->user_primary_email, "user", "primary_email", primary_email); -} - -const char ** -notmuch_config_get_user_other_email (notmuch_config_t *config, size_t *length) +notmuch_conffile_set_user_name (notmuch_conffile_t *config, + const char *user_name) { - return _config_get_list (config, "user", "other_email", - &(config->user_other_email), - &(config->user_other_email_length), length); -} - -const char ** -notmuch_config_get_new_tags (notmuch_config_t *config, size_t *length) -{ - return _config_get_list (config, "new", "tags", - &(config->new_tags), - &(config->new_tags_length), length); -} - -const char ** -notmuch_config_get_new_ignore (notmuch_config_t *config, size_t *length) -{ - return _config_get_list (config, "new", "ignore", - &(config->new_ignore), - &(config->new_ignore_length), length); + _config_set (config, "user", "name", user_name); } void -notmuch_config_set_user_other_email (notmuch_config_t *config, - const char *list[], - size_t length) +notmuch_conffile_set_user_primary_email (notmuch_conffile_t *config, + const char *primary_email) { - _config_set_list (config, "user", "other_email", list, length, - &(config->user_other_email)); + _config_set (config, "user", "primary_email", primary_email); } void -notmuch_config_set_new_tags (notmuch_config_t *config, - const char *list[], - size_t length) +notmuch_conffile_set_user_other_email (notmuch_conffile_t *config, + const char *list[], + size_t length) { - _config_set_list (config, "new", "tags", list, length, - &(config->new_tags)); + _config_set_list (config, "user", "other_email", list, length); } void -notmuch_config_set_new_ignore (notmuch_config_t *config, +notmuch_conffile_set_new_tags (notmuch_conffile_t *config, const char *list[], size_t length) { - _config_set_list (config, "new", "ignore", list, length, - &(config->new_ignore)); + _config_set_list (config, "new", "tags", list, length); } -const char ** -notmuch_config_get_search_exclude_tags (notmuch_config_t *config, size_t *length) +void +notmuch_conffile_set_new_ignore (notmuch_conffile_t *config, + const char *list[], + size_t length) { - return _config_get_list (config, "search", "exclude_tags", - &(config->search_exclude_tags), - &(config->search_exclude_tags_length), length); + _config_set_list (config, "new", "ignore", list, length); } void -notmuch_config_set_search_exclude_tags (notmuch_config_t *config, - const char *list[], - size_t length) +notmuch_conffile_set_search_exclude_tags (notmuch_conffile_t *config, + const char *list[], + size_t length) { - _config_set_list (config, "search", "exclude_tags", list, length, - &(config->search_exclude_tags)); + _config_set_list (config, "search", "exclude_tags", list, length); } @@ -833,16 +508,15 @@ validate_field_name (const char *str) typedef struct config_key { const char *name; - bool in_db; bool prefix; bool (*validate)(const char *); } config_key_info_t; static struct config_key config_key_table[] = { - { "index.decrypt", true, false, NULL }, - { "index.header.", true, true, validate_field_name }, - { "query.", true, true, NULL }, + { "index.decrypt", false, NULL }, + { "index.header.", true, validate_field_name }, + { "query.", true, NULL }, }; static config_key_info_t * @@ -859,95 +533,30 @@ _config_key_info (const char *item) return NULL; } -static bool -_stored_in_db (const char *item) -{ - config_key_info_t *info; - - info = _config_key_info (item); - - return (info && info->in_db); -} - static int -_print_db_config (notmuch_config_t *config, const char *name) +notmuch_config_command_get (notmuch_database_t *notmuch, char *item) { - notmuch_database_t *notmuch; - char *val; - - if (notmuch_database_open (notmuch_config_get_database_path (config), - NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much)) - return EXIT_FAILURE; + notmuch_config_values_t *list; - /* XXX Handle UUID mismatch? */ - - if (print_status_database ("notmuch config", notmuch, - notmuch_database_get_config (notmuch, name, &val))) - return EXIT_FAILURE; - - puts (val); - - return EXIT_SUCCESS; -} - -static int -notmuch_config_command_get (notmuch_config_t *config, char *item) -{ - if (strcmp (item, "database.path") == 0) { - printf ("%s\n", notmuch_config_get_database_path (config)); - } else if (strcmp (item, "user.name") == 0) { - printf ("%s\n", notmuch_config_get_user_name (config)); - } else if (strcmp (item, "user.primary_email") == 0) { - printf ("%s\n", notmuch_config_get_user_primary_email (config)); - } else if (strcmp (item, "user.other_email") == 0) { - const char **other_email; - size_t i, length; - - other_email = notmuch_config_get_user_other_email (config, &length); - for (i = 0; i < length; i++) - printf ("%s\n", other_email[i]); - } else if (strcmp (item, "new.tags") == 0) { - const char **tags; - size_t i, length; - - tags = notmuch_config_get_new_tags (config, &length); - for (i = 0; i < length; i++) - printf ("%s\n", tags[i]); - } else if (STRNCMP_LITERAL (item, BUILT_WITH_PREFIX) == 0) { - printf ("%s\n", - notmuch_built_with (item + strlen (BUILT_WITH_PREFIX)) ? "true" : "false"); - } else if (_stored_in_db (item)) { - return _print_db_config (config, item); + if (STRNCMP_LITERAL (item, BUILT_WITH_PREFIX) == 0) { + if (notmuch_built_with (item + strlen (BUILT_WITH_PREFIX))) + puts ("true"); + else + puts ("false"); } else { - char **value; - size_t i, length; - char *group, *key; - - if (_item_split (item, &group, &key)) - return 1; - - value = g_key_file_get_string_list (config->key_file, - group, key, - &length, NULL); - if (value == NULL) { - fprintf (stderr, "Unknown configuration item: %s.%s\n", - group, key); - return 1; + for (list = notmuch_config_get_values_string (notmuch, item); + notmuch_config_values_valid (list); + notmuch_config_values_move_to_next (list)) { + const char *val = notmuch_config_values_get (list); + puts (val); } - - for (i = 0; i < length; i++) - printf ("%s\n", value[i]); - - g_strfreev (value); } - - return 0; + return EXIT_SUCCESS; } static int -_set_db_config (notmuch_config_t *config, const char *key, int argc, char **argv) +_set_db_config (notmuch_database_t *notmuch, const char *key, int argc, char **argv) { - notmuch_database_t *notmuch; const char *val = ""; if (argc > 1) { @@ -960,12 +569,11 @@ _set_db_config (notmuch_config_t *config, const char *key, int argc, char **argv val = argv[0]; } - if (notmuch_database_open (notmuch_config_get_database_path (config), - NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much)) + if (print_status_database ("notmuch config", notmuch, + notmuch_database_reopen (notmuch, + NOTMUCH_DATABASE_MODE_READ_WRITE))) return EXIT_FAILURE; - /* XXX Handle UUID mismatch? */ - if (print_status_database ("notmuch config", notmuch, notmuch_database_set_config (notmuch, key, val))) return EXIT_FAILURE; @@ -978,10 +586,37 @@ _set_db_config (notmuch_config_t *config, const char *key, int argc, char **argv } static int -notmuch_config_command_set (notmuch_config_t *config, char *item, int argc, char *argv[]) +notmuch_config_command_set (notmuch_database_t *notmuch, + int argc, char *argv[]) { char *group, *key; config_key_info_t *key_info; + notmuch_conffile_t *config; + bool update_database = false; + int opt_index, ret; + char *item; + + notmuch_opt_desc_t options[] = { + { .opt_bool = &update_database, .name = "database" }, + { } + }; + + opt_index = parse_arguments (argc, argv, options, 1); + if (opt_index < 0) + return EXIT_FAILURE; + + argc -= opt_index; + argv += opt_index; + + if (argc < 1) { + fprintf (stderr, "Error: notmuch config set requires at least " + "one argument.\n"); + return EXIT_FAILURE; + } + + item = argv[0]; + argv++; + argc--; if (STRNCMP_LITERAL (item, BUILT_WITH_PREFIX) == 0) { fprintf (stderr, "Error: read only option: %s\n", item); @@ -992,13 +627,18 @@ notmuch_config_command_set (notmuch_config_t *config, char *item, int argc, char if (key_info && key_info->validate && (! key_info->validate (item))) return 1; - if (key_info && key_info->in_db) { - return _set_db_config (config, item, argc, argv); + if (update_database) { + return _set_db_config (notmuch, item, argc, argv); } if (_item_split (item, &group, &key)) return 1; + config = notmuch_conffile_open (notmuch, + notmuch_config_path (notmuch), false); + if (! config) + return 1; + /* With only the name of an item, we clear it from the * configuration file. * @@ -1019,7 +659,11 @@ notmuch_config_command_set (notmuch_config_t *config, char *item, int argc, char break; } - return notmuch_config_save (config); + ret = notmuch_conffile_save (config); + + notmuch_conffile_close (config); + + return ret; } static @@ -1038,71 +682,24 @@ _notmuch_config_list_built_with () } static int -_list_db_config (notmuch_config_t *config) -{ - notmuch_database_t *notmuch; - notmuch_config_list_t *list; - - if (notmuch_database_open (notmuch_config_get_database_path (config), - NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much)) - return EXIT_FAILURE; - - /* XXX Handle UUID mismatch? */ - - - if (print_status_database ("notmuch config", notmuch, - notmuch_database_get_config_list (notmuch, "", &list))) - return EXIT_FAILURE; - - for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) { - printf ("%s=%s\n", notmuch_config_list_key (list), notmuch_config_list_value (list)); - } - notmuch_config_list_destroy (list); - - return EXIT_SUCCESS; -} - -static int -notmuch_config_command_list (notmuch_config_t *config) +notmuch_config_command_list (notmuch_database_t *notmuch) { - char **groups; - size_t g, groups_length; - - groups = g_key_file_get_groups (config->key_file, &groups_length); - if (groups == NULL) - return 1; - - for (g = 0; g < groups_length; g++) { - char **keys; - size_t k, keys_length; - - keys = g_key_file_get_keys (config->key_file, - groups[g], &keys_length, NULL); - if (keys == NULL) - continue; - - for (k = 0; k < keys_length; k++) { - char *value; - - value = g_key_file_get_string (config->key_file, - groups[g], keys[k], NULL); - if (value != NULL) { - printf ("%s.%s=%s\n", groups[g], keys[k], value); - free (value); - } - } - - g_strfreev (keys); - } - - g_strfreev (groups); + notmuch_config_pairs_t *list; _notmuch_config_list_built_with (); - return _list_db_config (config); + for (list = notmuch_config_get_pairs (notmuch, ""); + notmuch_config_pairs_valid (list); + notmuch_config_pairs_move_to_next (list)) { + const char *value = notmuch_config_pairs_value (list); + if (value) + printf ("%s=%s\n", notmuch_config_pairs_key (list), value); + } + notmuch_config_pairs_destroy (list); + return EXIT_SUCCESS; } int -notmuch_config_command (notmuch_config_t *config, int argc, char *argv[]) +notmuch_config_command (notmuch_database_t *notmuch, int argc, char *argv[]) { int ret; int opt_index; @@ -1130,16 +727,11 @@ notmuch_config_command (notmuch_config_t *config, int argc, char *argv[]) "one argument.\n"); return EXIT_FAILURE; } - ret = notmuch_config_command_get (config, argv[1]); + ret = notmuch_config_command_get (notmuch, argv[1]); } else if (strcmp (argv[0], "set") == 0) { - if (argc < 2) { - fprintf (stderr, "Error: notmuch config set requires at least " - "one argument.\n"); - return EXIT_FAILURE; - } - ret = notmuch_config_command_set (config, argv[1], argc - 2, argv + 2); + ret = notmuch_config_command_set (notmuch, argc, argv); } else if (strcmp (argv[0], "list") == 0) { - ret = notmuch_config_command_list (config); + ret = notmuch_config_command_list (notmuch); } else { fprintf (stderr, "Unrecognized argument for notmuch config: %s\n", argv[0]); @@ -1150,17 +742,10 @@ notmuch_config_command (notmuch_config_t *config, int argc, char *argv[]) } -bool -notmuch_config_get_maildir_synchronize_flags (notmuch_config_t *config) -{ - return config->maildir_synchronize_flags; -} - void -notmuch_config_set_maildir_synchronize_flags (notmuch_config_t *config, - bool synchronize_flags) +notmuch_conffile_set_maildir_synchronize_flags (notmuch_conffile_t *config, + bool synchronize_flags) { g_key_file_set_boolean (config->key_file, "maildir", "synchronize_flags", synchronize_flags); - config->maildir_synchronize_flags = synchronize_flags; } diff --git a/notmuch-count.c b/notmuch-count.c index d8ad7d6d..5ac4292b 100644 --- a/notmuch-count.c +++ b/notmuch-count.c @@ -64,10 +64,9 @@ count_files (notmuch_query_t *query) /* return 0 on success, -1 on failure */ static int print_count (notmuch_database_t *notmuch, const char *query_str, - const char **exclude_tags, size_t exclude_tags_length, int output, int print_lastmod) + notmuch_config_values_t *exclude_tags, int output, int print_lastmod) { notmuch_query_t *query; - size_t i; int count; unsigned int ucount; unsigned long revision; @@ -81,11 +80,16 @@ print_count (notmuch_database_t *notmuch, const char *query_str, return -1; } - for (i = 0; i < exclude_tags_length; i++) { - status = notmuch_query_add_tag_exclude (query, exclude_tags[i]); + for (notmuch_config_values_start (exclude_tags); + notmuch_config_values_valid (exclude_tags); + notmuch_config_values_move_to_next (exclude_tags)) { + + status = notmuch_query_add_tag_exclude (query, + notmuch_config_values_get (exclude_tags)); if (status && status != NOTMUCH_STATUS_IGNORED) { print_status_query ("notmuch count", query, status); - return -1; + ret = -1; + goto DONE; } } @@ -127,8 +131,8 @@ print_count (notmuch_database_t *notmuch, const char *query_str, } static int -count_file (notmuch_database_t *notmuch, FILE *input, const char **exclude_tags, - size_t exclude_tags_length, int output, int print_lastmod) +count_file (notmuch_database_t *notmuch, FILE *input, notmuch_config_values_t *exclude_tags, + int output, int print_lastmod) { char *line = NULL; ssize_t line_len; @@ -137,8 +141,7 @@ count_file (notmuch_database_t *notmuch, FILE *input, const char **exclude_tags, while (! ret && (line_len = getline (&line, &line_size, input)) != -1) { chomp_newline (line); - ret = print_count (notmuch, line, exclude_tags, exclude_tags_length, - output, print_lastmod); + ret = print_count (notmuch, line, exclude_tags, output, print_lastmod); } if (line) @@ -148,15 +151,13 @@ count_file (notmuch_database_t *notmuch, FILE *input, const char **exclude_tags, } int -notmuch_count_command (notmuch_config_t *config, int argc, char *argv[]) +notmuch_count_command (notmuch_database_t *notmuch, int argc, char *argv[]) { - notmuch_database_t *notmuch; char *query_str; int opt_index; int output = OUTPUT_MESSAGES; bool exclude = true; - const char **search_exclude_tags = NULL; - size_t search_exclude_tags_length = 0; + notmuch_config_values_t *exclude_tags = NULL; bool batch = false; bool print_lastmod = false; FILE *input = stdin; @@ -200,29 +201,22 @@ notmuch_count_command (notmuch_config_t *config, int argc, char *argv[]) return EXIT_FAILURE; } - if (notmuch_database_open (notmuch_config_get_database_path (config), - NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much)) - return EXIT_FAILURE; - notmuch_exit_if_unmatched_db_uuid (notmuch); - query_str = query_string_from_args (config, argc - opt_index, argv + opt_index); + query_str = query_string_from_args (notmuch, argc - opt_index, argv + opt_index); if (query_str == NULL) { fprintf (stderr, "Out of memory.\n"); return EXIT_FAILURE; } if (exclude) { - search_exclude_tags = notmuch_config_get_search_exclude_tags - (config, &search_exclude_tags_length); + exclude_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_EXCLUDE_TAGS); } if (batch) - ret = count_file (notmuch, input, search_exclude_tags, - search_exclude_tags_length, output, print_lastmod); + ret = count_file (notmuch, input, exclude_tags, output, print_lastmod); else - ret = print_count (notmuch, query_str, search_exclude_tags, - search_exclude_tags_length, output, print_lastmod); + ret = print_count (notmuch, query_str, exclude_tags, output, print_lastmod); notmuch_database_destroy (notmuch); diff --git a/notmuch-dump.c b/notmuch-dump.c index 887ef7f0..ae89e4da 100644 --- a/notmuch-dump.c +++ b/notmuch-dump.c @@ -339,7 +339,7 @@ notmuch_database_dump (notmuch_database_t *notmuch, output = NULL; goto DONE; } else - output = NULL; + output = NULL; if (output_file_name) { ret = rename (tempname, output_file_name); @@ -361,16 +361,11 @@ notmuch_database_dump (notmuch_database_t *notmuch, } int -notmuch_dump_command (notmuch_config_t *config, int argc, char *argv[]) +notmuch_dump_command (notmuch_database_t *notmuch, int argc, char *argv[]) { - notmuch_database_t *notmuch; const char *query_str = NULL; int ret; - if (notmuch_database_open (notmuch_config_get_database_path (config), - NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much)) - return EXIT_FAILURE; - notmuch_exit_if_unmatched_db_uuid (notmuch); const char *output_file_name = NULL; diff --git a/notmuch-insert.c b/notmuch-insert.c index 1d3b0150..00c00468 100644 --- a/notmuch-insert.c +++ b/notmuch-insert.c @@ -444,14 +444,12 @@ add_file (notmuch_database_t *notmuch, const char *path, tag_op_list_t *tag_ops, } int -notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[]) +notmuch_insert_command (notmuch_database_t *notmuch, int argc, char *argv[]) { notmuch_status_t status, close_status; - notmuch_database_t *notmuch; struct sigaction action; - const char *db_path; - const char **new_tags; - size_t new_tags_length; + const char *mail_root; + notmuch_config_values_t *new_tags = NULL; tag_op_list_t *tag_ops; char *query_string = NULL; const char *folder = ""; @@ -459,11 +457,11 @@ notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[]) bool keep = false; bool hooks = true; bool world_readable = false; - bool synchronize_flags; + notmuch_bool_t synchronize_flags; char *maildir; char *newpath; int opt_index; - unsigned int i; + void *local = talloc_new (NULL); notmuch_opt_desc_t options[] = { { .opt_string = &folder, .name = "folder", .allow_empty = true }, @@ -482,30 +480,39 @@ notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[]) notmuch_process_shared_options (argv[0]); - db_path = notmuch_config_get_database_path (config); - new_tags = notmuch_config_get_new_tags (config, &new_tags_length); - synchronize_flags = notmuch_config_get_maildir_synchronize_flags (config); + mail_root = notmuch_config_get (notmuch, NOTMUCH_CONFIG_MAIL_ROOT); + + new_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_NEW_TAGS); + + if (print_status_database ( + "notmuch insert", + notmuch, + notmuch_config_get_bool (notmuch, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS, + &synchronize_flags))) + return EXIT_FAILURE; - tag_ops = tag_op_list_create (config); + tag_ops = tag_op_list_create (local); if (tag_ops == NULL) { fprintf (stderr, "Out of memory.\n"); return EXIT_FAILURE; } - for (i = 0; i < new_tags_length; i++) { + for (; + notmuch_config_values_valid (new_tags); + notmuch_config_values_move_to_next (new_tags)) { const char *error_msg; - - error_msg = illegal_tag (new_tags[i], false); + const char *tag = notmuch_config_values_get (new_tags); + error_msg = illegal_tag (tag, false); if (error_msg) { fprintf (stderr, "Error: tag '%s' in new.tags: %s\n", - new_tags[i], error_msg); + tag, error_msg); return EXIT_FAILURE; } - if (tag_op_list_append (tag_ops, new_tags[i], false)) + if (tag_op_list_append (tag_ops, tag, false)) return EXIT_FAILURE; } - if (parse_tag_command_line (config, argc - opt_index, argv + opt_index, + if (parse_tag_command_line (local, argc - opt_index, argv + opt_index, &query_string, tag_ops)) return EXIT_FAILURE; @@ -519,14 +526,14 @@ notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[]) return EXIT_FAILURE; } - maildir = talloc_asprintf (config, "%s/%s", db_path, folder); + maildir = talloc_asprintf (local, "%s/%s", mail_root, folder); if (! maildir) { fprintf (stderr, "Out of memory\n"); return EXIT_FAILURE; } strip_trailing (maildir, '/'); - if (create_folder && ! maildir_create_folder (config, maildir, world_readable)) + if (create_folder && ! maildir_create_folder (local, maildir, world_readable)) return EXIT_FAILURE; /* Set up our handler for SIGINT. We do not set SA_RESTART so that copying @@ -538,16 +545,11 @@ notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[]) sigaction (SIGINT, &action, NULL); /* Write the message to the Maildir new directory. */ - newpath = maildir_write_new (config, STDIN_FILENO, maildir, world_readable); + newpath = maildir_write_new (local, STDIN_FILENO, maildir, world_readable); if (! newpath) { return EXIT_FAILURE; } - status = notmuch_database_open (notmuch_config_get_database_path (config), - NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much); - if (status) - return keep ? NOTMUCH_STATUS_SUCCESS : status_to_exit (status); - notmuch_exit_if_unmatched_db_uuid (notmuch); status = notmuch_process_shared_indexing_options (notmuch); @@ -561,7 +563,7 @@ notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[]) status = add_file (notmuch, newpath, tag_ops, synchronize_flags, keep, indexing_cli_choices.opts); /* Commit changes. */ - close_status = notmuch_database_destroy (notmuch); + close_status = notmuch_database_close (notmuch); if (close_status) { /* Hold on to the first error, if any. */ if (! status) @@ -586,8 +588,12 @@ notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[]) if (hooks && status == NOTMUCH_STATUS_SUCCESS) { /* Ignore hook failures. */ - notmuch_run_hook (db_path, "post-insert"); + notmuch_run_hook (notmuch, "post-insert"); } + notmuch_database_destroy (notmuch); + + talloc_free (local); + return status_to_exit (status); } diff --git a/notmuch-new.c b/notmuch-new.c index 4075d395..993359d6 100644 --- a/notmuch-new.c +++ b/notmuch-new.c @@ -43,13 +43,13 @@ enum verbosity { typedef struct { const char *db_path; + const char *mail_root; int output_is_a_tty; enum verbosity verbosity; bool debug; bool full_scan; - const char **new_tags; - size_t new_tags_length; + notmuch_config_values_t *new_tags; const char **ignore_verbatim; size_t ignore_verbatim_length; regex_t *ignore_regex; @@ -65,7 +65,7 @@ typedef struct { _filename_list_t *removed_directories; _filename_list_t *directory_mtimes; - bool synchronize_flags; + notmuch_bool_t synchronize_flags; } add_files_state_t; static volatile sig_atomic_t do_print_progress = 0; @@ -245,19 +245,17 @@ _special_directory (const char *entry) } static bool -_setup_ignore (notmuch_config_t *config, add_files_state_t *state) +_setup_ignore (notmuch_database_t *notmuch, add_files_state_t *state) { - const char **ignore_list, **ignore; + notmuch_config_values_t *ignore_list; int nregex = 0, nverbatim = 0; const char **verbatim = NULL; regex_t *regex = NULL; - ignore_list = notmuch_config_get_new_ignore (config, NULL); - if (! ignore_list) - return true; - - for (ignore = ignore_list; *ignore; ignore++) { - const char *s = *ignore; + for (ignore_list = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_NEW_IGNORE); + notmuch_config_values_valid (ignore_list); + notmuch_config_values_move_to_next (ignore_list)) { + const char *s = notmuch_config_values_get (ignore_list); size_t len = strlen (s); if (len == 0) { @@ -276,8 +274,8 @@ _setup_ignore (notmuch_config_t *config, add_files_state_t *state) return false; } - r = talloc_strndup (config, s + 1, len - 2); - regex = talloc_realloc (config, regex, regex_t, nregex + 1); + r = talloc_strndup (notmuch, s + 1, len - 2); + regex = talloc_realloc (notmuch, regex, regex_t, nregex + 1); preg = ®ex[nregex]; rerr = regcomp (preg, r, REG_EXTENDED | REG_NOSUB); @@ -295,7 +293,7 @@ _setup_ignore (notmuch_config_t *config, add_files_state_t *state) talloc_free (r); } else { - verbatim = talloc_realloc (config, verbatim, const char *, + verbatim = talloc_realloc (notmuch, verbatim, const char *, nverbatim + 1); verbatim[nverbatim++] = s; } @@ -310,18 +308,18 @@ _setup_ignore (notmuch_config_t *config, add_files_state_t *state) } static char * -_get_relative_path (const char *db_path, const char *dirpath, const char *entry) +_get_relative_path (const char *mail_root, const char *dirpath, const char *entry) { - size_t db_path_len = strlen (db_path); + size_t mail_root_len = strlen (mail_root); /* paranoia? */ - if (strncmp (dirpath, db_path, db_path_len) != 0) { + if (strncmp (dirpath, mail_root, mail_root_len) != 0) { fprintf (stderr, "Warning: '%s' is not a subdirectory of '%s'\n", - dirpath, db_path); + dirpath, mail_root); return NULL; } - dirpath += db_path_len; + dirpath += mail_root_len; while (*dirpath == '/') dirpath++; @@ -349,7 +347,7 @@ _entry_in_ignore_list (add_files_state_t *state, const char *dirpath, if (state->ignore_regex_length == 0) return false; - path = _get_relative_path (state->db_path, dirpath, entry); + path = _get_relative_path (state->mail_root, dirpath, entry); if (! path) return false; @@ -371,7 +369,7 @@ add_file (notmuch_database_t *notmuch, const char *filename, add_files_state_t *state) { notmuch_message_t *message = NULL; - const char **tag; + const char *tag; notmuch_status_t status; status = notmuch_database_begin_atomic (notmuch); @@ -387,13 +385,17 @@ add_file (notmuch_database_t *notmuch, const char *filename, if (state->synchronize_flags) notmuch_message_maildir_flags_to_tags (message); - for (tag = state->new_tags; *tag != NULL; tag++) { + for (notmuch_config_values_start (state->new_tags); + notmuch_config_values_valid (state->new_tags); + notmuch_config_values_move_to_next (state->new_tags)) { notmuch_bool_t is_set; + + tag = notmuch_config_values_get (state->new_tags); /* Currently all errors from has_maildir_flag are fatal */ if ((status = notmuch_message_has_maildir_flag_st (message, 'S', &is_set))) goto DONE; - if (strcmp ("unread", *tag) != 0 || ! is_set) { - notmuch_message_add_tag (message, *tag); + if (strcmp ("unread", tag) != 0 || ! is_set) { + notmuch_message_add_tag (message, tag); } } @@ -672,8 +674,9 @@ add_files (notmuch_database_t *notmuch, char *absolute = talloc_asprintf (state->removed_directories, "%s/%s", path, filename); if (state->debug) - printf ("(D) add_files, pass 2: queuing passed directory %s for deletion from database\n", - absolute); + printf ( + "(D) add_files, pass 2: queuing passed directory %s for deletion from database\n", + absolute); _filename_list_add (state->removed_directories, absolute); } @@ -755,8 +758,9 @@ add_files (notmuch_database_t *notmuch, notmuch_filenames_get (db_subdirs)); if (state->debug) - printf ("(D) add_files, pass 3: queuing leftover directory %s for deletion from database\n", - absolute); + printf ( + "(D) add_files, pass 3: queuing leftover directory %s for deletion from database\n", + absolute); _filename_list_add (state->removed_directories, absolute); @@ -962,8 +966,7 @@ remove_filename (notmuch_database_t *notmuch, /* Recursively remove all filenames from the database referring to * 'path' (or to any of its children). */ static notmuch_status_t -_remove_directory (void *ctx, - notmuch_database_t *notmuch, +_remove_directory (notmuch_database_t *notmuch, const char *path, add_files_state_t *add_files_state) { @@ -979,7 +982,7 @@ _remove_directory (void *ctx, for (files = notmuch_directory_get_child_files (directory); notmuch_filenames_valid (files); notmuch_filenames_move_to_next (files)) { - absolute = talloc_asprintf (ctx, "%s/%s", path, + absolute = talloc_asprintf (notmuch, "%s/%s", path, notmuch_filenames_get (files)); status = remove_filename (notmuch, absolute, add_files_state); talloc_free (absolute); @@ -990,9 +993,9 @@ _remove_directory (void *ctx, for (subdirs = notmuch_directory_get_child_directories (directory); notmuch_filenames_valid (subdirs); notmuch_filenames_move_to_next (subdirs)) { - absolute = talloc_asprintf (ctx, "%s/%s", path, + absolute = talloc_asprintf (notmuch, "%s/%s", path, notmuch_filenames_get (subdirs)); - status = _remove_directory (ctx, notmuch, absolute, add_files_state); + status = _remove_directory (notmuch, absolute, add_files_state); talloc_free (absolute); if (status) goto DONE; @@ -1042,10 +1045,67 @@ print_results (const add_files_state_t *state) printf ("\n"); } +static int +_maybe_upgrade (notmuch_database_t *notmuch, add_files_state_t *state) +{ + if (notmuch_database_needs_upgrade (notmuch)) { + time_t now = time (NULL); + struct tm *gm_time = gmtime (&now); + int err; + notmuch_status_t status; + const char *backup_dir = notmuch_config_get (notmuch, NOTMUCH_CONFIG_BACKUP_DIR); + const char *backup_name; + + err = mkdir (backup_dir, 0755); + if (err && errno != EEXIST) { + fprintf (stderr, "Failed to create %s: %s\n", backup_dir, strerror (errno)); + return EXIT_FAILURE; + } + + /* since dump files are written atomically, the amount of + * harm from overwriting one within a second seems + * relatively small. */ + backup_name = talloc_asprintf (notmuch, "%s/dump-%04d%02d%02dT%02d%02d%02d.gz", + backup_dir, + gm_time->tm_year + 1900, + gm_time->tm_mon + 1, + gm_time->tm_mday, + gm_time->tm_hour, + gm_time->tm_min, + gm_time->tm_sec); + + if (state->verbosity >= VERBOSITY_NORMAL) { + printf ("Welcome to a new version of notmuch! Your database will now be upgraded.\n"); + printf ("This process is safe to interrupt.\n"); + printf ("Backing up tags to %s...\n", backup_name); + } + + if (notmuch_database_dump (notmuch, backup_name, "", + DUMP_FORMAT_BATCH_TAG, DUMP_INCLUDE_DEFAULT, true)) { + fprintf (stderr, "Backup failed. Aborting upgrade."); + return EXIT_FAILURE; + } + + gettimeofday (&state->tv_start, NULL); + status = notmuch_database_upgrade ( + notmuch, + state->verbosity >= VERBOSITY_NORMAL ? upgrade_print_progress : NULL, + state); + if (status) { + printf ("Upgrade failed: %s\n", + notmuch_status_to_string (status)); + notmuch_database_destroy (notmuch); + return EXIT_FAILURE; + } + if (state->verbosity >= VERBOSITY_NORMAL) + printf ("Your notmuch database has now been upgraded.\n"); + } + return EXIT_SUCCESS; +} + int -notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]) +notmuch_new_command (notmuch_database_t *notmuch, int argc, char *argv[]) { - notmuch_database_t *notmuch; add_files_state_t add_files_state = { .verbosity = VERBOSITY_NORMAL, .debug = false, @@ -1054,9 +1114,7 @@ notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]) }; struct timeval tv_start; int ret = 0; - struct stat st; - const char *db_path; - char *dot_notmuch_path; + const char *db_path, *mail_root; struct sigaction action; _filename_node_t *f; int opt_index; @@ -1089,103 +1147,68 @@ notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]) else if (verbose) add_files_state.verbosity = VERBOSITY_VERBOSE; - add_files_state.new_tags = notmuch_config_get_new_tags (config, &add_files_state.new_tags_length); - add_files_state.synchronize_flags = notmuch_config_get_maildir_synchronize_flags (config); - db_path = notmuch_config_get_database_path (config); + add_files_state.new_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_NEW_TAGS); + + if (print_status_database ( + "notmuch new", + notmuch, + notmuch_config_get_bool (notmuch, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS, + &add_files_state.synchronize_flags))) + return EXIT_FAILURE; + + db_path = notmuch_config_get (notmuch, NOTMUCH_CONFIG_DATABASE_PATH); add_files_state.db_path = db_path; - if (! _setup_ignore (config, &add_files_state)) + mail_root = notmuch_config_get (notmuch, NOTMUCH_CONFIG_MAIL_ROOT); + add_files_state.mail_root = mail_root; + + if (! _setup_ignore (notmuch, &add_files_state)) return EXIT_FAILURE; - for (i = 0; i < add_files_state.new_tags_length; i++) { - const char *error_msg; + for (notmuch_config_values_start (add_files_state.new_tags); + notmuch_config_values_valid (add_files_state.new_tags); + notmuch_config_values_move_to_next (add_files_state.new_tags)) { + const char *tag, *error_msg; - error_msg = illegal_tag (add_files_state.new_tags[i], false); + tag = notmuch_config_values_get (add_files_state.new_tags); + error_msg = illegal_tag (tag, false); if (error_msg) { - fprintf (stderr, "Error: tag '%s' in new.tags: %s\n", - add_files_state.new_tags[i], error_msg); + fprintf (stderr, "Error: tag '%s' in new.tags: %s\n", tag, error_msg); return EXIT_FAILURE; } } if (hooks) { - ret = notmuch_run_hook (db_path, "pre-new"); + /* Drop write lock to run hook */ + status = notmuch_database_reopen (notmuch, NOTMUCH_DATABASE_MODE_READ_ONLY); + if (print_status_database ("notmuch new", notmuch, status)) + return EXIT_FAILURE; + + ret = notmuch_run_hook (notmuch, "pre-new"); if (ret) return EXIT_FAILURE; - } - dot_notmuch_path = talloc_asprintf (config, "%s/%s", db_path, ".notmuch"); + /* acquire write lock again */ + status = notmuch_database_reopen (notmuch, NOTMUCH_DATABASE_MODE_READ_WRITE); + if (print_status_database ("notmuch new", notmuch, status)) + return EXIT_FAILURE; + } - if (stat (dot_notmuch_path, &st)) { - int count; + notmuch_exit_if_unmatched_db_uuid (notmuch); - count = 0; - count_files (db_path, &count, &add_files_state); + if (notmuch_database_get_revision (notmuch, NULL) == 0) { + int count = 0; + count_files (mail_root, &count, &add_files_state); if (interrupted) return EXIT_FAILURE; if (add_files_state.verbosity >= VERBOSITY_NORMAL) printf ("Found %d total files (that's not much mail).\n", count); - if (notmuch_database_create (db_path, ¬much)) - return EXIT_FAILURE; + add_files_state.total_files = count; } else { - char *status_string = NULL; - if (notmuch_database_open_verbose (db_path, NOTMUCH_DATABASE_MODE_READ_WRITE, - ¬much, &status_string)) { - if (status_string) { - fputs (status_string, stderr); - free (status_string); - } + if (_maybe_upgrade (notmuch, &add_files_state)) return EXIT_FAILURE; - } - - notmuch_exit_if_unmatched_db_uuid (notmuch); - - if (notmuch_database_needs_upgrade (notmuch)) { - time_t now = time (NULL); - struct tm *gm_time = gmtime (&now); - - /* since dump files are written atomically, the amount of - * harm from overwriting one within a second seems - * relatively small. */ - - const char *backup_name = - talloc_asprintf (notmuch, "%s/dump-%04d%02d%02dT%02d%02d%02d.gz", - dot_notmuch_path, - gm_time->tm_year + 1900, - gm_time->tm_mon + 1, - gm_time->tm_mday, - gm_time->tm_hour, - gm_time->tm_min, - gm_time->tm_sec); - - if (add_files_state.verbosity >= VERBOSITY_NORMAL) { - printf ("Welcome to a new version of notmuch! Your database will now be upgraded.\n"); - printf ("This process is safe to interrupt.\n"); - printf ("Backing up tags to %s...\n", backup_name); - } - - if (notmuch_database_dump (notmuch, backup_name, "", - DUMP_FORMAT_BATCH_TAG, DUMP_INCLUDE_DEFAULT, true)) { - fprintf (stderr, "Backup failed. Aborting upgrade."); - return EXIT_FAILURE; - } - - gettimeofday (&add_files_state.tv_start, NULL); - status = notmuch_database_upgrade ( - notmuch, - add_files_state.verbosity >= VERBOSITY_NORMAL ? upgrade_print_progress : NULL, - &add_files_state); - if (status) { - printf ("Upgrade failed: %s\n", - notmuch_status_to_string (status)); - notmuch_database_destroy (notmuch); - return EXIT_FAILURE; - } - if (add_files_state.verbosity >= VERBOSITY_NORMAL) - printf ("Your notmuch database has now been upgraded.\n"); - } add_files_state.total_files = 0; } @@ -1209,14 +1232,11 @@ notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]) action.sa_flags = SA_RESTART; sigaction (SIGINT, &action, NULL); - talloc_free (dot_notmuch_path); - dot_notmuch_path = NULL; - gettimeofday (&add_files_state.tv_start, NULL); - add_files_state.removed_files = _filename_list_create (config); - add_files_state.removed_directories = _filename_list_create (config); - add_files_state.directory_mtimes = _filename_list_create (config); + add_files_state.removed_files = _filename_list_create (notmuch); + add_files_state.removed_directories = _filename_list_create (notmuch); + add_files_state.directory_mtimes = _filename_list_create (notmuch); if (add_files_state.verbosity == VERBOSITY_NORMAL && add_files_state.output_is_a_tty && ! debugger_is_active ()) { @@ -1224,7 +1244,7 @@ notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]) timer_is_active = true; } - ret = add_files (notmuch, db_path, &add_files_state); + ret = add_files (notmuch, mail_root, &add_files_state); if (ret) goto DONE; @@ -1236,14 +1256,15 @@ notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]) if (do_print_progress) { do_print_progress = 0; generic_print_progress ("Cleaned up", "messages", - tv_start, add_files_state.removed_messages + add_files_state.renamed_messages, + tv_start, add_files_state.removed_messages + + add_files_state.renamed_messages, add_files_state.removed_files->count); } } gettimeofday (&tv_start, NULL); for (f = add_files_state.removed_directories->head, i = 0; f && ! interrupted; f = f->next, i++) { - ret = _remove_directory (config, notmuch, f->filename, &add_files_state); + ret = _remove_directory (notmuch, f->filename, &add_files_state); if (ret) goto DONE; if (do_print_progress) { @@ -1278,10 +1299,12 @@ notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]) fprintf (stderr, "Note: A fatal error was encountered: %s\n", notmuch_status_to_string (ret)); - notmuch_database_destroy (notmuch); + notmuch_database_close (notmuch); if (hooks && ! ret && ! interrupted) - ret = notmuch_run_hook (db_path, "post-new"); + ret = notmuch_run_hook (notmuch, "post-new"); + + notmuch_database_destroy (notmuch); if (ret || interrupted) return EXIT_FAILURE; diff --git a/notmuch-reindex.c b/notmuch-reindex.c index 5a39ade1..8904c1f4 100644 --- a/notmuch-reindex.c +++ b/notmuch-reindex.c @@ -83,10 +83,9 @@ reindex_query (notmuch_database_t *notmuch, const char *query_string, } int -notmuch_reindex_command (notmuch_config_t *config, int argc, char *argv[]) +notmuch_reindex_command (notmuch_database_t *notmuch, int argc, char *argv[]) { char *query_string = NULL; - notmuch_database_t *notmuch; struct sigaction action; int opt_index; int ret; @@ -111,10 +110,6 @@ notmuch_reindex_command (notmuch_config_t *config, int argc, char *argv[]) notmuch_process_shared_options (argv[0]); - if (notmuch_database_open (notmuch_config_get_database_path (config), - NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much)) - return EXIT_FAILURE; - notmuch_exit_if_unmatched_db_uuid (notmuch); status = notmuch_process_shared_indexing_options (notmuch); @@ -124,7 +119,7 @@ notmuch_reindex_command (notmuch_config_t *config, int argc, char *argv[]) return EXIT_FAILURE; } - query_string = query_string_from_args (config, argc - opt_index, argv + opt_index); + query_string = query_string_from_args (notmuch, argc - opt_index, argv + opt_index); if (query_string == NULL) { fprintf (stderr, "Out of memory\n"); return EXIT_FAILURE; diff --git a/notmuch-reply.c b/notmuch-reply.c index ceb4f39b..08140799 100644 --- a/notmuch-reply.c +++ b/notmuch-reply.c @@ -112,25 +112,26 @@ match_address (const char *str, const char *address, address_match_t mode) /* Match given string against user's configured "primary" and "other" * addresses according to mode. */ static const char * -address_match (const char *str, notmuch_config_t *config, address_match_t mode) +address_match (const char *str, notmuch_database_t *notmuch, address_match_t mode) { const char *primary; - const char **other; - size_t i, other_len; + notmuch_config_values_t *other = NULL; if (! str || *str == '\0') return NULL; - primary = notmuch_config_get_user_primary_email (config); + primary = notmuch_config_get (notmuch, NOTMUCH_CONFIG_PRIMARY_EMAIL); if (match_address (str, primary, mode)) return primary; - other = notmuch_config_get_user_other_email (config, &other_len); - for (i = 0; i < other_len; i++) { - if (match_address (str, other[i], mode)) - return other[i]; - } + for (other = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_OTHER_EMAIL); + notmuch_config_values_valid (other); + notmuch_config_values_move_to_next (other)) { + const char *addr = notmuch_config_values_get (other); + if (match_address (str, addr, mode)) + return addr; + } return NULL; } @@ -138,26 +139,26 @@ address_match (const char *str, notmuch_config_t *config, address_match_t mode) * user's "primary" or "other" addresses. If so, return the matching * address, NULL otherwise. */ static const char * -user_address_in_string (const char *str, notmuch_config_t *config) +user_address_in_string (const char *str, notmuch_database_t *notmuch) { - return address_match (str, config, USER_ADDRESS_IN_STRING); + return address_match (str, notmuch, USER_ADDRESS_IN_STRING); } /* Do any of the addresses configured as one of the user's "primary" * or "other" addresses contain the given string. If so, return the * matching address, NULL otherwise. */ static const char * -string_in_user_address (const char *str, notmuch_config_t *config) +string_in_user_address (const char *str, notmuch_database_t *notmuch) { - return address_match (str, config, STRING_IN_USER_ADDRESS); + return address_match (str, notmuch, STRING_IN_USER_ADDRESS); } /* Is the given address configured as one of the user's "primary" or * "other" addresses. */ static bool -address_is_users (const char *address, notmuch_config_t *config) +address_is_users (const char *address, notmuch_database_t *notmuch) { - return address_match (address, config, STRING_IS_USER_ADDRESS) != NULL; + return address_match (address, notmuch, STRING_IS_USER_ADDRESS) != NULL; } /* Scan addresses in 'list'. @@ -175,7 +176,7 @@ address_is_users (const char *address, notmuch_config_t *config) */ static unsigned int scan_address_list (InternetAddressList *list, - notmuch_config_t *config, + notmuch_database_t *notmuch, GMimeMessage *message, GMimeAddressType type, const char **user_from) @@ -195,7 +196,7 @@ scan_address_list (InternetAddressList *list, group = INTERNET_ADDRESS_GROUP (address); group_list = internet_address_group_get_members (group); - n += scan_address_list (group_list, config, message, type, user_from); + n += scan_address_list (group_list, notmuch, message, type, user_from); } else { InternetAddressMailbox *mailbox; const char *name; @@ -206,7 +207,7 @@ scan_address_list (InternetAddressList *list, name = internet_address_get_name (address); addr = internet_address_mailbox_get_addr (mailbox); - if (address_is_users (addr, config)) { + if (address_is_users (addr, notmuch)) { if (user_from && *user_from == NULL) *user_from = addr; } else if (message) { @@ -324,7 +325,7 @@ get_bcc (GMimeMessage *message) */ static const char * add_recipients_from_message (GMimeMessage *reply, - notmuch_config_t *config, + notmuch_database_t *notmuch, GMimeMessage *message, bool reply_all) { @@ -346,7 +347,7 @@ add_recipients_from_message (GMimeMessage *reply, recipients = reply_to_map[i].get_header (message); - n += scan_address_list (recipients, config, reply, + n += scan_address_list (recipients, notmuch, reply, reply_to_map[i].recipient_type, &from_addr); if (! reply_all && n) { @@ -384,7 +385,7 @@ add_recipients_from_message (GMimeMessage *reply, * Return the address that was found, if any, and NULL otherwise. */ static const char * -guess_from_in_received_for (notmuch_config_t *config, const char *received) +guess_from_in_received_for (notmuch_database_t *notmuch, const char *received) { const char *ptr; @@ -392,7 +393,7 @@ guess_from_in_received_for (notmuch_config_t *config, const char *received) if (! ptr) return NULL; - return user_address_in_string (ptr, config); + return user_address_in_string (ptr, notmuch); } /* @@ -408,7 +409,7 @@ guess_from_in_received_for (notmuch_config_t *config, const char *received) * Return the address that was found, if any, and NULL otherwise. */ static const char * -guess_from_in_received_by (notmuch_config_t *config, const char *received) +guess_from_in_received_by (notmuch_database_t *notmuch, const char *received) { const char *addr; const char *by = received; @@ -446,7 +447,7 @@ guess_from_in_received_by (notmuch_config_t *config, const char *received) */ *(tld - 1) = '.'; - addr = string_in_user_address (domain, config); + addr = string_in_user_address (domain, notmuch); if (addr) { free (mta); return addr; @@ -469,12 +470,13 @@ guess_from_in_received_by (notmuch_config_t *config, const char *received) * Return the address that was found, if any, and NULL otherwise. */ static const char * -guess_from_in_received_headers (notmuch_config_t *config, - notmuch_message_t *message) +guess_from_in_received_headers (notmuch_message_t *message) { const char *received, *addr; char *sanitized; + notmuch_database_t *notmuch = notmuch_message_get_database (message); + received = notmuch_message_get_header (message, "received"); if (! received) return NULL; @@ -483,9 +485,9 @@ guess_from_in_received_headers (notmuch_config_t *config, if (! sanitized) return NULL; - addr = guess_from_in_received_for (config, sanitized); + addr = guess_from_in_received_for (notmuch, sanitized); if (! addr) - addr = guess_from_in_received_by (config, sanitized); + addr = guess_from_in_received_by (notmuch, sanitized); talloc_free (sanitized); @@ -500,7 +502,7 @@ guess_from_in_received_headers (notmuch_config_t *config, * Return the address that was found, if any, and NULL otherwise. */ static const char * -get_from_in_to_headers (notmuch_config_t *config, notmuch_message_t *message) +get_from_in_to_headers (notmuch_message_t *message) { size_t i; const char *tohdr, *addr; @@ -510,11 +512,13 @@ get_from_in_to_headers (notmuch_config_t *config, notmuch_message_t *message) "Delivered-To", }; + notmuch_database_t *notmuch = notmuch_message_get_database (message); + for (i = 0; i < ARRAY_SIZE (to_headers); i++) { tohdr = notmuch_message_get_header (message, to_headers[i]); /* Note: tohdr potentially contains a list of email addresses. */ - addr = user_address_in_string (tohdr, config); + addr = user_address_in_string (tohdr, notmuch); if (addr) return addr; } @@ -524,7 +528,6 @@ get_from_in_to_headers (notmuch_config_t *config, notmuch_message_t *message) static GMimeMessage * create_reply_message (void *ctx, - notmuch_config_t *config, notmuch_message_t *message, GMimeMessage *mime_message, bool reply_all, @@ -532,7 +535,7 @@ create_reply_message (void *ctx, { const char *subject, *from_addr = NULL; const char *in_reply_to, *orig_references, *references; - + notmuch_database_t *notmuch = notmuch_message_get_database (message); /* * Use the below header order for limited headers, "pretty" order * otherwise. @@ -558,7 +561,7 @@ create_reply_message (void *ctx, g_mime_object_set_header (GMIME_OBJECT (reply), "References", references, NULL); - from_addr = add_recipients_from_message (reply, config, + from_addr = add_recipients_from_message (reply, notmuch, mime_message, reply_all); /* The above is all that is needed for limited headers. */ @@ -578,7 +581,7 @@ create_reply_message (void *ctx, * Delivered-To: headers. */ if (from_addr == NULL) - from_addr = get_from_in_to_headers (config, message); + from_addr = get_from_in_to_headers (message); /* * Check for a (for ) clause in Received: headers, @@ -586,14 +589,14 @@ create_reply_message (void *ctx, * of Received: headers */ if (from_addr == NULL) - from_addr = guess_from_in_received_headers (config, message); + from_addr = guess_from_in_received_headers (message); /* Default to user's primary address. */ if (from_addr == NULL) - from_addr = notmuch_config_get_user_primary_email (config); + from_addr = notmuch_config_get (notmuch, NOTMUCH_CONFIG_PRIMARY_EMAIL); from_addr = talloc_asprintf (ctx, "%s <%s>", - notmuch_config_get_user_name (config), + notmuch_config_get (notmuch, NOTMUCH_CONFIG_USER_NAME), from_addr); g_mime_object_set_header (GMIME_OBJECT (reply), "From", from_addr, NULL); @@ -615,7 +618,7 @@ enum { }; static int -do_reply (notmuch_config_t *config, +do_reply (notmuch_database_t *notmuch, notmuch_query_t *query, notmuch_show_params_t *params, int format, @@ -636,14 +639,16 @@ do_reply (notmuch_config_t *config, return 1; if (count != 1) { - fprintf (stderr, "Error: search term did not match precisely one message (matched %u messages).\n", count); + fprintf (stderr, + "Error: search term did not match precisely one message (matched %u messages).\n", + count); return 1; } if (format == FORMAT_JSON) - sp = sprinter_json_create (config, stdout); + sp = sprinter_json_create (notmuch, stdout); else - sp = sprinter_sexp_create (config, stdout); + sp = sprinter_sexp_create (notmuch, stdout); } status = notmuch_query_search_messages (query, &messages); @@ -655,10 +660,10 @@ do_reply (notmuch_config_t *config, notmuch_messages_move_to_next (messages)) { message = notmuch_messages_get (messages); - if (mime_node_open (config, message, ¶ms->crypto, &node)) + if (mime_node_open (notmuch, message, ¶ms->crypto, &node)) return 1; - reply = create_reply_message (config, config, message, + reply = create_reply_message (notmuch, message, GMIME_MESSAGE (node->part), reply_all, format == FORMAT_HEADERS_ONLY); if (! reply) @@ -675,7 +680,7 @@ do_reply (notmuch_config_t *config, /* Start the original */ sp->map_key (sp, "original"); - format_part_sprinter (config, sp, node, true, false); + format_part_sprinter (notmuch, sp, node, true, false); /* End */ sp->end (sp); @@ -700,9 +705,8 @@ do_reply (notmuch_config_t *config, } int -notmuch_reply_command (notmuch_config_t *config, int argc, char *argv[]) +notmuch_reply_command (notmuch_database_t *notmuch, int argc, char *argv[]) { - notmuch_database_t *notmuch; notmuch_query_t *query; char *query_string; int opt_index; @@ -743,7 +747,7 @@ notmuch_reply_command (notmuch_config_t *config, int argc, char *argv[]) notmuch_exit_if_unsupported_format (); - query_string = query_string_from_args (config, argc - opt_index, argv + opt_index); + query_string = query_string_from_args (notmuch, argc - opt_index, argv + opt_index); if (query_string == NULL) { fprintf (stderr, "Out of memory\n"); return EXIT_FAILURE; @@ -754,10 +758,6 @@ notmuch_reply_command (notmuch_config_t *config, int argc, char *argv[]) return EXIT_FAILURE; } - if (notmuch_database_open (notmuch_config_get_database_path (config), - NOTMUCH_DATABASE_MODE_READ_ONLY, ¬much)) - return EXIT_FAILURE; - notmuch_exit_if_unmatched_db_uuid (notmuch); query = notmuch_query_create (notmuch, query_string); @@ -766,7 +766,7 @@ notmuch_reply_command (notmuch_config_t *config, int argc, char *argv[]) return EXIT_FAILURE; } - if (do_reply (config, query, ¶ms, format, reply_all) != 0) + if (do_reply (notmuch, query, ¶ms, format, reply_all) != 0) return EXIT_FAILURE; _notmuch_crypto_cleanup (¶ms.crypto); diff --git a/notmuch-restore.c b/notmuch-restore.c index e2dc3d45..1a81212f 100644 --- a/notmuch-restore.c +++ b/notmuch-restore.c @@ -219,9 +219,8 @@ parse_sup_line (void *ctx, char *line, } int -notmuch_restore_command (notmuch_config_t *config, int argc, char *argv[]) +notmuch_restore_command (notmuch_database_t *notmuch, int argc, char *argv[]) { - notmuch_database_t *notmuch; bool accumulate = false; tag_op_flag_t flags = 0; tag_op_list_t *tag_ops; @@ -238,12 +237,16 @@ notmuch_restore_command (notmuch_config_t *config, int argc, char *argv[]) int include = 0; int input_format = DUMP_FORMAT_AUTO; int errnum; + notmuch_bool_t synchronize_flags; - if (notmuch_database_open (notmuch_config_get_database_path (config), - NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much)) + if (print_status_database ( + "notmuch restore", + notmuch, + notmuch_config_get_bool (notmuch, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS, + &synchronize_flags))) return EXIT_FAILURE; - if (notmuch_config_get_maildir_synchronize_flags (config)) + if (synchronize_flags) flags |= TAG_FLAG_MAILDIR_SYNC; notmuch_opt_desc_t options[] = { @@ -310,7 +313,7 @@ notmuch_restore_command (notmuch_config_t *config, int argc, char *argv[]) goto DONE; } - tag_ops = tag_op_list_create (config); + tag_ops = tag_op_list_create (notmuch); if (tag_ops == NULL) { fprintf (stderr, "Out of memory.\n"); ret = EXIT_FAILURE; @@ -340,7 +343,8 @@ notmuch_restore_command (notmuch_config_t *config, int argc, char *argv[]) if (ret) goto DONE; } - if ((include & DUMP_INCLUDE_PROPERTIES) && line_len >= 2 && line[0] == '#' && line[1] == '=') { + if ((include & DUMP_INCLUDE_PROPERTIES) && line_len >= 2 && line[0] == '#' && line[1] == + '=') { ret = process_properties_line (notmuch, line + 2); if (ret) goto DONE; @@ -357,6 +361,7 @@ notmuch_restore_command (notmuch_config_t *config, int argc, char *argv[]) } char *p; + for (p = line; (input_format == DUMP_FORMAT_AUTO) && *p; p++) { if (*p == '(') input_format = DUMP_FORMAT_SUP; @@ -377,9 +382,10 @@ notmuch_restore_command (notmuch_config_t *config, int argc, char *argv[]) if (line_ctx != NULL) talloc_free (line_ctx); - line_ctx = talloc_new (config); + line_ctx = talloc_new (notmuch); - if ((include & DUMP_INCLUDE_PROPERTIES) && line_len >= 2 && line[0] == '#' && line[1] == '=') { + if ((include & DUMP_INCLUDE_PROPERTIES) && line_len >= 2 && line[0] == '#' && line[1] == + '=') { ret = process_properties_line (notmuch, line + 2); if (ret) goto DONE; diff --git a/notmuch-search.c b/notmuch-search.c index 2805d960..244817a9 100644 --- a/notmuch-search.c +++ b/notmuch-search.c @@ -52,6 +52,7 @@ typedef enum { typedef struct { notmuch_database_t *notmuch; + void *talloc_ctx; int format_sel; sprinter_t *format; int exclude; @@ -677,28 +678,29 @@ do_search_tags (const search_context_t *ctx) } static int -_notmuch_search_prepare (search_context_t *ctx, notmuch_config_t *config, int argc, char *argv[]) +_notmuch_search_prepare (search_context_t *ctx, int argc, char *argv[]) { char *query_str; - unsigned int i; - char *status_string = NULL; + + if (! ctx->talloc_ctx) + ctx->talloc_ctx = talloc_new (NULL); switch (ctx->format_sel) { case NOTMUCH_FORMAT_TEXT: - ctx->format = sprinter_text_create (config, stdout); + ctx->format = sprinter_text_create (ctx->talloc_ctx, stdout); break; case NOTMUCH_FORMAT_TEXT0: if (ctx->output == OUTPUT_SUMMARY) { fprintf (stderr, "Error: --format=text0 is not compatible with --output=summary.\n"); return EXIT_FAILURE; } - ctx->format = sprinter_text0_create (config, stdout); + ctx->format = sprinter_text0_create (ctx->talloc_ctx, stdout); break; case NOTMUCH_FORMAT_JSON: - ctx->format = sprinter_json_create (config, stdout); + ctx->format = sprinter_json_create (ctx->talloc_ctx, stdout); break; case NOTMUCH_FORMAT_SEXP: - ctx->format = sprinter_sexp_create (config, stdout); + ctx->format = sprinter_sexp_create (ctx->talloc_ctx, stdout); break; default: /* this should never happen */ @@ -707,18 +709,6 @@ _notmuch_search_prepare (search_context_t *ctx, notmuch_config_t *config, int ar notmuch_exit_if_unsupported_format (); - if (notmuch_database_open_verbose ( - notmuch_config_get_database_path (config), - NOTMUCH_DATABASE_MODE_READ_ONLY, &ctx->notmuch, &status_string)) { - - if (status_string) { - fputs (status_string, stderr); - free (status_string); - } - - return EXIT_FAILURE; - } - notmuch_exit_if_unmatched_db_uuid (ctx->notmuch); query_str = query_string_from_args (ctx->notmuch, argc, argv); @@ -748,21 +738,20 @@ _notmuch_search_prepare (search_context_t *ctx, notmuch_config_t *config, int ar } if (ctx->exclude != NOTMUCH_EXCLUDE_FALSE) { - const char **search_exclude_tags; - size_t search_exclude_tags_length; + notmuch_config_values_t *exclude_tags; notmuch_status_t status; - search_exclude_tags = notmuch_config_get_search_exclude_tags ( - config, &search_exclude_tags_length); + for (exclude_tags = notmuch_config_get_values (ctx->notmuch, NOTMUCH_CONFIG_EXCLUDE_TAGS); + notmuch_config_values_valid (exclude_tags); + notmuch_config_values_move_to_next (exclude_tags)) { - for (i = 0; i < search_exclude_tags_length; i++) { - status = notmuch_query_add_tag_exclude (ctx->query, search_exclude_tags[i]); + status = notmuch_query_add_tag_exclude (ctx->query, + notmuch_config_values_get (exclude_tags)); if (status && status != NOTMUCH_STATUS_IGNORED) { print_status_query ("notmuch search", ctx->query, status); return EXIT_FAILURE; } } - notmuch_query_set_omit_excluded (ctx->query, ctx->exclude); } @@ -805,7 +794,7 @@ static const notmuch_opt_desc_t common_options[] = { }; int -notmuch_search_command (notmuch_config_t *config, int argc, char *argv[]) +notmuch_search_command (notmuch_database_t *notmuch, int argc, char *argv[]) { search_context_t *ctx = &search_context; int opt_index, ret; @@ -832,6 +821,7 @@ notmuch_search_command (notmuch_config_t *config, int argc, char *argv[]) { } }; + ctx->notmuch = notmuch; ctx->output = OUTPUT_SUMMARY; opt_index = parse_arguments (argc, argv, options, 1); if (opt_index < 0) @@ -841,12 +831,12 @@ notmuch_search_command (notmuch_config_t *config, int argc, char *argv[]) if (ctx->output != OUTPUT_FILES && ctx->output != OUTPUT_MESSAGES && ctx->dupe != -1) { - fprintf (stderr, "Error: --duplicate=N is only supported with --output=files and --output=messages.\n"); + fprintf (stderr, + "Error: --duplicate=N is only supported with --output=files and --output=messages.\n"); return EXIT_FAILURE; } - if (_notmuch_search_prepare (ctx, config, - argc - opt_index, argv + opt_index)) + if (_notmuch_search_prepare (ctx, argc - opt_index, argv + opt_index)) return EXIT_FAILURE; switch (ctx->output) { @@ -871,7 +861,7 @@ notmuch_search_command (notmuch_config_t *config, int argc, char *argv[]) } int -notmuch_address_command (notmuch_config_t *config, int argc, char *argv[]) +notmuch_address_command (notmuch_database_t *notmuch, int argc, char *argv[]) { search_context_t *ctx = &search_context; int opt_index, ret; @@ -897,6 +887,8 @@ notmuch_address_command (notmuch_config_t *config, int argc, char *argv[]) { } }; + ctx->notmuch = notmuch; + opt_index = parse_arguments (argc, argv, options, 1); if (opt_index < 0) return EXIT_FAILURE; @@ -911,8 +903,7 @@ notmuch_address_command (notmuch_config_t *config, int argc, char *argv[]) return EXIT_FAILURE; } - if (_notmuch_search_prepare (ctx, config, - argc - opt_index, argv + opt_index)) + if (_notmuch_search_prepare (ctx, argc - opt_index, argv + opt_index)) return EXIT_FAILURE; ctx->addresses = g_hash_table_new_full (strcase_hash, strcase_equal, diff --git a/notmuch-setup.c b/notmuch-setup.c index cd1a52ff..ace56967 100644 --- a/notmuch-setup.c +++ b/notmuch-setup.c @@ -88,14 +88,17 @@ welcome_message_post_setup (void) } static void -print_tag_list (const char **tags, size_t tags_len) +print_tag_list (notmuch_config_values_t *tags) { - unsigned int i; + bool first = false; - for (i = 0; i < tags_len; i++) { - if (i != 0) + for (; + notmuch_config_values_valid (tags); + notmuch_config_values_move_to_next (tags)) { + if (! first) printf (" "); - printf ("%s", tags[i]); + first = false; + printf ("%s", notmuch_config_values_get (tags)); } } @@ -121,19 +124,14 @@ parse_tag_list (void *ctx, char *response) } int -notmuch_setup_command (notmuch_config_t *config, +notmuch_setup_command (notmuch_database_t *notmuch, int argc, char *argv[]) { char *response = NULL; size_t response_size = 0; - const char **old_other_emails; - size_t old_other_emails_len; GPtrArray *other_emails; - unsigned int i; - const char **new_tags; - size_t new_tags_len; - const char **search_exclude_tags; - size_t search_exclude_tags_len; + notmuch_config_values_t *new_tags, *search_exclude_tags, *emails; + notmuch_conffile_t *config; #define prompt(format, ...) \ do { \ @@ -153,29 +151,35 @@ notmuch_setup_command (notmuch_config_t *config, fprintf (stderr, "Warning: ignoring --uuid=%s\n", notmuch_requested_db_uuid); - if (notmuch_config_is_new (config)) + config = notmuch_conffile_open (notmuch, + notmuch_config_path (notmuch), true); + if (! config) + return EXIT_FAILURE; + + if (notmuch_conffile_is_new (config)) welcome_message_pre_setup (); - prompt ("Your full name [%s]: ", notmuch_config_get_user_name (config)); + prompt ("Your full name [%s]: ", notmuch_config_get (notmuch, NOTMUCH_CONFIG_USER_NAME)); if (strlen (response)) - notmuch_config_set_user_name (config, response); + notmuch_conffile_set_user_name (config, response); prompt ("Your primary email address [%s]: ", - notmuch_config_get_user_primary_email (config)); + notmuch_config_get (notmuch, NOTMUCH_CONFIG_PRIMARY_EMAIL)); if (strlen (response)) - notmuch_config_set_user_primary_email (config, response); + notmuch_conffile_set_user_primary_email (config, response); other_emails = g_ptr_array_new (); - old_other_emails = notmuch_config_get_user_other_email (config, - &old_other_emails_len); - for (i = 0; i < old_other_emails_len; i++) { - prompt ("Additional email address [%s]: ", old_other_emails[i]); + for (emails = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_OTHER_EMAIL); + notmuch_config_values_valid (emails); + notmuch_config_values_move_to_next (emails)) { + const char *email = notmuch_config_values_get (emails); + + prompt ("Additional email address [%s]: ", email); if (strlen (response)) g_ptr_array_add (other_emails, talloc_strdup (config, response)); else - g_ptr_array_add (other_emails, talloc_strdup (config, - old_other_emails[i])); + g_ptr_array_add (other_emails, talloc_strdup (config, email)); } do { @@ -184,57 +188,59 @@ notmuch_setup_command (notmuch_config_t *config, g_ptr_array_add (other_emails, talloc_strdup (config, response)); } while (strlen (response)); if (other_emails->len) - notmuch_config_set_user_other_email (config, - (const char **) - other_emails->pdata, - other_emails->len); + notmuch_conffile_set_user_other_email (config, + (const char **) + other_emails->pdata, + other_emails->len); g_ptr_array_free (other_emails, true); prompt ("Top-level directory of your email archive [%s]: ", - notmuch_config_get_database_path (config)); + notmuch_config_get (notmuch, NOTMUCH_CONFIG_DATABASE_PATH)); if (strlen (response)) { const char *absolute_path; absolute_path = make_path_absolute (config, response); - notmuch_config_set_database_path (config, absolute_path); + notmuch_conffile_set_database_path (config, absolute_path); } - new_tags = notmuch_config_get_new_tags (config, &new_tags_len); + new_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_NEW_TAGS); printf ("Tags to apply to all new messages (separated by spaces) ["); - print_tag_list (new_tags, new_tags_len); + print_tag_list (new_tags); prompt ("]: "); if (strlen (response)) { GPtrArray *tags = parse_tag_list (config, response); - notmuch_config_set_new_tags (config, (const char **) tags->pdata, - tags->len); + notmuch_conffile_set_new_tags (config, (const char **) tags->pdata, + tags->len); g_ptr_array_free (tags, true); } - - search_exclude_tags = notmuch_config_get_search_exclude_tags (config, &search_exclude_tags_len); + search_exclude_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_EXCLUDE_TAGS); printf ("Tags to exclude when searching messages (separated by spaces) ["); - print_tag_list (search_exclude_tags, search_exclude_tags_len); + print_tag_list (search_exclude_tags); prompt ("]: "); if (strlen (response)) { GPtrArray *tags = parse_tag_list (config, response); - notmuch_config_set_search_exclude_tags (config, - (const char **) tags->pdata, - tags->len); + notmuch_conffile_set_search_exclude_tags (config, + (const char **) tags->pdata, + tags->len); g_ptr_array_free (tags, true); } - if (notmuch_config_save (config)) + if (notmuch_conffile_save (config)) return EXIT_FAILURE; - if (notmuch_config_is_new (config)) + if (config) + notmuch_conffile_close (config); + + if (notmuch_conffile_is_new (config)) welcome_message_post_setup (); return EXIT_SUCCESS; diff --git a/notmuch-show.c b/notmuch-show.c index dd836add..bdb87321 100644 --- a/notmuch-show.c +++ b/notmuch-show.c @@ -80,14 +80,16 @@ _get_disposition (GMimeObject *meta) return g_mime_content_disposition_get_disposition (disposition); } -static bool _get_message_flag (notmuch_message_t *message, notmuch_message_flag_t flag) { +static bool +_get_message_flag (notmuch_message_t *message, notmuch_message_flag_t flag) +{ notmuch_bool_t is_set; notmuch_status_t status; status = notmuch_message_get_flag_st (message, flag, &is_set); if (print_status_message ("notmuch show", message, status)) - INTERNAL_ERROR("unexpected error getting message flag\n"); + INTERNAL_ERROR ("unexpected error getting message flag\n"); return is_set; } @@ -438,6 +440,7 @@ format_part_sigstatus_sprinter (sprinter_t *sp, GMimeSignatureList *siglist) } int i; + for (i = 0; i < g_mime_signature_list_length (siglist); i++) { GMimeSignature *signature = g_mime_signature_list_get_signature (siglist, i); @@ -668,7 +671,8 @@ format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node, sp->map_key (sp, "decrypted"); sp->begin_map (sp); sp->map_key (sp, "status"); - sp->string (sp, msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL ? "full" : "partial"); + sp->string (sp, msg_crypto->decryption_status == NOTMUCH_MESSAGE_DECRYPTED_FULL ? + "full" : "partial"); if (msg_crypto->payload_subject) { const char *subject = g_mime_message_get_subject GMIME_MESSAGE (node->part); @@ -909,7 +913,7 @@ format_part_raw (unused (const void *ctx), unused (sprinter_t *sp), } if (ssize > 0 && fwrite (buf, ssize, 1, stdout) != 1) { - fprintf (stderr, "Error: Write %ld chars to stdout failed\n", ssize); + fprintf (stderr, "Error: Write %zd chars to stdout failed\n", ssize); goto DONE; } } @@ -965,7 +969,8 @@ show_message (void *ctx, notmuch_status_t session_key_count_error = NOTMUCH_STATUS_SUCCESS; if (params->crypto.decrypt == NOTMUCH_DECRYPT_TRUE) - session_key_count_error = notmuch_message_count_properties (message, "session-key", &session_keys); + session_key_count_error = notmuch_message_count_properties (message, "session-key", + &session_keys); status = mime_node_open (local, message, &(params->crypto), &root); if (status) @@ -973,12 +978,15 @@ show_message (void *ctx, part = mime_node_seek_dfs (root, (params->part < 0 ? 0 : params->part)); if (part) status = format->part (local, sp, part, indent, params); - if (params->crypto.decrypt == NOTMUCH_DECRYPT_TRUE && session_key_count_error == NOTMUCH_STATUS_SUCCESS) { + if (params->crypto.decrypt == NOTMUCH_DECRYPT_TRUE && session_key_count_error == + NOTMUCH_STATUS_SUCCESS) { unsigned int new_session_keys = 0; - if (notmuch_message_count_properties (message, "session-key", &new_session_keys) == NOTMUCH_STATUS_SUCCESS && + if (notmuch_message_count_properties (message, "session-key", &new_session_keys) == + NOTMUCH_STATUS_SUCCESS && new_session_keys > session_keys) { /* try a quiet re-indexing */ - notmuch_indexopts_t *indexopts = notmuch_database_get_default_indexopts (notmuch_message_get_database (message)); + notmuch_indexopts_t *indexopts = notmuch_database_get_default_indexopts ( + notmuch_message_get_database (message)); if (indexopts) { notmuch_indexopts_set_decrypt_policy (indexopts, NOTMUCH_DECRYPT_AUTO); print_status_message ("Error re-indexing message with --decrypt=stash", @@ -1064,7 +1072,9 @@ do_show_single (void *ctx, return 1; if (count != 1) { - fprintf (stderr, "Error: search term did not match precisely one message (matched %u messages).\n", count); + fprintf (stderr, + "Error: search term did not match precisely one message (matched %u messages).\n", + count); return 1; } @@ -1157,9 +1167,9 @@ do_show_unthreaded (void *ctx, notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, TRUE); excluded = _get_message_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED); - if (!excluded || !params->omit_excluded) { + if (! excluded || ! params->omit_excluded) { status = show_message (ctx, format, sp, message, 0, params); - if (status && !res) + if (status && ! res) res = status; } else { sp->null (sp); @@ -1215,9 +1225,8 @@ static const notmuch_show_format_t *formatters[] = { }; int -notmuch_show_command (notmuch_config_t *config, int argc, char *argv[]) +notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[]) { - notmuch_database_t *notmuch; notmuch_query_t *query; char *query_string; int opt_index, ret; @@ -1234,6 +1243,7 @@ notmuch_show_command (notmuch_config_t *config, int argc, char *argv[]) bool entire_thread_set = false; bool single_message; bool unthreaded = FALSE; + notmuch_status_t status; notmuch_opt_desc_t options[] = { { .opt_keyword = &format, .name = "format", .keywords = @@ -1320,10 +1330,22 @@ notmuch_show_command (notmuch_config_t *config, int argc, char *argv[]) (format != NOTMUCH_FORMAT_TEXT && format != NOTMUCH_FORMAT_JSON && format != NOTMUCH_FORMAT_SEXP)) { - fprintf (stderr, "Warning: --include-html only implemented for format=text, format=json and format=sexp\n"); + fprintf (stderr, + "Warning: --include-html only implemented for format=text, format=json and format=sexp\n"); + } + + if (params.crypto.decrypt == NOTMUCH_DECRYPT_TRUE) { + status = notmuch_database_reopen (notmuch, NOTMUCH_DATABASE_MODE_READ_WRITE); + if (status) { + fprintf (stderr, "Error reopening database for READ_WRITE: %s\n", + notmuch_status_to_string (status)); + return EXIT_FAILURE; + } } - query_string = query_string_from_args (config, argc - opt_index, argv + opt_index); + notmuch_exit_if_unmatched_db_uuid (notmuch); + + query_string = query_string_from_args (notmuch, argc - opt_index, argv + opt_index); if (query_string == NULL) { fprintf (stderr, "Out of memory\n"); return EXIT_FAILURE; @@ -1334,15 +1356,6 @@ notmuch_show_command (notmuch_config_t *config, int argc, char *argv[]) return EXIT_FAILURE; } - notmuch_database_mode_t mode = NOTMUCH_DATABASE_MODE_READ_ONLY; - if (params.crypto.decrypt == NOTMUCH_DECRYPT_TRUE) - mode = NOTMUCH_DATABASE_MODE_READ_WRITE; - if (notmuch_database_open (notmuch_config_get_database_path (config), - mode, ¬much)) - return EXIT_FAILURE; - - notmuch_exit_if_unmatched_db_uuid (notmuch); - query = notmuch_query_create (notmuch, query_string); if (query == NULL) { fprintf (stderr, "Out of memory\n"); @@ -1351,27 +1364,26 @@ notmuch_show_command (notmuch_config_t *config, int argc, char *argv[]) /* Create structure printer. */ formatter = formatters[format]; - sprinter = formatter->new_sprinter (config, stdout); + sprinter = formatter->new_sprinter (notmuch, stdout); params.out_stream = g_mime_stream_stdout_new (); /* If a single message is requested we do not use search_excludes. */ if (single_message) { - ret = do_show_single (config, query, formatter, sprinter, ¶ms); + ret = do_show_single (notmuch, query, formatter, sprinter, ¶ms); } else { /* We always apply set the exclude flag. The * exclude=true|false option controls whether or not we return * threads that only match in an excluded message */ - const char **search_exclude_tags; - size_t search_exclude_tags_length; - unsigned int i; + notmuch_config_values_t *exclude_tags; notmuch_status_t status; - search_exclude_tags = notmuch_config_get_search_exclude_tags - (config, &search_exclude_tags_length); + for (exclude_tags = notmuch_config_get_values (notmuch, NOTMUCH_CONFIG_EXCLUDE_TAGS); + notmuch_config_values_valid (exclude_tags); + notmuch_config_values_move_to_next (exclude_tags)) { - for (i = 0; i < search_exclude_tags_length; i++) { - status = notmuch_query_add_tag_exclude (query, search_exclude_tags[i]); + status = notmuch_query_add_tag_exclude (query, + notmuch_config_values_get (exclude_tags)); if (status && status != NOTMUCH_STATUS_IGNORED) { print_status_query ("notmuch show", query, status); ret = -1; @@ -1385,9 +1397,9 @@ notmuch_show_command (notmuch_config_t *config, int argc, char *argv[]) } if (unthreaded) - ret = do_show_unthreaded (config, query, formatter, sprinter, ¶ms); + ret = do_show_unthreaded (notmuch, query, formatter, sprinter, ¶ms); else - ret = do_show_threaded (config, query, formatter, sprinter, ¶ms); + ret = do_show_threaded (notmuch, query, formatter, sprinter, ¶ms); } DONE: diff --git a/notmuch-tag.c b/notmuch-tag.c index 05b1837d..667a75d6 100644 --- a/notmuch-tag.c +++ b/notmuch-tag.c @@ -187,11 +187,10 @@ tag_file (void *ctx, notmuch_database_t *notmuch, tag_op_flag_t flags, } int -notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[]) +notmuch_tag_command (notmuch_database_t *notmuch, int argc, char *argv[]) { tag_op_list_t *tag_ops = NULL; char *query_string = NULL; - notmuch_database_t *notmuch; struct sigaction action; tag_op_flag_t tag_flags = TAG_FLAG_NONE; bool batch = false; @@ -200,6 +199,7 @@ notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[]) const char *input_file_name = NULL; int opt_index; int ret; + notmuch_bool_t synchronize_flags; /* Set up our handler for SIGINT */ memset (&action, 0, sizeof (struct sigaction)); @@ -240,13 +240,13 @@ notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[]) return EXIT_FAILURE; } } else { - tag_ops = tag_op_list_create (config); + tag_ops = tag_op_list_create (notmuch); if (tag_ops == NULL) { fprintf (stderr, "Out of memory.\n"); return EXIT_FAILURE; } - if (parse_tag_command_line (config, argc - opt_index, argv + opt_index, + if (parse_tag_command_line (notmuch, argc - opt_index, argv + opt_index, &query_string, tag_ops)) return EXIT_FAILURE; @@ -261,22 +261,25 @@ notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[]) } } - if (notmuch_database_open (notmuch_config_get_database_path (config), - NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much)) - return EXIT_FAILURE; - notmuch_exit_if_unmatched_db_uuid (notmuch); - if (notmuch_config_get_maildir_synchronize_flags (config)) + if (print_status_database ( + "notmuch restore", + notmuch, + notmuch_config_get_bool (notmuch, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS, + &synchronize_flags))) + return EXIT_FAILURE; + + if (synchronize_flags) tag_flags |= TAG_FLAG_MAILDIR_SYNC; if (remove_all) tag_flags |= TAG_FLAG_REMOVE_ALL; if (batch) - ret = tag_file (config, notmuch, tag_flags, input); + ret = tag_file (notmuch, notmuch, tag_flags, input); else - ret = tag_query (config, notmuch, query_string, tag_ops, tag_flags); + ret = tag_query (notmuch, notmuch, query_string, tag_ops, tag_flags); notmuch_database_destroy (notmuch); diff --git a/notmuch-time.c b/notmuch-time.c index cc7ffc23..cd45818b 100644 --- a/notmuch-time.c +++ b/notmuch-time.c @@ -39,8 +39,8 @@ * */ #define MINUTE (60) -#define HOUR (60 *MINUTE) -#define DAY (24 *HOUR) +#define HOUR (60 * MINUTE) +#define DAY (24 * HOUR) #define RELATIVE_DATE_MAX 20 const char * notmuch_time_relative_date (const void *ctx, time_t then) diff --git a/notmuch.c b/notmuch.c index 4ef1484f..2429999c 100644 --- a/notmuch.c +++ b/notmuch.c @@ -27,21 +27,24 @@ * * The return value will be used as notmuch exit status code, * preferably EXIT_SUCCESS or EXIT_FAILURE. + * + * Each subcommand should be passed either a config object, or an open + * database */ -typedef int (*command_function_t) (notmuch_config_t *config, int argc, char *argv[]); +typedef int (*command_function_t) (notmuch_database_t *notmuch, int argc, char *argv[]); typedef struct command { const char *name; command_function_t function; - notmuch_config_mode_t config_mode; + notmuch_command_mode_t mode; const char *summary; } command_t; static int -notmuch_help_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_help_command (notmuch_database_t *notmuch, int argc, char *argv[]); static int -notmuch_command (notmuch_config_t *config, int argc, char *argv[]); +notmuch_command (notmuch_database_t *notmuch, int argc, char *argv[]); static int _help_for (const char *topic); @@ -122,7 +125,8 @@ notmuch_process_shared_indexing_options (notmuch_database_t *notmuch) notmuch_status_t status; if (indexing_cli_choices.opts == NULL) return NOTMUCH_STATUS_OUT_OF_MEMORY; - status = notmuch_indexopts_set_decrypt_policy (indexing_cli_choices.opts, indexing_cli_choices.decrypt_policy); + status = notmuch_indexopts_set_decrypt_policy (indexing_cli_choices.opts, + indexing_cli_choices.decrypt_policy); if (status != NOTMUCH_STATUS_SUCCESS) { fprintf (stderr, "Error: Failed to set index decryption policy to %d. (%s)\n", indexing_cli_choices.decrypt_policy, notmuch_status_to_string (status)); @@ -136,41 +140,47 @@ notmuch_process_shared_indexing_options (notmuch_database_t *notmuch) static command_t commands[] = { - { NULL, notmuch_command, NOTMUCH_CONFIG_OPEN | NOTMUCH_CONFIG_CREATE, + { NULL, notmuch_command, NOTMUCH_COMMAND_CONFIG_CREATE | NOTMUCH_COMMAND_CONFIG_LOAD, "Notmuch main command." }, - { "setup", notmuch_setup_command, NOTMUCH_CONFIG_OPEN | NOTMUCH_CONFIG_CREATE, + { "setup", notmuch_setup_command, NOTMUCH_COMMAND_CONFIG_CREATE | NOTMUCH_COMMAND_CONFIG_LOAD, "Interactively set up notmuch for first use." }, - { "new", notmuch_new_command, NOTMUCH_CONFIG_OPEN, + { "new", notmuch_new_command, + NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE | + NOTMUCH_COMMAND_DATABASE_CREATE, "Find and import new messages to the notmuch database." }, - { "insert", notmuch_insert_command, NOTMUCH_CONFIG_OPEN, + { "insert", notmuch_insert_command, NOTMUCH_COMMAND_DATABASE_EARLY | + NOTMUCH_COMMAND_DATABASE_WRITE, "Add a new message into the maildir and notmuch database." }, - { "search", notmuch_search_command, NOTMUCH_CONFIG_OPEN, + { "search", notmuch_search_command, NOTMUCH_COMMAND_DATABASE_EARLY, "Search for messages matching the given search terms." }, - { "address", notmuch_address_command, NOTMUCH_CONFIG_OPEN, + { "address", notmuch_address_command, NOTMUCH_COMMAND_DATABASE_EARLY, "Get addresses from messages matching the given search terms." }, - { "show", notmuch_show_command, NOTMUCH_CONFIG_OPEN, + { "show", notmuch_show_command, NOTMUCH_COMMAND_DATABASE_EARLY, "Show all messages matching the search terms." }, - { "count", notmuch_count_command, NOTMUCH_CONFIG_OPEN, + { "count", notmuch_count_command, NOTMUCH_COMMAND_DATABASE_EARLY, "Count messages matching the search terms." }, - { "reply", notmuch_reply_command, NOTMUCH_CONFIG_OPEN, + { "reply", notmuch_reply_command, NOTMUCH_COMMAND_DATABASE_EARLY, "Construct a reply template for a set of messages." }, - { "tag", notmuch_tag_command, NOTMUCH_CONFIG_OPEN, + { "tag", notmuch_tag_command, NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE, "Add/remove tags for all messages matching the search terms." }, - { "dump", notmuch_dump_command, NOTMUCH_CONFIG_OPEN, + { "dump", notmuch_dump_command, NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE, "Create a plain-text dump of the tags for each message." }, - { "restore", notmuch_restore_command, NOTMUCH_CONFIG_OPEN, + { "restore", notmuch_restore_command, NOTMUCH_COMMAND_DATABASE_EARLY | + NOTMUCH_COMMAND_DATABASE_WRITE, "Restore the tags from the given dump file (see 'dump')." }, - { "compact", notmuch_compact_command, NOTMUCH_CONFIG_OPEN, + { "compact", notmuch_compact_command, NOTMUCH_COMMAND_DATABASE_EARLY | + NOTMUCH_COMMAND_DATABASE_WRITE, "Compact the notmuch database." }, - { "reindex", notmuch_reindex_command, NOTMUCH_CONFIG_OPEN, + { "reindex", notmuch_reindex_command, NOTMUCH_COMMAND_DATABASE_EARLY | + NOTMUCH_COMMAND_DATABASE_WRITE, "Re-index all messages matching the search terms." }, - { "config", notmuch_config_command, NOTMUCH_CONFIG_OPEN, + { "config", notmuch_config_command, NOTMUCH_COMMAND_CONFIG_LOAD, "Get or set settings in the notmuch configuration file." }, #if WITH_EMACS { "emacs-mua", NULL, 0, "send mail with notmuch and emacs." }, #endif - { "help", notmuch_help_command, NOTMUCH_CONFIG_CREATE, /* create but don't save config */ + { "help", notmuch_help_command, NOTMUCH_COMMAND_CONFIG_CREATE, /* create but don't save config */ "This message, or more detailed help for the named command." } }; @@ -244,14 +254,16 @@ void notmuch_exit_if_unsupported_format (void) { if (notmuch_format_version > NOTMUCH_FORMAT_CUR) { - fprintf (stderr, "\ + fprintf (stderr, + "\ A caller requested output format version %d, but the installed notmuch\n\ CLI only supports up to format version %d. You may need to upgrade your\n\ notmuch CLI.\n", notmuch_format_version, NOTMUCH_FORMAT_CUR); exit (NOTMUCH_EXIT_FORMAT_TOO_NEW); } else if (notmuch_format_version < NOTMUCH_FORMAT_MIN) { - fprintf (stderr, "\ + fprintf (stderr, + "\ A caller requested output format version %d, which is no longer supported\n\ by the notmuch CLI (it requires at least version %d). You may need to\n\ upgrade your notmuch front-end.\n", @@ -261,7 +273,8 @@ upgrade your notmuch front-end.\n", /* Warn about old version requests so compatibility issues are * less likely when we drop support for a deprecated format * versions. */ - fprintf (stderr, "\ + fprintf (stderr, + "\ A caller requested deprecated output format version %d, which may not\n\ be supported in the future.\n", notmuch_format_version); } @@ -335,7 +348,7 @@ _help_for (const char *topic_name) } static int -notmuch_help_command (unused (notmuch_config_t *config), int argc, char *argv[]) +notmuch_help_command (unused(notmuch_database_t *notmuch), int argc, char *argv[]) { int opt_index; @@ -359,35 +372,24 @@ notmuch_help_command (unused (notmuch_config_t *config), int argc, char *argv[]) * to be more clever about this in the future. */ static int -notmuch_command (notmuch_config_t *config, +notmuch_command (notmuch_database_t *notmuch, unused(int argc), unused(char **argv)) { - char *db_path; - struct stat st; - /* If the user has never configured notmuch, then run + const char *config_path; + + /* If the user has not created a configuration file, then run * notmuch_setup_command which will give a nice welcome message, * and interactively guide the user through the configuration. */ - if (notmuch_config_is_new (config)) - return notmuch_setup_command (config, 0, NULL); - - /* Notmuch is already configured, but is there a database? */ - db_path = talloc_asprintf (config, "%s/%s", - notmuch_config_get_database_path (config), - ".notmuch"); - if (stat (db_path, &st)) { + config_path = notmuch_config_path (notmuch); + if (access (config_path, R_OK | F_OK) == -1) { if (errno != ENOENT) { - fprintf (stderr, "Error looking for notmuch database at %s: %s\n", - db_path, strerror (errno)); + fprintf (stderr, "Error: %s config file access failed: %s\n", config_path, + strerror (errno)); return EXIT_FAILURE; + } else { + return notmuch_setup_command (notmuch, 0, NULL); } - printf ("Notmuch is configured, but there's not yet a database at\n\n\t%s\n\n", - db_path); - printf ("You probably want to run \"notmuch new\" now to create that database.\n\n" - "Note that the first run of \"notmuch new\" can take a very long time\n" - "and that the resulting database will use roughly the same amount of\n" - "storage space as the email being indexed.\n\n"); - return EXIT_SUCCESS; } printf ("Notmuch is configured and appears to have a database. Excellent!\n\n" @@ -404,8 +406,8 @@ notmuch_command (notmuch_config_t *config, "or any other interface described at https://notmuchmail.org\n\n" "And don't forget to run \"notmuch new\" whenever new mail arrives.\n\n" "Have fun, and may your inbox never have much mail.\n\n", - notmuch_config_get_user_name (config), - notmuch_config_get_user_primary_email (config)); + notmuch_config_get (notmuch, NOTMUCH_CONFIG_USER_NAME), + notmuch_config_get (notmuch, NOTMUCH_CONFIG_PRIMARY_EMAIL)); return EXIT_SUCCESS; } @@ -452,12 +454,12 @@ main (int argc, char *argv[]) const char *command_name = NULL; command_t *command; const char *config_file_name = NULL; - notmuch_config_t *config = NULL; + notmuch_database_t *notmuch = NULL; int opt_index; - int ret; + int ret = EXIT_SUCCESS; notmuch_opt_desc_t options[] = { - { .opt_string = &config_file_name, .name = "config" }, + { .opt_string = &config_file_name, .name = "config", .allow_empty = TRUE }, { .opt_inherit = notmuch_shared_options }, { } }; @@ -496,18 +498,96 @@ main (int argc, char *argv[]) goto DONE; } - config = notmuch_config_open (local, config_file_name, command->config_mode); - if (! config) { - ret = EXIT_FAILURE; - goto DONE; + if (command->mode & NOTMUCH_COMMAND_DATABASE_EARLY) { + char *status_string = NULL; + notmuch_database_mode_t mode; + notmuch_status_t status; + + if (command->mode & NOTMUCH_COMMAND_DATABASE_WRITE || + command->mode & NOTMUCH_COMMAND_DATABASE_CREATE) + mode = NOTMUCH_DATABASE_MODE_READ_WRITE; + else + mode = NOTMUCH_DATABASE_MODE_READ_ONLY; + + if (command->mode & NOTMUCH_COMMAND_DATABASE_CREATE) { + status = notmuch_database_create_with_config (NULL, + config_file_name, + NULL, + ¬much, + &status_string); + if (status && status != NOTMUCH_STATUS_DATABASE_EXISTS) { + if (status_string) { + fputs (status_string, stderr); + free (status_string); + } + + if (status == NOTMUCH_STATUS_NO_CONFIG) + fputs ("Try running 'notmuch setup' to create a configuration.", stderr); + + return EXIT_FAILURE; + } + } + + if (notmuch == NULL) { + status = notmuch_database_open_with_config (NULL, + mode, + config_file_name, + NULL, + ¬much, + &status_string); + if (status) { + if (status_string) { + fputs (status_string, stderr); + free (status_string); + } + + return EXIT_FAILURE; + } + } } - ret = (command->function)(config, argc - opt_index, argv + opt_index); + if (command->mode & NOTMUCH_COMMAND_CONFIG_LOAD) { + char *status_string = NULL; + notmuch_status_t status; + status = notmuch_database_load_config (NULL, + config_file_name, + NULL, + ¬much, + &status_string); + + if (status == NOTMUCH_STATUS_NO_CONFIG && ! (command->mode & NOTMUCH_COMMAND_CONFIG_CREATE)) { + fputs ("Try running 'notmuch setup' to create a configuration.", stderr); + goto DONE; + } + switch (status) { + case NOTMUCH_STATUS_NO_CONFIG: + if (! (command->mode & NOTMUCH_COMMAND_CONFIG_CREATE)) { + fputs ("Try running 'notmuch setup' to create a configuration.", stderr); + goto DONE; + } + break; + case NOTMUCH_STATUS_NO_DATABASE: + if (! command_name) { + printf ("Notmuch is configured, but no database was found.\n"); + printf ("You probably want to run \"notmuch new\" now to create a database.\n\n" + "Note that the first run of \"notmuch new\" can take a very long time\n" + "and that the resulting database will use roughly the same amount of\n" + "storage space as the email being indexed.\n\n"); + status = NOTMUCH_STATUS_SUCCESS; + goto DONE; + } + break; + case NOTMUCH_STATUS_SUCCESS: + break; + default: + goto DONE; + } - DONE: - if (config) - notmuch_config_close (config); + } + ret = (command->function)(notmuch, argc - opt_index, argv + opt_index); + + DONE: talloc_report = getenv ("NOTMUCH_TALLOC_REPORT"); if (talloc_report && strcmp (talloc_report, "") != 0) { /* this relies on the previous call to diff --git a/performance-test/T00-new.sh b/performance-test/T00-new.sh index a14dd13f..de260b2d 100755 --- a/performance-test/T00-new.sh +++ b/performance-test/T00-new.sh @@ -5,16 +5,16 @@ test_description='notmuch new' . $(dirname "$0")/perf-test-lib.sh || exit 1 uncache_database - time_start +manifest=$(mktemp manifestXXXXXX) +find mail -type f ! -path 'mail/.notmuch/*' | sed -n '1~4 p' > $manifest +xargs tar uf backup.tar < $manifest + for i in $(seq 2 6); do time_run "notmuch new #$i" 'notmuch new' done -manifest=$(mktemp manifestXXXXXX) - -find mail -type f ! -path 'mail/.notmuch/*' | sed -n '1~4 p' > $manifest # arithmetic context is to eat extra whitespace on e.g. some BSDs count=$((`wc -l < $manifest`)) @@ -26,6 +26,14 @@ perl -nle 'rename "$_.renamed", $_' $manifest time_run "new ($count mv back)" 'notmuch new' +perl -nle 'unlink $_; unlink $_.copy' $manifest + +time_run "new ($count rm)" 'notmuch new' + +tar xf backup.tar + +time_run "new ($count restore)" 'notmuch new' + perl -nle 'link $_, "$_.copy"' $manifest time_run "new ($count cp)" 'notmuch new' diff --git a/sprinter-json.c b/sprinter-json.c index 273bdeca..c7f4851c 100644 --- a/sprinter-json.c +++ b/sprinter-json.c @@ -129,7 +129,7 @@ json_integer (struct sprinter *sp, int64_t val) { struct sprinter_json *spj = json_begin_value (sp); - fprintf (spj->stream, "%"PRId64, val); + fprintf (spj->stream, "%" PRId64, val); } static void diff --git a/sprinter-sexp.c b/sprinter-sexp.c index 35c007d5..63b25428 100644 --- a/sprinter-sexp.c +++ b/sprinter-sexp.c @@ -166,7 +166,7 @@ sexp_integer (struct sprinter *sp, int64_t val) { struct sprinter_sexp *sps = sexp_begin_value (sp); - fprintf (sps->stream, "%"PRId64, val); + fprintf (sps->stream, "%" PRId64, val); } static void diff --git a/sprinter-text.c b/sprinter-text.c index 7b68f98c..c75ec5be 100644 --- a/sprinter-text.c +++ b/sprinter-text.c @@ -49,7 +49,7 @@ text_integer (struct sprinter *sp, int64_t val) { struct sprinter_text *sptxt = (struct sprinter_text *) sp; - fprintf (sptxt->stream, "%"PRId64, val); + fprintf (sptxt->stream, "%" PRId64, val); } static void diff --git a/test/README b/test/README index 11eaf18f..10f127cb 100644 --- a/test/README +++ b/test/README @@ -25,6 +25,7 @@ that you know if you break anything. - gdb(1) - gpg(1) - python(1) + - xapian-metadata(1) If your system lacks these tools or have older, non-upgradable versions of these, please (possibly compile and) install these to some other diff --git a/test/T020-compact.sh b/test/T020-compact.sh index 02f8738f..d77db00d 100755 --- a/test/T020-compact.sh +++ b/test/T020-compact.sh @@ -11,7 +11,7 @@ notmuch tag +tag2 subject:Two notmuch tag -tag1 +tag3 subject:Three test_begin_subtest "Running compact" -test_expect_success "notmuch compact --backup=${TEST_DIRECTORY}/xapian.old" +test_expect_success "notmuch compact --backup=${TMP_DIRECTORY}/xapian.old" test_begin_subtest "Compact preserves database" output=$(notmuch search \* | notmuch_search_sanitize) @@ -22,7 +22,7 @@ thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Three (inbox tag3 unread)" test_begin_subtest "Restoring Backup" test_expect_success 'rm -Rf ${MAIL_DIR}/.notmuch/xapian && - mv ${TEST_DIRECTORY}/xapian.old ${MAIL_DIR}/.notmuch/xapian' + mv ${TMP_DIRECTORY}/xapian.old ${MAIL_DIR}/.notmuch/xapian' test_begin_subtest "Checking restored backup" output=$(notmuch search \* | notmuch_search_sanitize) diff --git a/test/T030-config.sh b/test/T030-config.sh index 883541d5..f8897471 100755 --- a/test/T030-config.sh +++ b/test/T030-config.sh @@ -7,9 +7,12 @@ test_begin_subtest "Get string value" test_expect_equal "$(notmuch config get user.name)" "Notmuch Test Suite" test_begin_subtest "Get list value" -test_expect_equal "$(notmuch config get new.tags)" "\ +cat < EXPECTED +inbox unread -inbox" +EOF +notmuch config get new.tags | sort > OUTPUT +test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "Set string value" notmuch config set foo.string "this is a string value" @@ -43,25 +46,22 @@ notmuch config set foo.nonexistent test_expect_equal "$(notmuch config get foo.nonexistent)" "" test_begin_subtest "List all items" -notmuch config list > STDOUT 2> STDERR -printf "%s\n====\n%s\n" "$(< STDOUT)" "$(< STDERR)" | notmuch_config_sanitize > OUTPUT - +notmuch config list 2>&1 | notmuch_config_sanitize > OUTPUT cat < EXPECTED -database.path=MAIL_DIR -user.name=Notmuch Test Suite -user.primary_email=test_suite@notmuchmail.org -user.other_email=test_suite_other@notmuchmail.org;test_suite@otherdomain.org -new.tags=unread;inbox; -new.ignore= -search.exclude_tags= -maildir.synchronize_flags=true -foo.string=this is another string value -foo.list=this;is another;list value; built_with.compact=something built_with.field_processor=something built_with.retry_lock=something -==== -Error opening database at MAIL_DIR/.notmuch: No such file or directory +database.mail_root=MAIL_DIR +database.path=MAIL_DIR +foo.list=this;is another;list value; +foo.string=this is another string value +maildir.synchronize_flags=true +new.ignore= +new.tags=unread;inbox +search.exclude_tags= +user.name=Notmuch Test Suite +user.other_email=test_suite_other@notmuchmail.org;test_suite@otherdomain.org +user.primary_email=test_suite@notmuchmail.org EOF test_expect_equal_file EXPECTED OUTPUT @@ -76,7 +76,7 @@ test_expect_equal "$(notmuch --config:alt-config config get user.name)" \ "Another Name" test_begin_subtest "Top level --configFILE option" -test_expect_equal "$(notmuch --config alt-config config get user.name)" \ +test_expect_equal "$(notmuch --config alt-config config get user.name)" \ "Another Name" test_begin_subtest "Top level --config=FILE option changed the right file" @@ -96,14 +96,69 @@ test_expect_equal "$(notmuch --config=alt-config-link config get user.name)" \ test_begin_subtest "Writing config file through symlink follows symlink" test_expect_equal "$(readlink alt-config-link)" "alt-config" +test_begin_subtest "Round trip arbitrary key" +key=g${RANDOM}.m${RANDOM} +value=${RANDOM} +notmuch config set ${key} ${value} +output=$(notmuch config get ${key}) +test_expect_equal "${output}" "${value}" + +test_begin_subtest "Clear arbitrary key" +notmuch config set ${key} +output=$(notmuch config get ${key}) +test_expect_equal "${output}" "" + +db_path=${HOME}/database-path + test_begin_subtest "Absolute database path returned" notmuch config set database.path ${HOME}/Maildir test_expect_equal "$(notmuch config get database.path)" \ "${HOME}/Maildir" -test_begin_subtest "Relative database path properly expanded" +ln -s `pwd`/mail home/Maildir +add_email_corpus +test_begin_subtest "Relative database path expanded" notmuch config set database.path Maildir -test_expect_equal "$(notmuch config get database.path)" \ - "${HOME}/Maildir" +path=$(notmuch config get database.path | notmuch_dir_sanitize) +count=$(notmuch count '*') +test_expect_equal "${path} ${count}" \ + "CWD/home/Maildir 52" + +test_begin_subtest "Add config to database" +notmuch new +key=g${RANDOM}.m${RANDOM} +value=${RANDOM} +notmuch config set --database ${key} ${value} +notmuch dump --include=config > OUTPUT +cat < EXPECTED +#notmuch-dump batch-tag:3 config +#@ ${key} ${value} +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Roundtrip config to/from database" +notmuch new +key=g${RANDOM}.m${RANDOM} +value=${RANDOM} +notmuch config set --database ${key} ${value} +output=$(notmuch config get ${key}) +test_expect_equal "${output}" "${value}" + +test_begin_subtest "set built_with.* yields error" +test_expect_code 1 "notmuch config set built_with.compact false" + +test_begin_subtest "get built_with.{compact,field_processor} prints true" +for key in compact field_processor; do + notmuch config get built_with.${key} +done > OUTPUT +cat < EXPECTED +true +true +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "get built_with.nonexistent prints false" +output=$(notmuch config get built_with.nonexistent) +test_expect_equal "$output" "false" test_done diff --git a/test/T035-read-config.sh b/test/T035-read-config.sh new file mode 100755 index 00000000..ac0f420b --- /dev/null +++ b/test/T035-read-config.sh @@ -0,0 +1,476 @@ +#!/usr/bin/env bash +test_description='Various options for reading configuration' +. $(dirname "$0")/test-lib.sh || exit 1 + +backup_config () { + local test_name=$(basename $0 .sh) + cp ${NOTMUCH_CONFIG} notmuch-config-backup.${test_name} +} + +xdg_config () { + local dir + local profile=${1:-default} + if [[ $profile != default ]]; then + export NOTMUCH_PROFILE=$profile + fi + backup_config + dir="${HOME}/.config/notmuch/${profile}" + rm -rf $dir + mkdir -p $dir + CONFIG_PATH=$dir/config + mv ${NOTMUCH_CONFIG} ${CONFIG_PATH} + unset NOTMUCH_CONFIG +} + +restore_config () { + local test_name=$(basename $0 .sh) + export NOTMUCH_CONFIG="${TMP_DIRECTORY}/notmuch-config" + unset CONFIG_PATH + unset NOTMUCH_PROFILE + cp notmuch-config-backup.${test_name} ${NOTMUCH_CONFIG} +} + +add_email_corpus + +test_begin_subtest "count with saved query from config file" +backup_config +query_name="test${RANDOM}" +notmuch count query:$query_name > OUTPUT +printf "\n[query]\n${query_name} = tag:inbox\n" >> notmuch-config +notmuch count query:$query_name >> OUTPUT +cat < EXPECTED +0 +52 +EOF +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "count with saved query from config file (xdg)" +query_name="test${RANDOM}" +xdg_config +notmuch count query:$query_name > OUTPUT +printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH} +notmuch count query:$query_name >> OUTPUT +cat < EXPECTED +0 +52 +EOF +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "count with saved query from config file (xdg+profile)" +query_name="test${RANDOM}" +xdg_config work +notmuch count query:$query_name > OUTPUT +printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH} +notmuch count query:$query_name >> OUTPUT +cat < EXPECTED +0 +52 +EOF +restore_config +test_expect_equal_file EXPECTED OUTPUT + +cat < EXPECTED +Before: +#notmuch-dump batch-tag:3 tags + +After: +#notmuch-dump batch-tag:3 tags ++attachment +inbox +signed +unread -- id:20091118005829.GB25380@dottiness.seas.harvard.edu ++attachment +inbox +signed +unread -- id:20091118010116.GC25380@dottiness.seas.harvard.edu ++inbox +signed +unread -- id:20091117190054.GU3165@dottiness.seas.harvard.edu ++inbox +signed +unread -- id:20091117203301.GV3165@dottiness.seas.harvard.edu ++inbox +signed +unread -- id:20091118002059.067214ed@hikari ++inbox +signed +unread -- id:20091118005040.GA25380@dottiness.seas.harvard.edu ++inbox +signed +unread -- id:87iqd9rn3l.fsf@vertex.dottedmag +EOF + +test_begin_subtest "dump with saved query from config file" +backup_config +query_name="test${RANDOM}" +CONFIG_PATH=notmuch-config +printf "Before:\n" > OUTPUT +notmuch dump --include=tags query:$query_name | sort >> OUTPUT +printf "\nAfter:\n" >> OUTPUT +printf "\n[query]\n${query_name} = tag:signed\n" >> ${CONFIG_PATH} +notmuch dump --include=tags query:$query_name | sort >> OUTPUT +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "dump with saved query from config file (xdg)" +backup_config +query_name="test${RANDOM}" +xdg_config +printf "Before:\n" > OUTPUT +notmuch dump --include=tags query:$query_name | sort >> OUTPUT +printf "\nAfter:\n" >> OUTPUT +printf "\n[query]\n${query_name} = tag:signed\n" >> ${CONFIG_PATH} +notmuch dump --include=tags query:$query_name | sort >> OUTPUT +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "dump with saved query from config file (xdg+profile)" +backup_config +query_name="test${RANDOM}" +xdg_config work +printf "Before:\n" > OUTPUT +notmuch dump --include=tags query:$query_name | sort >> OUTPUT +printf "\nAfter:\n" >> OUTPUT +printf "\n[query]\n${query_name} = tag:signed\n" >> ${CONFIG_PATH} +notmuch dump --include=tags query:$query_name | sort >> OUTPUT +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "restore with xdg config" +backup_config +notmuch dump '*' > EXPECTED +notmuch tag -inbox '*' +xdg_config +notmuch restore --input=EXPECTED +notmuch dump > OUTPUT +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "restore with xdg+profile config" +backup_config +notmuch dump '*' > EXPECTED +notmuch tag -inbox '*' +xdg_config work +notmuch restore --input=EXPECTED +notmuch dump > OUTPUT +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Insert message with custom new.tags (xdg)" +backup_config +xdg_config +tag=test${RANDOM} +notmuch --config=${CONFIG_PATH} config set new.tags $tag +generate_message \ + "[subject]=\"insert-subject\"" \ + "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" \ + "[body]=\"insert-message\"" +mkdir -p ${MAIL_DIR}/{cur,new,tmp} +notmuch insert < "$gen_msg_filename" +notmuch dump id:$gen_msg_id > OUTPUT +cat < EXPECTED +#notmuch-dump batch-tag:3 config,properties,tags ++$tag -- id:$gen_msg_id +EOF +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Insert message with custom new.tags (xdg+profile)" +backup_config +tag=test${RANDOM} +xdg_config $tag +notmuch --config=${CONFIG_PATH} config set new.tags $tag +generate_message \ + "[subject]=\"insert-subject\"" \ + "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" \ + "[body]=\"insert-message\"" +mkdir -p ${MAIL_DIR}/{cur,new,tmp} +notmuch insert < "$gen_msg_filename" +notmuch dump id:$gen_msg_id > OUTPUT +cat < EXPECTED +#notmuch-dump batch-tag:3 config,properties,tags ++$tag -- id:$gen_msg_id +EOF +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "reindex with saved query from config file" +backup_config +query_name="test${RANDOM}" +count1=$(notmuch count --lastmod '*' | cut -f3) +printf "\n[query]\n${query_name} = tag:inbox\n" >> notmuch-config +notmuch reindex query:$query_name +count2=$(notmuch count --lastmod '*' | cut -f3) +restore_config +test_expect_success "test '$count2 -gt $count1'" + +test_begin_subtest "reindex with saved query from config file (xdg)" +query_name="test${RANDOM}" +count1=$(notmuch count --lastmod '*' | cut -f3) +xdg_config +printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH} +notmuch reindex query:$query_name +count2=$(notmuch count --lastmod '*' | cut -f3) +restore_config +test_expect_success "test '$count2 -gt $count1'" + +test_begin_subtest "reindex with saved query from config file (xdg+profile)" +query_name="test${RANDOM}" +count1=$(notmuch count --lastmod '*' | cut -f3) +xdg_config $query_name +printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH} +notmuch reindex query:$query_name +count2=$(notmuch count --lastmod '*' | cut -f3) +restore_config +test_expect_success "test '$count2 -gt $count1'" + + + +add_message '[from]="Sender "' \ + [to]=test_suite@notmuchmail.org \ + '[cc]="Other Parties "' \ + [subject]=notmuch-reply-test \ + '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \ + '[body]="reply with CC"' + +cat < EXPECTED +Before: +After: +From: Notmuch Test Suite +Subject: Re: notmuch-reply-test +To: Sender +Cc: Other Parties +In-Reply-To: <${gen_msg_id}> +References: <${gen_msg_id}> + +On Tue, 05 Jan 2010 15:43:56 -0000, Sender wrote: +> reply with CC +EOF + +test_begin_subtest "reply with saved query from config file" +backup_config +query_name="test${RANDOM}" +printf "Before:\n" > OUTPUT +notmuch reply query:$query_name 2>&1 >> OUTPUT +printf "\n[query]\n${query_name} = id:${gen_msg_id}\n" >> notmuch-config +printf "After:\n" >> OUTPUT +notmuch reply query:$query_name >> OUTPUT +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "reply with saved query from config file (xdg)" +backup_config +query_name="test${RANDOM}" +xdg_config +printf "Before:\n" > OUTPUT +notmuch reply query:$query_name 2>&1 >> OUTPUT +printf "\n[query]\n${query_name} = id:${gen_msg_id}\n" >> ${CONFIG_PATH} +printf "After:\n" >> OUTPUT +notmuch reply query:$query_name >> OUTPUT +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "reply with saved query from config file (xdg+profile)" +backup_config +query_name="test${RANDOM}" +xdg_config $query_name +printf "Before:\n" > OUTPUT +notmuch reply query:$query_name 2>&1 >> OUTPUT +printf "\n[query]\n${query_name} = id:${gen_msg_id}\n" >> ${CONFIG_PATH} +printf "After:\n" >> OUTPUT +notmuch reply query:$query_name >> OUTPUT +restore_config +test_expect_equal_file EXPECTED OUTPUT + +backup_database +test_begin_subtest "search with alternate config" +notmuch tag -- +foobar17 '*' +cp notmuch-config alt-config +notmuch --config=alt-config config set search.exclude_tags foobar17 +output=$(notmuch --config=alt-config count '*') +test_expect_equal "$output" "0" +restore_database + +cat < EXPECTED +Before: +After: +thread:XXX 2009-11-18 [1/2] Carl Worth| Alex Botero-Lowry; [notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) +thread:XXX 2009-11-18 [1/2] Carl Worth| Ingmar Vanhassel; [notmuch] [PATCH] Typsos (inbox unread) +thread:XXX 2009-11-18 [1/3] Carl Worth| Adrian Perez de Castro, Keith Packard; [notmuch] Introducing myself (inbox signed unread) +thread:XXX 2009-11-18 [1/3] Carl Worth| Israel Herraiz, Keith Packard; [notmuch] New to the list (inbox unread) +thread:XXX 2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea! (inbox unread) +thread:XXX 2009-11-18 [1/2] Carl Worth| Jan Janak; [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread) +thread:XXX 2009-11-18 [1/3(4)] Carl Worth| Aron Griffis, Keith Packard; [notmuch] archive (inbox unread) +thread:XXX 2009-11-18 [1/2] Carl Worth| Keith Packard; [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) +thread:XXX 2009-11-18 [1/7] Carl Worth| Lars Kellogg-Stedman, Mikhail Gusarov, Keith Packard; [notmuch] Working with Maildir storage? (inbox signed unread) +thread:XXX 2009-11-18 [2/5] Carl Worth| Mikhail Gusarov, Keith Packard; [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) +thread:XXX 2009-11-17 [1/2] Carl Worth| Alex Botero-Lowry; [notmuch] preliminary FreeBSD support (attachment inbox unread) +EOF + +test_begin_subtest "search with saved query from config file" +query_name="test${RANDOM}" +backup_config +printf "Before:\n" > OUTPUT +notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT +printf "\n[query]\n${query_name} = from:cworth\n" >> notmuch-config +printf "After:\n" >> OUTPUT +notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "search with saved query from config file (xdg)" +query_name="test${RANDOM}" +xdg_config +printf "Before:\n" > OUTPUT +notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT +printf "\n[query]\n${query_name} = from:cworth\n" >> ${CONFIG_PATH} +printf "After:\n" >> OUTPUT +notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "search with saved query from config file (xdg + profile)" +query_name="test${RANDOM}" +xdg_config $query_name +printf "Before:\n" > OUTPUT +notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT +printf "\n[query]\n${query_name} = from:cworth\n" >> ${CONFIG_PATH} +printf "After:\n" >> OUTPUT +notmuch search query:$query_name 2>&1 | notmuch_search_sanitize >> OUTPUT +restore_config +test_expect_equal_file EXPECTED OUTPUT + +cat < EXPECTED +Before: +After: +Alex Botero-Lowry +Alexander Botero-Lowry +François Boulogne +Jjgod Jiang +EOF + +test_begin_subtest "address: saved query from config file" +backup_config +query_name="test${RANDOM}" +printf "Before:\n" > OUTPUT +notmuch address --deduplicate=no --output=sender query:$query_name 2>&1 | sort >> OUTPUT +printf "\n[query]\n${query_name} = from:gmail.com\n" >> notmuch-config +printf "After:\n" >> OUTPUT +notmuch address --output=sender query:$query_name | sort >> OUTPUT +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "address: saved query from config file (xdg)" +query_name="test${RANDOM}" +xdg_config +printf "Before:\n" > OUTPUT +notmuch address --deduplicate=no --output=sender query:$query_name 2>&1 | sort >> OUTPUT +printf "\n[query]\n${query_name} = from:gmail.com\n" >> ${CONFIG_PATH} +printf "After:\n" >> OUTPUT +notmuch address --output=sender query:$query_name | sort >> OUTPUT +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "address: saved query from config file (xdg+profile)" +query_name="test${RANDOM}" +xdg_config $query_name +printf "Before:\n" > OUTPUT +notmuch address --deduplicate=no --output=sender query:$query_name 2>&1 | sort >> OUTPUT +printf "\n[query]\n${query_name} = from:gmail.com\n" >> ${CONFIG_PATH} +printf "After:\n" >> OUTPUT +notmuch address --output=sender query:$query_name | sort >> OUTPUT +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "show with alternate config" +backup_database +cp notmuch-config alt-config +notmuch --config=alt-config config set search.exclude_tags foobar17 +notmuch tag -- +foobar17 '*' +output=$(notmuch --config=alt-config show '*' && echo OK) +restore_database +test_expect_equal "$output" "OK" + +test_begin_subtest "show with alternate config (xdg)" +backup_database +notmuch tag -- +foobar17 '*' +xdg_config +notmuch --config=${CONFIG_PATH} config set search.exclude_tags foobar17 +output=$(notmuch show '*' && echo OK) +restore_database +restore_config +test_expect_equal "$output" "OK" + +test_begin_subtest "show with alternate config (xdg+profile)" +backup_database +notmuch tag -- +foobar17 '*' +xdg_config foobar17 +notmuch --config=${CONFIG_PATH} config set search.exclude_tags foobar17 +output=$(notmuch show '*' && echo OK) +restore_database +restore_config +test_expect_equal "$output" "OK" + +# reset to known state +add_email_corpus + +test_begin_subtest "tag with saved query from config file" +backup_config +query_name="test${RANDOM}" +tag_name="tag${RANDOM}" +notmuch count query:$query_name > OUTPUT +printf "\n[query]\n${query_name} = tag:inbox\n" >> notmuch-config +notmuch tag +$tag_name -- query:${query_name} +notmuch count tag:$tag_name >> OUTPUT +cat < EXPECTED +0 +52 +EOF +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "tag with saved query from config file (xdg)" +xdg_config +query_name="test${RANDOM}" +tag_name="tag${RANDOM}" +notmuch count query:$query_name > OUTPUT +printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH} +notmuch tag +$tag_name -- query:${query_name} +notmuch count tag:$tag_name >> OUTPUT +cat < EXPECTED +0 +52 +EOF +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "tag with saved query from config file (xdg+profile)" +query_name="test${RANDOM}" +xdg_config ${query_name} +tag_name="tag${RANDOM}" +notmuch count query:$query_name > OUTPUT +printf "\n[query]\n${query_name} = tag:inbox\n" >> ${CONFIG_PATH} +notmuch tag +$tag_name -- query:${query_name} +notmuch count tag:$tag_name >> OUTPUT +cat < EXPECTED +0 +52 +EOF +restore_config +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "running compact (xdg)" +xdg_config +notmuch compact +output=$(notmuch count '*') +restore_config +test_expect_equal "52" "$output" + +test_begin_subtest "running compact (xdg + profile)" +xdg_config ${RANDOM} +notmuch compact +output=$(notmuch count '*') +restore_config +test_expect_equal "52" "$output" + +test_begin_subtest "run notmuch-new (xdg)" +xdg_config +generate_message +output=$(NOTMUCH_NEW --debug) +restore_config +test_expect_equal "$output" "Added 1 new message to the database." + +test_begin_subtest "run notmuch-new (xdg + profile)" +xdg_config ${RANDOM} +generate_message +output=$(NOTMUCH_NEW --debug) +restore_config +test_expect_equal "$output" "Added 1 new message to the database." + +test_done diff --git a/test/T040-setup.sh b/test/T040-setup.sh index fbfe200a..42c621c8 100755 --- a/test/T040-setup.sh +++ b/test/T040-setup.sh @@ -6,11 +6,11 @@ test_description='"notmuch setup"' test_begin_subtest "Notmuch new without a config suggests notmuch setup" output=$(notmuch --config=new-notmuch-config new 2>&1) test_expect_equal "$output" "\ -Configuration file new-notmuch-config not found. +Error: cannot load config file. Try running 'notmuch setup' to create a configuration." test_begin_subtest "Create a new config interactively" -notmuch --config=new-notmuch-config > /dev/null < log.${test_count} <&1 | notmuch_dir_sanitize > OUTPUT +cat < EXPECTED +Notmuch is configured, but no database was found. +You probably want to run "notmuch new" now to create a database. + +Note that the first run of "notmuch new" can take a very long time +and that the resulting database will use roughly the same amount of +storage space as the email being indexed. + +EOF +test_expect_equal_file EXPECTED OUTPUT test_done diff --git a/test/T050-new.sh b/test/T050-new.sh index 009b2633..4beae379 100755 --- a/test/T050-new.sh +++ b/test/T050-new.sh @@ -324,10 +324,10 @@ test_expect_equal "$output" "" OLDCONFIG=$(notmuch config get new.tags) -test_begin_subtest "Empty tags in new.tags are forbidden" +test_begin_subtest "Empty tags in new.tags are ignored" notmuch config set new.tags "foo;;bar" -output=$(NOTMUCH_NEW --debug 2>&1) -test_expect_equal "$output" "Error: tag '' in new.tags: empty tag forbidden" +output=$(NOTMUCH_NEW --quiet 2>&1) +test_expect_equal "$output" "" test_begin_subtest "Tags starting with '-' in new.tags are forbidden" notmuch config set new.tags "-foo;bar" @@ -339,11 +339,25 @@ test_expect_code 1 "NOTMUCH_NEW --debug 2>&1" notmuch config set new.tags $OLDCONFIG +test_begin_subtest "Long directory names don't cause rescan" +test_subtest_known_broken +printf -v name 'z%.0s' {1..234} +generate_message [dir]=$name +NOTMUCH_NEW > OUTPUT +notmuch new >> OUTPUT +rm -r ${MAIL_DIR}/${name} +notmuch new >> OUTPUT +cat < EXPECTED +Added 1 new message to the database. +No new mail. +No new mail. Removed 1 message. +EOF +test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "Xapian exception: read only files" -chmod u-w ${MAIL_DIR}/.notmuch/xapian/*.* +chmod u-w ${MAIL_DIR}/.notmuch/xapian/*.* output=$(NOTMUCH_NEW --debug 2>&1 | sed 's/: .*$//' ) -chmod u+w ${MAIL_DIR}/.notmuch/xapian/*.* +chmod u+w ${MAIL_DIR}/.notmuch/xapian/*.* test_expect_equal "$output" "A Xapian exception occurred opening database" @@ -380,13 +394,37 @@ exit status: 75 EOF test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "Relative database path expanded in new" +ln -s "$PWD/mail" home/Maildir +notmuch config set database.path Maildir +generate_message +NOTMUCH_NEW > OUTPUT +cat <EXPECTED +Added 1 new message to the database. +EOF +notmuch config set database.path ${MAIL_DIR} +rm home/Maildir +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Relative mail root (in db) expanded in new" +ln -s "$PWD/mail" home/Maildir +notmuch config set --database database.mail_root Maildir +generate_message +NOTMUCH_NEW > OUTPUT +cat <EXPECTED +Added 1 new message to the database. +EOF +notmuch config set database.mail_root +rm home/Maildir +test_expect_equal_file EXPECTED OUTPUT + add_email_corpus broken test_begin_subtest "reference loop does not crash" test_expect_code 0 "notmuch show --format=json id:mid-loop-12@example.org id:mid-loop-21@example.org > OUTPUT" test_begin_subtest "reference loop ordered by date" -threadid=$(notmuch search --output=threads id:mid-loop-12@example.org) -notmuch show --format=mbox $threadid | grep '^Date' > OUTPUT +threadid=$(notmuch search --output=threads id:mid-loop-12@example.org) +notmuch show --format=mbox $threadid | grep '^Date' > OUTPUT cat < EXPECTED Date: Thu, 16 Jun 2016 22:14:41 -0400 Date: Fri, 17 Jun 2016 22:14:41 -0400 diff --git a/test/T055-path-config.sh b/test/T055-path-config.sh new file mode 100755 index 00000000..3e782f65 --- /dev/null +++ b/test/T055-path-config.sh @@ -0,0 +1,315 @@ +#!/usr/bin/env bash +test_description='Configuration of mail-root and database path' +. $(dirname "$0")/test-lib.sh || exit 1 + +test_require_external_prereq xapian-metdata + +backup_config () { + local test_name=$(basename $0 .sh) + cp ${NOTMUCH_CONFIG} notmuch-config-backup.${test_name} +} + +restore_config () { + local test_name=$(basename $0 .sh) + export NOTMUCH_CONFIG="${TMP_DIRECTORY}/notmuch-config" + unset CONFIG_PATH + unset DATABASE_PATH + unset NOTMUCH_PROFILE + unset XAPIAN_PATH + unset MAILDIR + rm -f "$HOME/mail" + cp notmuch-config-backup.${test_name} ${NOTMUCH_CONFIG} +} + +split_config () { + local dir + backup_config + dir="$TMP_DIRECTORY/database.$test_count" + rm -rf $dir + mkdir $dir + notmuch config set database.path $dir + notmuch config set database.mail_root $MAIL_DIR + DATABASE_PATH=$dir + XAPIAN_PATH="$dir/xapian" +} + +symlink_config () { + local dir + backup_config + dir="$TMP_DIRECTORY/link.$test_count" + ln -s $MAIL_DIR $dir + notmuch config set database.path $dir + notmuch config set database.mail_root $MAIL_DIR + XAPIAN_PATH="$MAIL_DIR/.notmuch/xapian" + unset DATABASE_PATH +} + + +home_mail_config () { + local dir + backup_config + dir="${HOME}/mail" + ln -s $MAIL_DIR $dir + notmuch config set database.path + notmuch config set database.mail_root + XAPIAN_PATH="$MAIL_DIR/.notmuch/xapian" + unset DATABASE_PATH +} + +maildir_env_config () { + local dir + backup_config + dir="${HOME}/env_points_here" + ln -s $MAIL_DIR $dir + export MAILDIR=$dir + notmuch config set database.path + notmuch config set database.mail_root + XAPIAN_PATH="${MAIL_DIR}/.notmuch/xapian" + unset DATABASE_PATH +} + +xdg_config () { + local dir + local profile=${1:-default} + + if [[ $profile != default ]]; then + export NOTMUCH_PROFILE=$profile + fi + + backup_config + DATABASE_PATH="${HOME}/.local/share/notmuch/${profile}" + rm -rf $DATABASE_PATH + mkdir -p $DATABASE_PATH + + config_dir="${HOME}/.config/notmuch/${profile}" + mkdir -p ${config_dir} + CONFIG_PATH=$config_dir/config + mv ${NOTMUCH_CONFIG} $CONFIG_PATH + unset NOTMUCH_CONFIG + + XAPIAN_PATH="${DATABASE_PATH}/xapian" + notmuch --config=${CONFIG_PATH} config set database.mail_root ${TMP_DIRECTORY}/mail + notmuch --config=${CONFIG_PATH} config set database.path +} + +for config in traditional split XDG XDG+profile symlink home_mail maildir_env; do + #start each set of tests with an known set of messages + add_email_corpus + + case $config in + traditional) + backup_config + XAPIAN_PATH="$MAIL_DIR/.notmuch/xapian" + ;; + split) + split_config + mv mail/.notmuch/xapian $DATABASE_PATH + ;; + XDG) + xdg_config + mv mail/.notmuch/xapian $DATABASE_PATH + ;; + XDG+profile) + xdg_config ${RANDOM} + mv mail/.notmuch/xapian $DATABASE_PATH + ;; + symlink) + symlink_config + ;; + home_mail) + home_mail_config + ;; + maildir_env) + maildir_env_config + ;; + esac + + test_begin_subtest "count ($config)" + output=$(notmuch count '*') + test_expect_equal "$output" '52' + + test_begin_subtest "count+tag ($config)" + tag="tag${RANDOM}" + notmuch tag +$tag '*' + output=$(notmuch count tag:$tag) + notmuch tag -$tag '*' + test_expect_equal "$output" '52' + + test_begin_subtest "address ($config)" + notmuch address --deduplicate=no --sort=newest-first --output=sender --output=recipients path:foo >OUTPUT + cat <EXPECTED +Carl Worth +notmuch@notmuchmail.org +EOF + test_expect_equal_file EXPECTED OUTPUT + + test_begin_subtest "dump ($config)" + notmuch dump is:attachment and is:signed | sort > OUTPUT + cat < EXPECTED +#notmuch-dump batch-tag:3 config,properties,tags ++attachment +inbox +signed +unread -- id:20091118005829.GB25380@dottiness.seas.harvard.edu ++attachment +inbox +signed +unread -- id:20091118010116.GC25380@dottiness.seas.harvard.edu +EOF + test_expect_equal_file EXPECTED OUTPUT + + test_begin_subtest "dump + tag + restore ($config)" + notmuch dump '*' > EXPECTED + notmuch tag -inbox '*' + notmuch restore < EXPECTED + notmuch dump > OUTPUT + test_expect_equal_file EXPECTED OUTPUT + + test_begin_subtest "reindex ($config)" + notmuch search --output=messages '*' > EXPECTED + notmuch reindex '*' + notmuch search --output=messages '*' > OUTPUT + test_expect_equal_file EXPECTED OUTPUT + + test_begin_subtest "use existing database ($config)" + output=$(notmuch new) + test_expect_equal "$output" 'No new mail.' + + test_begin_subtest "create database ($config)" + rm -rf $DATABASE_PATH/{.notmuch,}/xapian + notmuch new + output=$(notmuch count '*') + test_expect_equal "$output" '52' + + test_begin_subtest "detect new files ($config)" + generate_message + generate_message + notmuch new + output=$(notmuch count '*') + test_expect_equal "$output" '54' + + test_begin_subtest "Show a raw message ($config)" + add_message + notmuch show --format=raw id:$gen_msg_id > OUTPUT + test_expect_equal_file $gen_msg_filename OUTPUT + rm -f $gen_msg_filename + + test_begin_subtest "reply ($config)" + add_message '[from]="Sender "' \ + [to]=test_suite@notmuchmail.org \ + [subject]=notmuch-reply-test \ + '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \ + '[body]="basic reply test"' + notmuch reply id:${gen_msg_id} 2>&1 > OUTPUT + cat < EXPECTED +From: Notmuch Test Suite +Subject: Re: notmuch-reply-test +To: Sender +In-Reply-To: <${gen_msg_id}> +References: <${gen_msg_id}> + +On Tue, 05 Jan 2010 15:43:56 -0000, Sender wrote: +> basic reply test +EOF + test_expect_equal_file EXPECTED OUTPUT + + test_begin_subtest "insert+search ($config)" + generate_message \ + "[subject]=\"insert-subject\"" \ + "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" \ + "[body]=\"insert-message\"" + mkdir -p "$MAIL_DIR"/{cur,new,tmp} + notmuch insert < "$gen_msg_filename" + cur_msg_filename=$(notmuch search --output=files "subject:insert-subject") + test_expect_equal_file "$cur_msg_filename" "$gen_msg_filename" + + + test_begin_subtest "compact+search ($config)" + notmuch search --output=messages '*' | sort > EXPECTED + notmuch compact + notmuch search --output=messages '*' | sort > OUTPUT + test_expect_equal_file EXPECTED OUTPUT + + test_begin_subtest "upgrade backup ($config)" + features=$(xapian-metadata get $XAPIAN_PATH features | grep -v "^relative directory paths") + xapian-metadata set $XAPIAN_PATH features "$features" + output=$(notmuch new | grep Welcome) + test_expect_equal \ + "$output" \ + "Welcome to a new version of notmuch! Your database will now be upgraded." + + test_begin_subtest "notmuch +config -database suggests notmuch new ($config)" + mv "$XAPIAN_PATH" "${XAPIAN_PATH}.bak" + notmuch > OUTPUT +cat < EXPECTED +Notmuch is configured, but no database was found. +You probably want to run "notmuch new" now to create a database. + +Note that the first run of "notmuch new" can take a very long time +and that the resulting database will use roughly the same amount of +storage space as the email being indexed. + +EOF + mv "${XAPIAN_PATH}.bak" "$XAPIAN_PATH" + + test_expect_equal_file EXPECTED OUTPUT + + test_begin_subtest "Set config value ($config)" + name=${RANDOM} + value=${RANDOM} + notmuch config set test${test_count}.${name} ${value} + output=$(notmuch config get test${test_count}.${name}) + notmuch config set test${test_count}.${name} + output2=$(notmuch config get test${test_count}.${name}) + test_expect_equal "${output}+${output2}" "${value}+" + + test_begin_subtest "Set config value in database ($config)" + name=${RANDOM} + value=${RANDOM} + notmuch config set --database test${test_count}.${name} ${value} + output=$(notmuch config get test${test_count}.${name}) + notmuch config set --database test${test_count}.${name} + output2=$(notmuch config get test${test_count}.${name}) + test_expect_equal "${output}+${output2}" "${value}+" + + test_begin_subtest "Config list ($config)" + notmuch config list | notmuch_dir_sanitize | \ + sed -e "s/^database.backup_dir=.*$/database.backup_dir/" \ + -e "s/^database.hook_dir=.*$/database.hook_dir/" \ + -e "s/^database.path=.*$/database.path/" \ + -e "s,^database.mail_root=CWD/home/mail,database.mail_root=MAIL_DIR," \ + -e "s,^database.mail_root=CWD/home/env_points_here,database.mail_root=MAIL_DIR," \ + > OUTPUT + cat < EXPECTED +built_with.compact=true +built_with.field_processor=true +built_with.retry_lock=true +database.backup_dir +database.hook_dir +database.mail_root=MAIL_DIR +database.path +maildir.synchronize_flags=true +new.ignore= +new.tags=unread;inbox +search.exclude_tags= +user.name=Notmuch Test Suite +user.other_email=test_suite_other@notmuchmail.org;test_suite@otherdomain.org +user.primary_email=test_suite@notmuchmail.org +EOF + test_expect_equal_file EXPECTED OUTPUT + + case $config in + XDG*) + test_begin_subtest "Set shadowed config value in database ($config)" + name=${RANDOM} + value=${RANDOM} + key=test${test_count}.${name} + notmuch config set --database ${key} ${value} + notmuch config set ${key} shadow${value} + output=$(notmuch --config='' config get ${key}) + notmuch config set --database ${key} + output2=$(notmuch --config='' config get ${key}) + notmuch config set ${key} + test_expect_equal "${output}+${output2}" "${value}+" + ;; + esac + restore_config + rm -rf home/.local + rm -rf home/.config +done + +test_done diff --git a/test/T070-insert.sh b/test/T070-insert.sh index 1c7ca846..b37a9b67 100755 --- a/test/T070-insert.sh +++ b/test/T070-insert.sh @@ -226,11 +226,13 @@ test_expect_code 1 "notmuch insert --folder=../G --create-folder < $gen_msg_file OLDCONFIG=$(notmuch config get new.tags) -test_begin_subtest "Empty tags in new.tags are forbidden" +test_begin_subtest "Empty tags in new.tags are ignored" notmuch config set new.tags "foo;;bar" gen_insert_msg -output=$(notmuch insert < $gen_msg_filename 2>&1) -test_expect_equal "$output" "Error: tag '' in new.tags: empty tag forbidden" +notmuch insert < $gen_msg_filename +output=$(notmuch show --format=json id:$gen_msg_id) +test_json_nodes <<<"$output" \ + 'new_tags:[0][0][0]["tags"] = ["bar", "foo"]' test_begin_subtest "Tags starting with '-' in new.tags are forbidden" notmuch config set new.tags "-foo;bar" @@ -245,7 +247,7 @@ notmuch config set new.tags $OLDCONFIG # DUPLICATE_MESSAGE_ID is not tested here, because it should actually pass. # pregenerate all of the test shims -for code in FILE_NOT_EMAIL READ_ONLY_DATABASE UPGRADE_REQUIRED PATH_ERROR OUT_OF_MEMORY XAPIAN_EXCEPTION; do +for code in FILE_NOT_EMAIL READ_ONLY_DATABASE UPGRADE_REQUIRED PATH_ERROR OUT_OF_MEMORY XAPIAN_EXCEPTION; do make_shim shim-$code < #include @@ -262,7 +264,7 @@ done gen_insert_msg -for code in FILE_NOT_EMAIL READ_ONLY_DATABASE UPGRADE_REQUIRED PATH_ERROR; do +for code in FILE_NOT_EMAIL READ_ONLY_DATABASE UPGRADE_REQUIRED PATH_ERROR; do test_begin_subtest "EXIT_FAILURE when index_file returns $code" test_expect_code 1 "notmuch_with_shim shim-$code insert < \"$gen_msg_filename\"" diff --git a/test/T140-excludes.sh b/test/T140-excludes.sh index 0cf69975..acab5381 100755 --- a/test/T140-excludes.sh +++ b/test/T140-excludes.sh @@ -39,6 +39,16 @@ deleted_id=$gen_msg_id output=$(notmuch search subject:deleted | notmuch_search_sanitize) test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)" +test_begin_subtest "Search, exclude \"deleted\" messages; alternate config file" +cp ${NOTMUCH_CONFIG} alt-config +notmuch config set search.exclude_tags +notmuch --config=alt-config search subject:deleted | notmuch_search_sanitize > OUTPUT +cp alt-config ${NOTMUCH_CONFIG} +cat < EXPECTED +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "Search, exclude \"deleted\" messages from message search" output=$(notmuch search --output=messages subject:deleted | notmuch_search_sanitize) test_expect_equal "$output" "id:$not_deleted_id" @@ -276,6 +286,21 @@ test_begin_subtest "Count, default exclusion: tag in query (threads)" output=$(notmuch count --output=threads tag:test and tag:deleted) test_expect_equal "$output" "3" +test_begin_subtest "Count, default exclusion, batch" +notmuch count --batch --output=messages< OUTPUT +tag:test +tag:test and tag:deleted +tag:test +tag:test and tag:deleted +EOF +cat <EXPECTED +2 +4 +2 +4 +EOF +test_expect_equal_file EXPECTED OUTPUT + test_begin_subtest "Count, exclude=true: tag in query (messages)" output=$(notmuch count --exclude=true tag:test and tag:deleted) test_expect_equal "$output" "4" @@ -364,7 +389,7 @@ Subject: No messages excluded: single match: reply 4 Subject: No messages excluded: single match: reply 5" test_begin_subtest "Show, exclude=false" -output=$(notmuch show --exclude=false tag:test | notmuch_show_sanitize_all | egrep "Subject:|message{") +output=$(notmuch show --exclude=false tag:test | notmuch_show_sanitize_all | egrep "Subject:|message{") test_expect_equal "$output" " message{ id:XXXXX depth:0 match:1 excluded:1 filename:XXXXX Subject: All messages excluded: single match: reply 2 message{ id:XXXXX depth:0 match:1 excluded:1 filename:XXXXX diff --git a/test/T150-tagging.sh b/test/T150-tagging.sh index 2f0b3531..c292b24e 100755 --- a/test/T150-tagging.sh +++ b/test/T150-tagging.sh @@ -90,7 +90,7 @@ thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; One (inbox tag5 unread) thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag4 tag5 unread)" # generate a common input file for the next several tests. -cat > batch.in < batch.in <&1 | sed 's/: .*$//' ) -chmod u+w ${MAIL_DIR}/.notmuch/xapian/*.* +chmod u+w ${MAIL_DIR}/.notmuch/xapian/*.* test_expect_equal "$output" "A Xapian exception occurred opening database" test_done diff --git a/test/T210-raw.sh b/test/T210-raw.sh index 9a8b990c..e1d50bf9 100755 --- a/test/T210-raw.sh +++ b/test/T210-raw.sh @@ -61,7 +61,7 @@ for pow in {10..20}; do notmuch show --format=raw subject:$size > OUTPUT test_expect_equal_file mail/size-$size OUTPUT test_begin_subtest "return value, message of size $size" - test_expect_success "notmuch show --format=raw subject:$size > /dev/null" + test_expect_success "notmuch show --format=raw subject:$size > /dev/null" done test_done diff --git a/test/T230-reply-to-sender.sh b/test/T230-reply-to-sender.sh index bbeaa2b9..38fbe96a 100755 --- a/test/T230-reply-to-sender.sh +++ b/test/T230-reply-to-sender.sh @@ -43,7 +43,7 @@ add_message '[from]="Sender "' \ '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \ '[body]="Multiple recipients"' -output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) +output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) test_expect_equal "$output" "From: Notmuch Test Suite Subject: Re: notmuch-reply-test To: Sender @@ -60,7 +60,7 @@ add_message '[from]="Notmuch Test Suite "' \ '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \ '[body]="From Us, Multiple TO recipients"' -output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) +output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) test_expect_equal "$output" "From: Notmuch Test Suite Subject: Re: notmuch-reply-test To: Recipient , Someone Else @@ -78,7 +78,7 @@ add_message '[from]="Sender "' \ '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \ '[body]="reply with CC"' -output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) +output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) test_expect_equal "$output" "From: Notmuch Test Suite Subject: Re: notmuch-reply-test To: Sender @@ -96,7 +96,7 @@ add_message '[from]="Notmuch Test Suite "' \ '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \ '[body]="reply with CC"' -output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) +output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) test_expect_equal "$output" "From: Notmuch Test Suite Subject: Re: notmuch-reply-test To: Recipient @@ -113,7 +113,7 @@ add_message '[from]="Notmuch Test Suite "' \ '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \ '[body]="reply with CC"' -output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) +output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) test_expect_equal "$output" "From: Notmuch Test Suite Subject: Re: notmuch-reply-test Cc: Other Parties @@ -130,7 +130,7 @@ add_message '[from]="Sender "' \ '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \ '[body]="reply from alternate address"' -output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) +output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) test_expect_equal "$output" "From: Notmuch Test Suite Subject: Re: notmuch-reply-test To: Sender @@ -148,7 +148,7 @@ add_message '[from]="Sender "' \ '[body]="support for reply-to"' \ '[reply-to]="Sender "' -output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) +output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) test_expect_equal "$output" "From: Notmuch Test Suite Subject: Re: notmuch-reply-test To: Sender @@ -166,7 +166,7 @@ add_message '[from]="Sender "' \ '[body]="support for reply-to with multiple recipients"' \ '[reply-to]="Sender "' -output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) +output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) test_expect_equal "$output" "From: Notmuch Test Suite Subject: Re: notmuch-reply-test To: Sender @@ -184,7 +184,7 @@ add_message '[from]="Sender "' \ '[body]="Un-munging Reply-To"' \ '[reply-to]="Evil Munging List "' -output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) +output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) test_expect_equal "$output" "From: Notmuch Test Suite Subject: Re: notmuch-reply-test To: Sender @@ -198,7 +198,7 @@ test_begin_subtest "Message with header of exactly 200 bytes" add_message '[subject]="This subject is exactly 200 bytes in length. Other than its length there is not much of note here. Note that the length of 200 bytes includes the Subject: and Re: prefixes with two spaces"' \ '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \ '[body]="200-byte header"' -output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) +output=$(notmuch reply --reply-to=sender id:${gen_msg_id}) test_expect_equal "$output" "From: Notmuch Test Suite Subject: Re: This subject is exactly 200 bytes in length. Other than its length there is not much of note here. Note that the length of 200 bytes diff --git a/test/T240-dump-restore.sh b/test/T240-dump-restore.sh index 0870ff92..105de130 100755 --- a/test/T240-dump-restore.sh +++ b/test/T240-dump-restore.sh @@ -65,7 +65,7 @@ test_begin_subtest "Accumulate with new tags" test_expect_success \ 'notmuch restore --input=dump.expected && notmuch restore --accumulate --input=dump-ABC_DEF.expected && - notmuch dump > OUTPUT.$test_count && + notmuch dump > OUTPUT.$test_count && notmuch restore --input=dump.expected && test_cmp dump-ABC_DEF.expected OUTPUT.$test_count' @@ -235,7 +235,7 @@ test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count test_begin_subtest 'format=batch-tag, checking encoded output' NOTMUCH_DUMP_TAGS --format=batch-tag -- from:cworth |\ awk "{ print \"+$enc1 +$enc2 +$enc3 -- \" \$5 }" > EXPECTED.$test_count -NOTMUCH_DUMP_TAGS --format=batch-tag -- from:cworth > OUTPUT.$test_count +NOTMUCH_DUMP_TAGS --format=batch-tag -- from:cworth > OUTPUT.$test_count test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count test_begin_subtest 'restoring sane tags' @@ -322,6 +322,7 @@ EOF test_expect_equal_file EXPECTED OUTPUT +backup_database test_begin_subtest 'roundtripping random message-ids and tags' ${TEST_DIRECTORY}/random-corpus --config-path=${NOTMUCH_CONFIG} \ @@ -338,7 +339,7 @@ test_begin_subtest 'roundtripping random message-ids and tags' sort > OUTPUT.$test_count test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count +restore_database test_done -# Note the database is "poisoned" for sup format at this point. diff --git a/test/T310-emacs.sh b/test/T310-emacs.sh index 78ac19a8..e6489246 100755 --- a/test/T310-emacs.sh +++ b/test/T310-emacs.sh @@ -161,6 +161,28 @@ test_emacs "(notmuch-show \"$os_x_darwin_thread\") output=$(notmuch search $os_x_darwin_thread | notmuch_search_sanitize) test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread)" +test_begin_subtest "notmuch-show: before-tag-hook is run, variables are defined" +output=$(test_emacs '(let ((notmuch-test-tag-hook-output nil) + (notmuch-before-tag-hook (function notmuch-test-tag-hook))) + (notmuch-show "id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com") + (execute-kbd-macro "+activate-hook\n") + (execute-kbd-macro "-activate-hook\n") + notmuch-test-tag-hook-output)') +test_expect_equal "$output" \ +'(("id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com" "-activate-hook") + ("id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com" "+activate-hook"))' + +test_begin_subtest "notmuch-show: after-tag-hook is run, variables are defined" +output=$(test_emacs '(let ((notmuch-test-tag-hook-output nil) + (notmuch-after-tag-hook (function notmuch-test-tag-hook))) + (notmuch-show "id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com") + (execute-kbd-macro "+activate-hook\n") + (execute-kbd-macro "-activate-hook\n") + notmuch-test-tag-hook-output)') +test_expect_equal "$output" \ +'(("id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com" "-activate-hook") + ("id:ddd65cda0911171950o4eea4389v86de9525e46052d3@mail.gmail.com" "+activate-hook"))' + test_begin_subtest "Message with .. in Message-Id:" add_message [id]=123..456@example '[subject]="Message with .. in Message-Id"' test_emacs '(notmuch-search "id:\"123..456@example\"") diff --git a/test/T340-maildir-sync.sh b/test/T340-maildir-sync.sh index 7416dd61..a697317f 100755 --- a/test/T340-maildir-sync.sh +++ b/test/T340-maildir-sync.sh @@ -174,7 +174,7 @@ thread:XXX 2001-01-05 [1/1(3)] Notmuch Test Suite; Duplicated message (inbox r test_begin_subtest "Tag changes modify flags of multiple files" notmuch tag -replied subject:"Duplicated message" (cd $MAIL_DIR/cur/; ls duplicated*) > actual -test_expect_equal "$(< actual)" "duplicated-message-another-copy:2,S +test_expect_equal "$(< actual)" "duplicated-message-another-copy:2,S duplicated-message-copy:2,S duplicated-message:2,S" diff --git a/test/T360-symbol-hiding.sh b/test/T360-symbol-hiding.sh index 729b9d72..642457bf 100755 --- a/test/T360-symbol-hiding.sh +++ b/test/T360-symbol-hiding.sh @@ -17,7 +17,7 @@ $TEST_DIRECTORY/symbol-test ${PWD}/fakedb ${PWD}/nonexistent 2>&1 \ | notmuch_dir_sanitize | sed -e "s,\`,\',g" -e "s,No [^[:space:]]* database,No XXXXXX database,g" > OUTPUT cat < EXPECTED -A Xapian exception occurred opening database: Couldn't stat 'CWD/fakedb/.notmuch/xapian' +Cannot open Xapian database at CWD/fakedb/.notmuch/xapian: Couldn't stat 'CWD/fakedb/.notmuch/xapian' caught No XXXXXX database found at path 'CWD/nonexistent' EOF test_expect_equal_file EXPECTED OUTPUT diff --git a/test/T391-python-cffi.sh b/test/T391-python-cffi.sh index f961069b..d54bad27 100755 --- a/test/T391-python-cffi.sh +++ b/test/T391-python-cffi.sh @@ -10,5 +10,5 @@ fi test_begin_subtest "python cffi tests" pytest_dir=$NOTMUCH_BUILDDIR/bindings/python-cffi/build/stage printf "[pytest]\nminversion = 3.0\naddopts = -ra\n" > $pytest_dir/pytest.ini -test_expect_success "(cd $pytest_dir && ${NOTMUCH_PYTHON} -m pytest --log-file=$TMP_DIRECTORY/test.output)" +test_expect_success "(cd $pytest_dir && ${NOTMUCH_PYTHON} -m pytest --verbose --log-file=$TMP_DIRECTORY/test.output)" test_done diff --git a/test/T400-hooks.sh b/test/T400-hooks.sh index 49c690eb..0c84b7dd 100755 --- a/test/T400-hooks.sh +++ b/test/T400-hooks.sh @@ -2,7 +2,7 @@ test_description='hooks' . $(dirname "$0")/test-lib.sh || exit 1 -HOOK_DIR=${MAIL_DIR}/.notmuch/hooks +test_require_external_prereq xapian-delve create_echo_hook () { local TOKEN="${RANDOM}" @@ -15,17 +15,37 @@ EOF echo "${TOKEN}" > ${2} } -create_failing_hook () { +create_write_hook () { + local TOKEN="${RANDOM}" mkdir -p ${HOOK_DIR} cat <"${HOOK_DIR}/${1}" #!/bin/sh -exit 13 +if xapian-delve ${MAIL_DIR}/.notmuch/xapian | grep -q "writing = false"; then + echo "${TOKEN}" > ${3} +fi EOF chmod +x "${HOOK_DIR}/${1}" + echo "${TOKEN}" > ${2} } -rm_hooks () { - rm -rf ${HOOK_DIR} +create_change_hook () { + mkdir -p ${HOOK_DIR} + cat <"${HOOK_DIR}/${1}" +#!/bin/sh +notmuch insert --no-hooks < ${2} > /dev/null +rm -f ${2} +EOF + chmod +x "${HOOK_DIR}/${1}" +} + +create_failing_hook () { + local HOOK_DIR=${2} + mkdir -p ${HOOK_DIR} + cat <"${HOOK_DIR}/${1}" +#!/bin/sh +exit 13 +EOF + chmod +x "${HOOK_DIR}/${1}" } # add a message to generate mail dir and database @@ -33,89 +53,153 @@ add_message # create maildir structure for notmuch-insert mkdir -p "$MAIL_DIR"/{cur,new,tmp} -test_begin_subtest "pre-new is run" -rm_hooks -generate_message -create_echo_hook "pre-new" expected output -notmuch new > /dev/null -test_expect_equal_file expected output - -test_begin_subtest "post-new is run" -rm_hooks -generate_message -create_echo_hook "post-new" expected output -notmuch new > /dev/null -test_expect_equal_file expected output - -test_begin_subtest "post-insert hook is run" -rm_hooks -generate_message -create_echo_hook "post-insert" expected output -notmuch insert < "$gen_msg_filename" -test_expect_equal_file expected output - -test_begin_subtest "pre-new is run before post-new" -rm_hooks -generate_message -create_echo_hook "pre-new" pre-new.expected pre-new.output -create_echo_hook "post-new" post-new.expected post-new.output -notmuch new > /dev/null -test_expect_equal_file post-new.expected post-new.output - -test_begin_subtest "pre-new non-zero exit status (hook status)" -rm_hooks -generate_message -create_failing_hook "pre-new" -output=`notmuch new 2>&1` -test_expect_equal "$output" "Error: pre-new hook failed with status 13" - -# depends on the previous subtest leaving broken hook behind -test_begin_subtest "pre-new non-zero exit status (notmuch status)" -test_expect_code 1 "notmuch new" - -# depends on the previous subtests leaving 1 new message behind -test_begin_subtest "pre-new non-zero exit status aborts new" -rm_hooks -output=$(NOTMUCH_NEW) -test_expect_equal "$output" "Added 1 new message to the database." - -test_begin_subtest "post-new non-zero exit status (hook status)" -rm_hooks -generate_message -create_failing_hook "post-new" -NOTMUCH_NEW 2>output.stderr >output -cat output.stderr >> output -echo "Added 1 new message to the database." > expected -echo "Error: post-new hook failed with status 13" >> expected -test_expect_equal_file expected output - -# depends on the previous subtest leaving broken hook behind -test_begin_subtest "post-new non-zero exit status (notmuch status)" -test_expect_code 1 "notmuch new" - -test_begin_subtest "post-insert hook does not affect insert status" -rm_hooks -generate_message -create_failing_hook "post-insert" -test_expect_success "notmuch insert < \"$gen_msg_filename\" > /dev/null" - -test_begin_subtest "hook without executable permissions" -rm_hooks -mkdir -p ${HOOK_DIR} -cat <"${HOOK_DIR}/pre-new" -#!/bin/sh -echo foo +for config in traditional profile explicit relative XDG split; do + unset NOTMUCH_PROFILE + notmuch config set database.hook_dir + notmuch config set database.path ${MAIL_DIR} + case $config in + traditional) + HOOK_DIR=${MAIL_DIR}/.notmuch/hooks + ;; + profile) + dir=${HOME}/.config/notmuch/other + mkdir -p ${dir} + HOOK_DIR=${dir}/hooks + cp ${NOTMUCH_CONFIG} ${dir}/config + export NOTMUCH_PROFILE=other + ;; + explicit) + HOOK_DIR=${HOME}/.notmuch-hooks + mkdir -p $HOOK_DIR + notmuch config set database.hook_dir $HOOK_DIR + ;; + relative) + HOOK_DIR=${HOME}/.notmuch-hooks + mkdir -p $HOOK_DIR + notmuch config set database.hook_dir .notmuch-hooks + ;; + XDG) + HOOK_DIR=${HOME}/.config/notmuch/default/hooks + ;; + split) + dir="$TMP_DIRECTORY/database.$test_count" + notmuch config set database.path $dir + notmuch config set database.mail_root $MAIL_DIR + HOOK_DIR=${dir}/hooks + ;; + esac + + test_begin_subtest "pre-new is run [${config}]" + rm -rf ${HOOK_DIR} + generate_message + create_echo_hook "pre-new" expected output $HOOK_DIR + notmuch new > /dev/null + test_expect_equal_file expected output + + test_begin_subtest "post-new is run [${config}]" + rm -rf ${HOOK_DIR} + generate_message + create_echo_hook "post-new" expected output $HOOK_DIR + notmuch new > /dev/null + test_expect_equal_file expected output + + test_begin_subtest "post-insert hook is run [${config}]" + rm -rf ${HOOK_DIR} + generate_message + create_echo_hook "post-insert" expected output $HOOK_DIR + notmuch insert < "$gen_msg_filename" + test_expect_equal_file expected output + + test_begin_subtest "pre-new is run before post-new [${config}]" + rm -rf ${HOOK_DIR} + generate_message + create_echo_hook "pre-new" pre-new.expected pre-new.output $HOOK_DIR + create_echo_hook "post-new" post-new.expected post-new.output $HOOK_DIR + notmuch new > /dev/null + test_expect_equal_file post-new.expected post-new.output + + test_begin_subtest "pre-new non-zero exit status (hook status) [${config}]" + rm -rf ${HOOK_DIR} + generate_message + create_failing_hook "pre-new" $HOOK_DIR + output=`notmuch new 2>&1` + test_expect_equal "$output" "Error: pre-new hook failed with status 13" + + # depends on the previous subtest leaving broken hook behind + test_begin_subtest "pre-new non-zero exit status (notmuch status) [${config}]" + test_expect_code 1 "notmuch new" + + # depends on the previous subtests leaving 1 new message behind + test_begin_subtest "pre-new non-zero exit status aborts new [${config}]" + rm -rf ${HOOK_DIR} + output=$(NOTMUCH_NEW) + test_expect_equal "$output" "Added 1 new message to the database." + + test_begin_subtest "post-new non-zero exit status (hook status) [${config}]" + rm -rf ${HOOK_DIR} + generate_message + create_failing_hook "post-new" $HOOK_DIR + NOTMUCH_NEW 2>output.stderr >output + cat output.stderr >> output + echo "Added 1 new message to the database." > expected + echo "Error: post-new hook failed with status 13" >> expected + test_expect_equal_file expected output + + # depends on the previous subtest leaving broken hook behind + test_begin_subtest "post-new non-zero exit status (notmuch status) [${config}]" + test_expect_code 1 "notmuch new" + + test_begin_subtest "post-insert hook does not affect insert status [${config}]" + rm -rf ${HOOK_DIR} + generate_message + create_failing_hook "post-insert" $HOOK_DIR + test_expect_success "notmuch insert < \"$gen_msg_filename\" > /dev/null" + + test_begin_subtest "hook without executable permissions [${config}]" + rm -rf ${HOOK_DIR} + mkdir -p ${HOOK_DIR} + cat <"${HOOK_DIR}/pre-new" + #!/bin/sh + echo foo +EOF + output=`notmuch new 2>&1` + test_expect_code 1 "notmuch new" + + test_begin_subtest "hook execution failure [${config}]" + rm -rf ${HOOK_DIR} + mkdir -p ${HOOK_DIR} + cat <"${HOOK_DIR}/pre-new" + no hashbang, execl fails EOF -output=`notmuch new 2>&1` -test_expect_code 1 "notmuch new" - -test_begin_subtest "hook execution failure" -rm_hooks -mkdir -p ${HOOK_DIR} -cat <"${HOOK_DIR}/pre-new" -no hashbang, execl fails + chmod +x "${HOOK_DIR}/pre-new" + test_expect_code 1 "notmuch new" + + test_begin_subtest "post-new with write access [${config}]" + rm -rf ${HOOK_DIR} + create_write_hook "post-new" write.expected write.output $HOOK_DIR + NOTMUCH_NEW + test_expect_equal_file write.expected write.output + + test_begin_subtest "pre-new with write access [${config}]" + rm -rf ${HOOK_DIR} + create_write_hook "pre-new" write.expected write.output $HOOK_DIR + NOTMUCH_NEW + test_expect_equal_file write.expected write.output + + test_begin_subtest "add message in pre-new [${config}]" + rm -rf ${HOOK_DIR} + generate_message '[subject]="add msg in pre-new"' + id1=$gen_msg_id + create_change_hook "pre-new" $gen_msg_filename $HOOK_DIR + generate_message '[subject]="add msg in new"' + NOTMUCH_NEW + notmuch search id:$id1 or id:$gen_msg_id | notmuch_search_sanitize > OUTPUT + cat < EXPECTED + thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; add msg in pre-new (inbox unread) + thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; add msg in new (inbox unread) EOF -chmod +x "${HOOK_DIR}/pre-new" -test_expect_code 1 "notmuch new" + test_expect_equal_file EXPECTED OUTPUT + rm -rf ${HOOK_DIR} +done test_done diff --git a/test/T410-argument-parsing.sh b/test/T410-argument-parsing.sh index b31d239a..d9aa8e2d 100755 --- a/test/T410-argument-parsing.sh +++ b/test/T410-argument-parsing.sh @@ -3,7 +3,7 @@ test_description="argument parsing" . $(dirname "$0")/test-lib.sh || exit 1 test_begin_subtest "sanity check" -$TEST_DIRECTORY/arg-test pos1 --keyword=one --boolean --string=foo pos2 --int=7 --flag=one --flag=three > OUTPUT +$TEST_DIRECTORY/arg-test pos1 --keyword=one --boolean --string=foo pos2 --int=7 --flag=one --flag=three > OUTPUT cat < EXPECTED boolean 1 keyword 1 diff --git a/test/T480-hex-escaping.sh b/test/T480-hex-escaping.sh index 2c5bbb63..b2eb80b9 100755 --- a/test/T480-hex-escaping.sh +++ b/test/T480-hex-escaping.sh @@ -14,13 +14,13 @@ test_expect_equal "$tag_enc1" "comic_swear=%24%26%5e%25%24%5e%25%5c%5c%2f%2f-+%2 test_begin_subtest "round trip newlines" printf 'this\n tag\t has\n spaces\n' > EXPECTED.$test_count -$TEST_DIRECTORY/hex-xcode --direction=encode < EXPECTED.$test_count |\ +$TEST_DIRECTORY/hex-xcode --direction=encode < EXPECTED.$test_count |\ $TEST_DIRECTORY/hex-xcode --direction=decode > OUTPUT.$test_count test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count test_begin_subtest "round trip 8bit chars" echo '%c3%91%c3%a5%c3%b0%c3%a3%c3%a5%c3%a9-%c3%8f%c3%8a' > EXPECTED.$test_count -$TEST_DIRECTORY/hex-xcode --direction=decode < EXPECTED.$test_count |\ +$TEST_DIRECTORY/hex-xcode --direction=decode < EXPECTED.$test_count |\ $TEST_DIRECTORY/hex-xcode --direction=encode > OUTPUT.$test_count test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count @@ -37,13 +37,13 @@ test_expect_equal "$tag_enc1" "comic_swear=%24%26%5e%25%24%5e%25%5c%5c%2f%2f-+%2 test_begin_subtest "round trip newlines (in-place)" printf 'this\n tag\t has\n spaces\n' > EXPECTED.$test_count -$TEST_DIRECTORY/hex-xcode --in-place --direction=encode < EXPECTED.$test_count |\ +$TEST_DIRECTORY/hex-xcode --in-place --direction=encode < EXPECTED.$test_count |\ $TEST_DIRECTORY/hex-xcode --in-place --direction=decode > OUTPUT.$test_count test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count test_begin_subtest "round trip 8bit chars (in-place)" echo '%c3%91%c3%a5%c3%b0%c3%a3%c3%a5%c3%a9-%c3%8f%c3%8a' > EXPECTED.$test_count -$TEST_DIRECTORY/hex-xcode --in-place --direction=decode < EXPECTED.$test_count |\ +$TEST_DIRECTORY/hex-xcode --in-place --direction=decode < EXPECTED.$test_count |\ $TEST_DIRECTORY/hex-xcode --in-place --direction=encode > OUTPUT.$test_count test_expect_equal_file EXPECTED.$test_count OUTPUT.$test_count diff --git a/test/T510-thread-replies.sh b/test/T510-thread-replies.sh index 8b96a1db..2859d29f 100755 --- a/test/T510-thread-replies.sh +++ b/test/T510-thread-replies.sh @@ -178,7 +178,7 @@ test_expect_equal_json "$output" "$expected" add_email_corpus threading test_begin_subtest "reply to ghost" -notmuch show --entire-thread=true id:000-real-root@example.org | grep ^Subject: | head -1 > OUTPUT +notmuch show --entire-thread=true id:000-real-root@example.org | grep ^Subject: | head -1 > OUTPUT cat < EXPECTED Subject: root message EOF diff --git a/test/T530-upgrade.sh b/test/T530-upgrade.sh new file mode 100755 index 00000000..5f0de2ed --- /dev/null +++ b/test/T530-upgrade.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +test_description='database upgrades' +. $(dirname "$0")/test-lib.sh || exit 1 + +test_require_external_prereq xapian-metadata + +XAPIAN_PATH=$MAIL_DIR/.notmuch/xapian +BACKUP_PATH=$MAIL_DIR/.notmuch/backups + +delete_feature () { + local key=$1 + features=$(xapian-metadata get $XAPIAN_PATH features | grep -v "^$key") + xapian-metadata set $XAPIAN_PATH features "$features" +} + +add_email_corpus + +for key in 'multiple paths per message' \ + 'relative directory paths' \ + 'exact folder:/path: search' \ + 'mail documents for missing messages' \ + 'modification tracking'; do + backup_database + test_begin_subtest "upgrade is triggered by missing '$key'" + delete_feature "$key" + output=$(notmuch new | grep Welcome) + test_expect_equal \ + "$output" \ + "Welcome to a new version of notmuch! Your database will now be upgraded." + + restore_database + + backup_database + test_begin_subtest "backup can be restored ['$key']" + notmuch dump > BEFORE + delete_feature "$key" + notmuch new + notmuch tag -inbox '*' + dump_file=$(echo ${BACKUP_PATH}/dump*) + notmuch restore --input=$dump_file + notmuch dump > AFTER + test_expect_equal_file BEFORE AFTER + restore_database +done + +for key in 'from/subject/message-ID in database' \ + 'indexed MIME types' \ + 'index body and headers separately'; do + backup_database + test_begin_subtest "upgrade not triggered by missing '$key'" + delete_feature "$key" + output=$(notmuch new | grep Welcome) + test_expect_equal "$output" "" + restore_database +done + +test_begin_subtest "upgrade with configured backup dir" +notmuch config set database.backup_dir ${HOME}/backups +delete_feature 'modification tracking' +notmuch new | grep Backing | notmuch_dir_sanitize | sed 's/dump-[0-9T]*/dump-XXX/' > OUTPUT +cat < EXPECTED +Backing up tags to CWD/home/backups/dump-XXX.gz... +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "upgrade with relative configured backup dir" +notmuch config set database.backup_dir ${HOME}/backups +delete_feature 'modification tracking' +notmuch new | grep Backing | notmuch_dir_sanitize | sed 's/dump-[0-9T]*/dump-XXX/' > OUTPUT +cat < EXPECTED +Backing up tags to CWD/home/backups/dump-XXX.gz... +EOF +test_expect_equal_file EXPECTED OUTPUT + + +test_done diff --git a/test/T560-lib-error.sh b/test/T560-lib-error.sh index 260ac120..1f4482cb 100755 --- a/test/T560-lib-error.sh +++ b/test/T560-lib-error.sh @@ -22,7 +22,7 @@ EOF cat <<'EOF' >EXPECTED == stdout == == stderr == -Error: Cannot open a database for a NULL path. +Error: could not locate database. EOF test_expect_equal_file EXPECTED OUTPUT @@ -76,7 +76,7 @@ EOF cat <<'EOF' >EXPECTED == stdout == == stderr == -Error opening database at CWD/nonexistent/foo/.notmuch: No such file or directory +Error: Cannot open database at CWD/nonexistent/foo: No such file or directory. EOF test_expect_equal_file EXPECTED OUTPUT @@ -93,7 +93,7 @@ EOF cat <<'EOF' >EXPECTED == stdout == == stderr == -Error: Cannot create a database for a NULL path. +Error: could not locate database. EOF test_expect_equal_file EXPECTED OUTPUT @@ -111,7 +111,7 @@ EOF cat <<'EOF' >EXPECTED == stdout == == stderr == -Error: Cannot create database at CWD/nonexistent/foo: No such file or directory. +Error: Cannot open database at CWD/nonexistent/foo: No such file or directory. EOF test_expect_equal_file EXPECTED OUTPUT diff --git a/test/T562-lib-database.sh b/test/T562-lib-database.sh index dd4f2566..769fe86e 100755 --- a/test/T562-lib-database.sh +++ b/test/T562-lib-database.sh @@ -325,7 +325,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} notmuch_tags_t *result; EXPECT0(notmuch_database_close (db)); result = notmuch_database_get_all_tags (db); - printf("%d\n", result == NULL); + printf("%d\n", result == NULL); stat = NOTMUCH_STATUS_XAPIAN_EXCEPTION; } EOF @@ -340,17 +340,16 @@ test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "get config from closed database" cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} { - const char *result; + char *result; EXPECT0(notmuch_database_close (db)); stat = notmuch_database_get_config (db, "foo", &result); - printf("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION); + printf("%d\n", stat == NOTMUCH_STATUS_SUCCESS); } EOF cat < EXPECTED == stdout == 1 == stderr == -Error: A Xapian exception occurred getting metadata: Database has been closed EOF test_expect_equal_file EXPECTED OUTPUT @@ -359,7 +358,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} { EXPECT0(notmuch_database_close (db)); stat = notmuch_database_set_config (db, "foo", "bar"); - printf("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION); + printf("%d\n", stat == NOTMUCH_STATUS_XAPIAN_EXCEPTION); } EOF cat < EXPECTED @@ -376,7 +375,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} notmuch_indexopts_t *result; EXPECT0(notmuch_database_close (db)); result = notmuch_database_get_default_indexopts (db); - printf("%d\n", result == NULL); + printf("%d\n", result != NULL); } EOF cat < EXPECTED @@ -414,7 +413,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} EXPECT0(notmuch_database_close (db)); notmuch_decryption_policy_t policy = notmuch_indexopts_get_decrypt_policy (result); stat = notmuch_indexopts_set_decrypt_policy (result, policy); - printf("%d\n%d\n", policy == NOTMUCH_DECRYPT_AUTO, stat == NOTMUCH_STATUS_SUCCESS); + printf("%d\n%d\n", policy == NOTMUCH_DECRYPT_AUTO, stat == NOTMUCH_STATUS_SUCCESS); } EOF cat < EXPECTED diff --git a/test/T566-lib-message.sh b/test/T566-lib-message.sh index 0ba601f9..ee55ef29 100755 --- a/test/T566-lib-message.sh +++ b/test/T566-lib-message.sh @@ -279,7 +279,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} notmuch_status_t status; notmuch_bool_t out; status = notmuch_message_has_maildir_flag_st (message, 'S', &out); - printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION); + printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION); } EOF cat < EXPECTED @@ -295,7 +295,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} { notmuch_status_t status; status = notmuch_message_maildir_flags_to_tags (message); - printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION); + printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION); } EOF cat < EXPECTED @@ -311,7 +311,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} { notmuch_status_t status; status = notmuch_message_remove_all_tags (message); - printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION); + printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION); } EOF cat < EXPECTED @@ -327,7 +327,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} { notmuch_status_t status; status = notmuch_message_freeze (message); - printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_SUCCESS); + printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_SUCCESS); } EOF cat < EXPECTED @@ -343,7 +343,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} { notmuch_status_t status; status = notmuch_message_thaw (message); - printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW); + printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW); } EOF cat < EXPECTED @@ -358,7 +358,7 @@ test_begin_subtest "Handle destroying message with closed db" cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} { notmuch_message_destroy (message); - printf("%d\n%d\n", message != NULL, 1); + printf("%d\n%d\n", message != NULL, 1); } EOF cat < EXPECTED @@ -374,7 +374,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} { notmuch_database_t *db2; db2 = notmuch_message_get_database (message); - printf("%d\n%d\n", message != NULL, db == db2); + printf("%d\n%d\n", message != NULL, db == db2); } EOF cat < EXPECTED @@ -390,7 +390,7 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} { notmuch_status_t status; status = notmuch_message_reindex (message, NULL); - printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION); + printf("%d\n%d\n", message != NULL, status == NOTMUCH_STATUS_XAPIAN_EXCEPTION); } EOF cat < EXPECTED diff --git a/test/T590-libconfig.sh b/test/T590-libconfig.sh index 360e45b0..745e1bb4 100755 --- a/test/T590-libconfig.sh +++ b/test/T590-libconfig.sh @@ -15,9 +15,21 @@ int main (int argc, char** argv) notmuch_database_t *db; char *val; notmuch_status_t stat; + char *msg = NULL; - EXPECT0(notmuch_database_open (argv[1], NOTMUCH_DATABASE_MODE_READ_WRITE, &db)); + for (int i = 1; i < argc; i++) + if (strcmp (argv[i], "%NULL%") == 0) argv[i] = NULL; + stat = notmuch_database_open_with_config (argv[1], + NOTMUCH_DATABASE_MODE_READ_WRITE, + argv[2], + argv[3], + &db, + &msg); + if (stat != NOTMUCH_STATUS_SUCCESS) { + fprintf (stderr, "error opening database\n%s\n%s\n", notmuch_status_to_string (stat), msg ? msg : ""); + exit (1); + } EOF cat < c_tail @@ -26,27 +38,27 @@ cat < c_tail EOF test_begin_subtest "notmuch_database_{set,get}_config" -cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} { - EXPECT0(notmuch_database_set_config (db, "testkey1", "testvalue1")); - EXPECT0(notmuch_database_set_config (db, "testkey2", "testvalue2")); - EXPECT0(notmuch_database_get_config (db, "testkey1", &val)); - printf("testkey1 = %s\n", val); - EXPECT0(notmuch_database_get_config (db, "testkey2", &val)); - printf("testkey2 = %s\n", val); + EXPECT0(notmuch_database_set_config (db, "test.key1", "testvalue1")); + EXPECT0(notmuch_database_set_config (db, "test.key2", "testvalue2")); + EXPECT0(notmuch_database_get_config (db, "test.key1", &val)); + printf("test.key1 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key2", &val)); + printf("test.key2 = %s\n", val); } EOF cat <<'EOF' >EXPECTED == stdout == -testkey1 = testvalue1 -testkey2 = testvalue2 +test.key1 = testvalue1 +test.key2 = testvalue2 == stderr == EOF test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "notmuch_database_get_config_list: empty list" -cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL% { notmuch_config_list_t *list; EXPECT0(notmuch_database_get_config_list (db, "nonexistent", &list)); @@ -78,7 +90,7 @@ EOF test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "notmuch_database_get_config_list: all pairs" -cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL% { notmuch_config_list_t *list; EXPECT0(notmuch_database_set_config (db, "zzzafter", "afterval")); @@ -93,8 +105,8 @@ EOF cat <<'EOF' >EXPECTED == stdout == aaabefore beforeval -testkey1 testvalue1 -testkey2 testvalue2 +test.key1 testvalue1 +test.key2 testvalue2 zzzafter afterval == stderr == EOF @@ -115,18 +127,18 @@ EOF cat <<'EOF' >EXPECTED == stdout == aaabefore 1 -testkey1 1 -testkey2 1 +test.key1 1 +test.key2 1 zzzafter 1 == stderr == EOF test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "notmuch_database_get_config_list: one prefix" -cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL% { notmuch_config_list_t *list; - EXPECT0(notmuch_database_get_config_list (db, "testkey", &list)); + EXPECT0(notmuch_database_get_config_list (db, "test.key", &list)); for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) { printf("%s %s\n", notmuch_config_list_key (list), notmuch_config_list_value(list)); } @@ -135,14 +147,14 @@ cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} EOF cat <<'EOF' >EXPECTED == stdout == -testkey1 testvalue1 -testkey2 testvalue2 +test.key1 testvalue1 +test.key2 testvalue2 == stderr == EOF test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "dump config" -cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL% { EXPECT0(notmuch_database_set_config (db, "key with spaces", "value, with, spaces!")); } @@ -152,21 +164,667 @@ cat <<'EOF' >EXPECTED #notmuch-dump batch-tag:3 config #@ aaabefore beforeval #@ key%20with%20spaces value,%20with,%20spaces%21 -#@ testkey1 testvalue1 -#@ testkey2 testvalue2 +#@ test.key1 testvalue1 +#@ test.key2 testvalue2 #@ zzzafter afterval EOF test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "restore config" notmuch dump --include=config >EXPECTED -cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL% { - EXPECT0(notmuch_database_set_config (db, "testkey1", "mutatedvalue")); + EXPECT0(notmuch_database_set_config (db, "test.key1", "mutatedvalue")); } EOF notmuch restore --include=config OUTPUT test_expect_equal_file EXPECTED OUTPUT +backup_database +test_begin_subtest "override config from file" +notmuch config set test.key1 overridden +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} +{ + EXPECT0(notmuch_database_get_config (db, "test.key1", &val)); + printf("test.key1 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key2", &val)); + printf("test.key2 = %s\n", val); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +test.key1 = overridden +test.key2 = testvalue2 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT +restore_database + +test_begin_subtest "NOTMUCH_CONFIG_HOOK_DIR: traditional" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL% +{ + const char *val = notmuch_config_get (db, NOTMUCH_CONFIG_HOOK_DIR); + printf("database.hook_dir = %s\n", val); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +database.hook_dir = MAIL_DIR/.notmuch/hooks +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "NOTMUCH_CONFIG_HOOK_DIR: xdg" +dir="${HOME}/.config/notmuch/default/hooks" +mkdir -p $dir +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL% +{ + const char *val = notmuch_config_get (db, NOTMUCH_CONFIG_HOOK_DIR); + printf("database.hook_dir = %s\n", val); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +database.hook_dir = CWD/home/.config/notmuch/default/hooks +== stderr == +EOF +rmdir $dir +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "notmuch_config_get_values" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL% +{ + notmuch_config_values_t *values; + EXPECT0(notmuch_config_set (db, NOTMUCH_CONFIG_NEW_TAGS, "a;b;c")); + for (values = notmuch_config_get_values (db, NOTMUCH_CONFIG_NEW_TAGS); + notmuch_config_values_valid (values); + notmuch_config_values_move_to_next (values)) + { + puts (notmuch_config_values_get (values)); + } +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +a +b +c +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT +restore_database + +test_begin_subtest "notmuch_config_get_values_string" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL% +{ + notmuch_config_values_t *values; + EXPECT0(notmuch_database_set_config (db, "test.list", "x;y;z")); + for (values = notmuch_config_get_values_string (db, "test.list"); + notmuch_config_values_valid (values); + notmuch_config_values_move_to_next (values)) + { + puts (notmuch_config_values_get (values)); + } +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +x +y +z +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT +restore_database + +test_begin_subtest "notmuch_config_get_values (restart)" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL% +{ + notmuch_config_values_t *values; + EXPECT0(notmuch_config_set (db, NOTMUCH_CONFIG_NEW_TAGS, "a;b;c")); + for (values = notmuch_config_get_values (db, NOTMUCH_CONFIG_NEW_TAGS); + notmuch_config_values_valid (values); + notmuch_config_values_move_to_next (values)) + { + puts (notmuch_config_values_get (values)); + } + for (notmuch_config_values_start (values); + notmuch_config_values_valid (values); + notmuch_config_values_move_to_next (values)) + { + puts (notmuch_config_values_get (values)); + } +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +a +b +c +a +b +c +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT +restore_database + +backup_database +test_begin_subtest "notmuch_config_get_values, trailing ;" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL% +{ + notmuch_config_values_t *values; + EXPECT0(notmuch_config_set (db, NOTMUCH_CONFIG_NEW_TAGS, "a;b;c")); + for (values = notmuch_config_get_values (db, NOTMUCH_CONFIG_NEW_TAGS); + notmuch_config_values_valid (values); + notmuch_config_values_move_to_next (values)) + { + puts (notmuch_config_values_get (values)); + } +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +a +b +c +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT +restore_database + +backup_database +test_begin_subtest "get config by key" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} +{ + printf("before = %s\n", notmuch_config_get (db, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS)); + EXPECT0(notmuch_database_set_config (db, "maildir.synchronize_flags", "false")); + printf("after = %s\n", notmuch_config_get (db, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS)); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +before = true +after = false +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT +restore_database + +backup_database +test_begin_subtest "set config by key" +notmuch config set test.key1 overridden +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} +{ + printf("before = %s\n", notmuch_config_get (db, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS)); + EXPECT0(notmuch_config_set (db, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS, "false")); + printf("after = %s\n", notmuch_config_get (db, NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS)); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +before = true +after = false +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT +restore_database + +test_begin_subtest "load default values" +export MAILDIR=${MAIL_DIR} +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} '' %NULL% +{ + notmuch_config_key_t key; + for (key = NOTMUCH_CONFIG_FIRST; + key < NOTMUCH_CONFIG_LAST; + key = (notmuch_config_key_t)(key + 1)) { + const char *val = notmuch_config_get (db, key); + printf("%s\n", val ? val : "NULL" ); + } +} +EOF + +notmuch_passwd_sanitize < OUTPUT > OUTPUT.clean + +cat <<'EOF' >EXPECTED +== stdout == +MAIL_DIR +MAIL_DIR +MAIL_DIR/.notmuch/hooks +MAIL_DIR/.notmuch/backups + +unread;inbox + +true +USERNAME@FQDN +NULL +USER_FULL_NAME +== stderr == +EOF +unset MAILDIR +test_expect_equal_file EXPECTED OUTPUT.clean + +backup_database +test_begin_subtest "override config from \${NOTMUCH_CONFIG}" +notmuch config set test.key1 overridden +# second argument omitted to make argv[2] == NULL +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} +{ + EXPECT0(notmuch_database_get_config (db, "test.key1", &val)); + printf("test.key1 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key2", &val)); + printf("test.key2 = %s\n", val); +} +EOF +notmuch config set test.key1 +cat <<'EOF' >EXPECTED +== stdout == +test.key1 = overridden +test.key2 = testvalue2 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT +restore_database + +backup_database +test_begin_subtest "override config from \${HOME}/.notmuch-config" +ovconfig=${HOME}/.notmuch-config +cp ${NOTMUCH_CONFIG} ${ovconfig} +old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} +unset NOTMUCH_CONFIG +notmuch --config=${ovconfig} config set test.key1 overridden-home +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL% +{ + EXPECT0(notmuch_database_get_config (db, "test.key1", &val)); + printf("test.key1 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key2", &val)); + printf("test.key2 = %s\n", val); +} +EOF +rm -f ${ovconfig} +NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +cat <<'EOF' >EXPECTED +== stdout == +test.key1 = overridden-home +test.key2 = testvalue2 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT +restore_database + +backup_database +test_begin_subtest "override config from \${XDG_CONFIG_HOME}/notmuch" +ovconfig=${HOME}/.config/notmuch/default/config +mkdir -p $(dirname ${ovconfig}) +cp ${NOTMUCH_CONFIG} ${ovconfig} +old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} +unset NOTMUCH_CONFIG +notmuch --config=${ovconfig} config set test.key1 overridden-xdg +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL% +{ + EXPECT0(notmuch_database_get_config (db, "test.key1", &val)); + printf("test.key1 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key2", &val)); + printf("test.key2 = %s\n", val); +} +EOF +rm -f ${ovconfig} +NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +cat <<'EOF' >EXPECTED +== stdout == +test.key1 = overridden-xdg +test.key2 = testvalue2 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT +restore_database + +backup_database +test_begin_subtest "override config from \${XDG_CONFIG_HOME}/notmuch with profile" +ovconfig=${HOME}/.config/notmuch/work/config +mkdir -p $(dirname ${ovconfig}) +cp ${NOTMUCH_CONFIG} ${ovconfig} +old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} +unset NOTMUCH_CONFIG +notmuch --config=${ovconfig} config set test.key1 overridden-xdg-profile +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% work +{ + EXPECT0(notmuch_database_get_config (db, "test.key1", &val)); + printf("test.key1 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key2", &val)); + printf("test.key2 = %s\n", val); +} +EOF +rm -f ${ovconfig} +NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +cat <<'EOF' >EXPECTED +== stdout == +test.key1 = overridden-xdg-profile +test.key2 = testvalue2 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT +restore_database + +backup_database +test_begin_subtest "override config from \${HOME}/.notmuch-config.work (via args)" +ovconfig=${HOME}/.notmuch-config.work +cp ${NOTMUCH_CONFIG} ${ovconfig} +old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} +unset NOTMUCH_CONFIG +notmuch --config=${ovconfig} config set test.key1 overridden-profile +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% work +{ + EXPECT0(notmuch_database_get_config (db, "test.key1", &val)); + printf("test.key1 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key2", &val)); + printf("test.key2 = %s\n", val); +} +EOF +#rm -f ${ovconfig} +NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +cat <<'EOF' >EXPECTED +== stdout == +test.key1 = overridden-profile +test.key2 = testvalue2 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT +restore_database + +test_begin_subtest "no config, fail to open database" +old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} +unset NOTMUCH_CONFIG +cat c_head - c_tail <<'EOF' | test_C %NULL% '' %NULL% +{ + printf("NOT RUN"); +} +EOF +NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +cat <<'EOF' >EXPECTED +== stdout == +== stderr == +error opening database +No database found +Error: could not locate database. + +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "open database from NOTMUCH_DATABASE" +old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} +unset NOTMUCH_CONFIG +export NOTMUCH_DATABASE=${MAIL_DIR} +cat c_head - c_tail <<'EOF' | test_C %NULL% '' %NULL% +{ + EXPECT0(notmuch_database_get_config (db, "test.key1", &val)); + printf("test.key1 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key2", &val)); + printf("test.key2 = %s\n", val); +} +EOF +export NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +unset NOTMUCH_DATABASE +cat <<'EOF' >EXPECTED +== stdout == +test.key1 = testvalue1 +test.key2 = testvalue2 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "NOTMUCH_DATABASE overrides config" +cp notmuch-config notmuch-config.bak +notmuch config set database.path /nonexistent +export NOTMUCH_DATABASE=${MAIL_DIR} +cat c_head - c_tail <<'EOF' | test_C %NULL% '' %NULL% +{ + EXPECT0(notmuch_database_get_config (db, "test.key1", &val)); + printf("test.key1 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key2", &val)); + printf("test.key2 = %s\n", val); +} +EOF +NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +unset NOTMUCH_DATABASE +cat <<'EOF' >EXPECTED +== stdout == +test.key1 = testvalue1 +test.key2 = testvalue2 +== stderr == +EOF +cp notmuch-config.bak notmuch-config +test_expect_equal_file EXPECTED OUTPUT + +cat < c_head2 +#include +#include +#include + +int main (int argc, char** argv) +{ + notmuch_database_t *db; + char *val; + notmuch_status_t stat; + char *msg = NULL; + + for (int i = 1; i < argc; i++) + if (strcmp (argv[i], "%NULL%") == 0) argv[i] = NULL; + + stat = notmuch_database_load_config (argv[1], + argv[2], + argv[3], + &db, + &msg); + if (stat != NOTMUCH_STATUS_SUCCESS && stat != NOTMUCH_STATUS_NO_CONFIG) { + fprintf (stderr, "error opening database\n%d: %s\n%s\n", stat, + notmuch_status_to_string (stat), msg ? msg : ""); + exit (1); + } +EOF + + +test_begin_subtest "notmuch_database_get_config (ndlc)" +cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL% +{ + EXPECT0(notmuch_database_get_config (db, "test.key1", &val)); + printf("test.key1 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key2", &val)); + printf("test.key2 = %s\n", val); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +test.key1 = testvalue1 +test.key2 = testvalue2 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + + +test_begin_subtest "notmuch_database_get_config_list: all pairs (ndlc)" +cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL% +{ + notmuch_config_list_t *list; + EXPECT0(notmuch_database_get_config_list (db, "", &list)); + for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) { + printf("%s %s\n", notmuch_config_list_key (list), notmuch_config_list_value(list)); + } + notmuch_config_list_destroy (list); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +aaabefore beforeval +key with spaces value, with, spaces! +test.key1 testvalue1 +test.key2 testvalue2 +zzzafter afterval +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "notmuch_database_get_config_list: one prefix (ndlc)" +cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL% +{ + notmuch_config_list_t *list; + EXPECT0(notmuch_database_get_config_list (db, "test.key", &list)); + for (; notmuch_config_list_valid (list); notmuch_config_list_move_to_next (list)) { + printf("%s %s\n", notmuch_config_list_key (list), notmuch_config_list_value(list)); + } + notmuch_config_list_destroy (list); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +test.key1 testvalue1 +test.key2 testvalue2 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "list by keys (ndlc)" +notmuch config set search.exclude_tags "foo;bar;fub" +notmuch config set new.ignore "sekrit_junk" +cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL% +{ + notmuch_config_key_t key; + for (key = NOTMUCH_CONFIG_FIRST; + key < NOTMUCH_CONFIG_LAST; + key = (notmuch_config_key_t)(key + 1)) { + const char *val = notmuch_config_get (db, key); + printf("%s\n", val ? val : "NULL" ); + } +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +MAIL_DIR +MAIL_DIR +MAIL_DIR/.notmuch/hooks +MAIL_DIR/.notmuch/backups +foo;bar;fub +unread;inbox +sekrit_junk +true +test_suite@notmuchmail.org +test_suite_other@notmuchmail.org;test_suite@otherdomain.org +Notmuch Test Suite +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "load default values (ndlc, nonexistent config)" +cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} /nonexistent %NULL% +{ + notmuch_config_key_t key; + for (key = NOTMUCH_CONFIG_FIRST; + key < NOTMUCH_CONFIG_LAST; + key = (notmuch_config_key_t)(key + 1)) { + const char *val = notmuch_config_get (db, key); + printf("%s\n", val ? val : "NULL" ); + } +} +EOF + +notmuch_passwd_sanitize < OUTPUT > OUTPUT.clean +cat <<'EOF' >EXPECTED +== stdout == +MAIL_DIR +MAIL_DIR +MAIL_DIR/.notmuch/hooks +MAIL_DIR/.notmuch/backups + +unread;inbox + +true +USERNAME@FQDN +NULL +USER_FULL_NAME +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT.clean + +backup_database +test_begin_subtest "override config from \${HOME}/.notmuch-config (ndlc)" +ovconfig=${HOME}/.notmuch-config +cp ${NOTMUCH_CONFIG} ${ovconfig} +old_NOTMUCH_CONFIG=${NOTMUCH_CONFIG} +unset NOTMUCH_CONFIG +notmuch --config=${ovconfig} config set test.key1 overridden-home +cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} %NULL% %NULL% +{ + EXPECT0(notmuch_database_get_config (db, "test.key1", &val)); + printf("test.key1 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key2", &val)); + printf("test.key2 = %s\n", val); +} +EOF +rm -f ${ovconfig} +NOTMUCH_CONFIG=${old_NOTMUCH_CONFIG} +cat <<'EOF' >EXPECTED +== stdout == +test.key1 = overridden-home +test.key2 = testvalue2 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT +restore_database + +test_begin_subtest "notmuch_config_get_pairs: prefix (ndlc)" +cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL% +{ + notmuch_config_pairs_t *list; + for (list = notmuch_config_get_pairs (db, "user."); + notmuch_config_pairs_valid (list); + notmuch_config_pairs_move_to_next (list)) { + printf("%s %s\n", notmuch_config_pairs_key (list), notmuch_config_pairs_value(list)); + } + notmuch_config_pairs_destroy (list); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +user.name Notmuch Test Suite +user.other_email test_suite_other@notmuchmail.org;test_suite@otherdomain.org +user.primary_email test_suite@notmuchmail.org +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "notmuch_config_get_pairs: all pairs (ndlc)" +cat c_head2 - c_tail <<'EOF' | test_C ${MAIL_DIR} ${NOTMUCH_CONFIG} %NULL% +{ + notmuch_config_pairs_t *list; + for (list = notmuch_config_get_pairs (db, ""); + notmuch_config_pairs_valid (list); + notmuch_config_pairs_move_to_next (list)) { + printf("%s %s\n", notmuch_config_pairs_key (list), notmuch_config_pairs_value(list)); + } + notmuch_config_pairs_destroy (list); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +aaabefore beforeval +database.backup_dir MAIL_DIR/.notmuch/backups +database.hook_dir MAIL_DIR/.notmuch/hooks +database.mail_root MAIL_DIR +database.path MAIL_DIR +key with spaces value, with, spaces! +maildir.synchronize_flags true +new.ignore sekrit_junk +new.tags unread;inbox +search.exclude_tags foo;bar;fub +test.key1 testvalue1 +test.key2 testvalue2 +user.name Notmuch Test Suite +user.other_email test_suite_other@notmuchmail.org;test_suite@otherdomain.org +user.primary_email test_suite@notmuchmail.org +zzzafter afterval +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + test_done diff --git a/test/T595-reopen.sh b/test/T595-reopen.sh new file mode 100755 index 00000000..7375e2ac --- /dev/null +++ b/test/T595-reopen.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +test_description="library reopen API" + +. $(dirname "$0")/test-lib.sh || exit 1 + +add_email_corpus + +cat < c_head +#include +#include +#include + +int main (int argc, char** argv) +{ + notmuch_database_t *db; + char *val; + notmuch_status_t stat; + notmuch_database_mode_t mode = NOTMUCH_DATABASE_MODE_READ_ONLY; + + char *msg = NULL; + + for (int i = 1; i < argc; i++) + if (strcmp (argv[i], "%NULL%") == 0) argv[i] = NULL; + + if (argv[2] && (argv[2][0] == 'w' || argv[2][0] == 'W')) + mode = NOTMUCH_DATABASE_MODE_READ_WRITE; + + stat = notmuch_database_open_with_config (argv[1], + mode, + argv[3], + argv[4], + &db, + &msg); + if (stat != NOTMUCH_STATUS_SUCCESS) { + fprintf (stderr, "error opening database: %d %s\n", stat, msg ? msg : ""); + exit (1); + } +EOF + +cat < c_tail + EXPECT0(notmuch_database_destroy(db)); +} +EOF + +# The sequence of tests is important here + +test_begin_subtest "notmuch_database_reopen (read=>write)" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} read ${NOTMUCH_CONFIG} +{ + EXPECT0(notmuch_database_reopen (db, NOTMUCH_DATABASE_MODE_READ_WRITE)); + EXPECT0(notmuch_database_set_config (db, "test.key1", "testvalue1")); + EXPECT0(notmuch_database_set_config (db, "test.key2", "testvalue2")); + EXPECT0(notmuch_database_get_config (db, "test.key1", &val)); + printf("test.key1 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key2", &val)); + printf("test.key2 = %s\n", val); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +test.key1 = testvalue1 +test.key2 = testvalue2 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "notmuch_database_reopen (read=>read)" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} read ${NOTMUCH_CONFIG} +{ + EXPECT0(notmuch_database_reopen (db, NOTMUCH_DATABASE_MODE_READ_ONLY)); + EXPECT0(notmuch_database_get_config (db, "test.key1", &val)); + printf("test.key1 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key2", &val)); + printf("test.key2 = %s\n", val); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +test.key1 = testvalue1 +test.key2 = testvalue2 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "notmuch_database_reopen (write=>read)" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} write ${NOTMUCH_CONFIG} +{ + EXPECT0(notmuch_database_reopen (db, NOTMUCH_DATABASE_MODE_READ_ONLY)); + EXPECT0(notmuch_database_get_config (db, "test.key1", &val)); + printf("test.key1 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key2", &val)); + printf("test.key2 = %s\n", val); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +test.key1 = testvalue1 +test.key2 = testvalue2 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "notmuch_database_reopen (write=>write)" +cat c_head - c_tail <<'EOF' | test_C ${MAIL_DIR} write ${NOTMUCH_CONFIG} +{ + EXPECT0(notmuch_database_reopen (db, NOTMUCH_DATABASE_MODE_READ_WRITE)); + EXPECT0(notmuch_database_set_config (db, "test.key3", "testvalue3")); + EXPECT0(notmuch_database_get_config (db, "test.key1", &val)); + printf("test.key1 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key2", &val)); + printf("test.key2 = %s\n", val); + EXPECT0(notmuch_database_get_config (db, "test.key3", &val)); + printf("test.key3 = %s\n", val); +} +EOF +cat <<'EOF' >EXPECTED +== stdout == +test.key1 = testvalue1 +test.key2 = testvalue2 +test.key3 = testvalue3 +== stderr == +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_done diff --git a/test/T600-named-queries.sh b/test/T600-named-queries.sh index 0ae8b83d..a7b84995 100755 --- a/test/T600-named-queries.sh +++ b/test/T600-named-queries.sh @@ -4,13 +4,13 @@ test_description='named queries' QUERYSTR="date:2009-11-18..2009-11-18 and tag:unread" -test_begin_subtest "error adding named query before initializing DB" -test_expect_code 1 "notmuch config set query.test \"$QUERYSTR\"" +test_begin_subtest "error adding named query to DB before initialization" +test_expect_code 1 "notmuch config set --database query.test \"$QUERYSTR\"" add_email_corpus -test_begin_subtest "adding named query" -test_expect_success "notmuch config set query.test \"$QUERYSTR\"" +test_begin_subtest "adding named query (database)" +test_expect_success "notmuch config set --database query.test \"$QUERYSTR\"" test_begin_subtest "adding nested named query" QUERYSTR2="query:test and subject:Maildir" @@ -32,7 +32,6 @@ test_begin_subtest "dump named queries" notmuch dump | grep '^#@' > OUTPUT cat< QUERIES.BEFORE #@ query.test date%3a2009-11-18..2009-11-18%20and%20tag%3aunread -#@ query.test2 query%3atest%20and%20subject%3aMaildir EOF test_expect_equal_file QUERIES.BEFORE OUTPUT @@ -40,23 +39,21 @@ test_begin_subtest 'dumping large queries' # This value is just large enough to trigger a limitation of gzprintf # to 8191 bytes in total (by default). repeat=1329 -notmuch config set query.big "$(seq -s' ' $repeat)" +notmuch config set --database query.big "$(seq -s' ' $repeat)" notmuch dump --include=config > OUTPUT -notmuch config set query.big '' +notmuch config set --database query.big printf "#notmuch-dump batch-tag:3 config\n#@ query.big " > EXPECTED seq -s'%20' $repeat >> EXPECTED cat <> EXPECTED #@ query.test date%3a2009-11-18..2009-11-18%20and%20tag%3aunread -#@ query.test2 query%3atest%20and%20subject%3aMaildir EOF test_expect_equal_file EXPECTED OUTPUT test_begin_subtest "delete named queries" notmuch dump > BEFORE -notmuch config set query.test +notmuch config set --database query.test notmuch dump | grep '^#@' > OUTPUT cat< EXPECTED -#@ query.test2 query%3atest%20and%20subject%3aMaildir EOF test_expect_equal_file EXPECTED OUTPUT diff --git a/test/T670-duplicate-mid.sh b/test/T670-duplicate-mid.sh index 7ae60595..8fec291e 100755 --- a/test/T670-duplicate-mid.sh +++ b/test/T670-duplicate-mid.sh @@ -2,6 +2,8 @@ test_description="duplicate message ids" . $(dirname "$0")/test-lib.sh || exit 1 +test_require_external_prereq xapian-delve + add_message '[id]="duplicate"' '[subject]="message 1" [filename]=copy1' add_message '[id]="duplicate"' '[subject]="message 2" [filename]=copy2' diff --git a/test/T700-reindex.sh b/test/T700-reindex.sh index 3d7c930d..f51130e8 100755 --- a/test/T700-reindex.sh +++ b/test/T700-reindex.sh @@ -34,11 +34,11 @@ notmuch dump > OUTPUT test_expect_equal_file initial-dump OUTPUT test_begin_subtest 'reindex preserves tags with special prefixes' -notmuch tag +attachment2 +encrypted2 +signed2 '*' +notmuch tag +attachment2 +encrypted2 +signed2 '*' notmuch dump > EXPECTED notmuch reindex '*' notmuch dump > OUTPUT -notmuch tag -attachment2 -encrypted2 -signed2 '*' +notmuch tag -attachment2 -encrypted2 -signed2 '*' test_expect_equal_file EXPECTED OUTPUT test_begin_subtest 'reindex moves a message between threads' diff --git a/test/T750-gzip.sh b/test/T750-gzip.sh index fac41d39..4408d085 100755 --- a/test/T750-gzip.sh +++ b/test/T750-gzip.sh @@ -171,7 +171,7 @@ test_expect_equal_file EXPECTED OUTPUT add_email_corpus lkml test_begin_subtest "new doesn't run out of file descriptors with many gzipped files" ulimit -n 200 -gzip --recursive ${MAIL_DIR} +find ${MAIL_DIR} -name .notmuch -prune -o -type f -print0 | xargs -0 gzip -- test_expect_success "notmuch new" test_done diff --git a/test/T750-user-header.sh b/test/T750-user-header.sh index 204c052a..05f80885 100755 --- a/test/T750-user-header.sh +++ b/test/T750-user-header.sh @@ -2,13 +2,6 @@ test_description='indexing user specified headers' . $(dirname "$0")/test-lib.sh || exit 1 -test_begin_subtest "error adding user header before initializing DB" -notmuch config set index.header.List List-Id 2>&1 | notmuch_dir_sanitize > OUTPUT -cat < EXPECTED -Error opening database at MAIL_DIR/.notmuch: No such file or directory -EOF -test_expect_equal_file EXPECTED OUTPUT - add_email_corpus notmuch search '*' | notmuch_search_sanitize > initial-threads @@ -108,4 +101,22 @@ MAIL_DIR/new/04:2, EOF test_expect_equal_file EXPECTED OUTPUT +test_begin_subtest "index user header, config from file" +field_name="Test" +printf "\n[index]\nheader.${field_name} = List-Id\n" >> notmuch-config +notmuch reindex '*' +notmuch search --output=files ${field_name}:notmuch | notmuch_search_files_sanitize | sort > OUTPUT +cat < EXPECTED +MAIL_DIR/bar/baz/05:2, +MAIL_DIR/bar/baz/23:2, +MAIL_DIR/bar/baz/24:2, +MAIL_DIR/bar/cur/20:2, +MAIL_DIR/bar/new/21:2, +MAIL_DIR/bar/new/22:2, +MAIL_DIR/foo/cur/08:2, +MAIL_DIR/foo/new/03:2, +MAIL_DIR/new/04:2, +EOF +test_expect_equal_file EXPECTED OUTPUT + test_done diff --git a/test/ghost-report.cc b/test/ghost-report.cc index fad9a71d..9d9e7a7f 100644 --- a/test/ghost-report.cc +++ b/test/ghost-report.cc @@ -12,5 +12,6 @@ main (int argc, char **argv) } Xapian::Database db (argv[1]); + std::cout << db.get_termfreq ("Tghost") << std::endl; } diff --git a/test/json_check_nodes.py b/test/json_check_nodes.py index 17403c57..fd8f1607 100755 --- a/test/json_check_nodes.py +++ b/test/json_check_nodes.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import re import sys import json diff --git a/test/make-db-version.cc b/test/make-db-version.cc index 78feaf72..238584e2 100644 --- a/test/make-db-version.cc +++ b/test/make-db-version.cc @@ -17,6 +17,7 @@ main (int argc, char **argv) } std::string nmpath (argv[1]); + nmpath += "/.notmuch"; if (mkdir (nmpath.c_str (), 0777) < 0) { perror (("failed to create " + nmpath).c_str ()); diff --git a/test/notmuch-test b/test/notmuch-test index b58fd3b3..cbd33f93 100755 --- a/test/notmuch-test +++ b/test/notmuch-test @@ -50,7 +50,7 @@ META_FAILURE= # Run the tests if test -z "$NOTMUCH_TEST_SERIALIZE" && command -v parallel >/dev/null ; then test -t 1 && export COLORS_WITHOUT_TTY=t || : - if parallel -h | grep -q GNU ; then + if parallel --version 2>&1 | grep -q GNU ; then echo "INFO: running tests with GNU parallel" printf '%s\n' $TESTS | $TEST_TIMEOUT_CMD parallel else diff --git a/test/random-corpus.c b/test/random-corpus.c index ff413252..7cde22c5 100644 --- a/test/random-corpus.c +++ b/test/random-corpus.c @@ -141,7 +141,6 @@ main (int argc, char **argv) void *ctx = talloc_new (NULL); const char *config_path = NULL; - notmuch_config_t *config; notmuch_database_t *notmuch; int num_messages = 500; @@ -179,17 +178,18 @@ main (int argc, char **argv) exit (1); } - config = notmuch_config_open (ctx, config_path, false); - if (config == NULL) - return 1; - - if (notmuch_database_open (notmuch_config_get_database_path (config), - NOTMUCH_DATABASE_MODE_READ_WRITE, ¬much)) + if (notmuch_database_open_with_config (NULL, + NOTMUCH_DATABASE_MODE_READ_WRITE, + config_path, + NULL, + ¬much, + NULL)) return 1; srandom (seed); int count; + for (count = 0; count < num_messages; count++) { int j; /* explicitly allow zero tags */ diff --git a/test/setup.expected-output/config-with-comments b/test/setup.expected-output/config-with-comments new file mode 100644 index 00000000..56c628e5 --- /dev/null +++ b/test/setup.expected-output/config-with-comments @@ -0,0 +1,86 @@ +# .notmuch-config - Configuration file for the notmuch mail system +# +# For more information about notmuch, see https://notmuchmail.org + +# Database configuration +# +# The only value supported here is 'path' which should be the top-level +# directory where your mail currently exists and to where mail will be +# delivered in the future. Files should be individual email messages. +# Notmuch will store its database within a sub-directory of the path +# configured here named ".notmuch". +# +[database] +path=/path/to/maildir + +# User configuration +# +# Here is where you can let notmuch know how you would like to be +# addressed. Valid settings are +# +# name Your full name. +# primary_email Your primary email address. +# other_email A list (separated by ';') of other email addresses +# at which you receive email. +# +# Notmuch will use the various email addresses configured here when +# formatting replies. It will avoid including your own addresses in the +# recipient list of replies, and will set the From address based on the +# address to which the original email was addressed. +# +[user] +name=Test Suite +primary_email=test.suite@example.com +other_email=another.suite@example.com; + +# Configuration for "notmuch new" +# +# The following options are supported here: +# +# tags A list (separated by ';') of the tags that will be +# added to all messages incorporated by "notmuch new". +# +# ignore A list (separated by ';') of file and directory names +# that will not be searched for messages by "notmuch new". +# +# NOTE: *Every* file/directory that goes by one of those +# names will be ignored, independent of its depth/location +# in the mail store. +# +[new] +tags=foo;bar; + +# Search configuration +# +# The following option is supported here: +# +# exclude_tags +# A ;-separated list of tags that will be excluded from +# search results by default. Using an excluded tag in a +# query will override that exclusion. +# +[search] +exclude_tags=baz; + +# Maildir compatibility configuration +# +# The following option is supported here: +# +# synchronize_flags Valid values are true and false. +# +# If true, then the following maildir flags (in message filenames) +# will be synchronized with the corresponding notmuch tags: +# +# Flag Tag +# ---- ------- +# D draft +# F flagged +# P passed +# R replied +# S unread (added when 'S' flag is not present) +# +# The "notmuch new" command will notice flag changes in filenames +# and update tags, while the "notmuch tag" and "notmuch restore" +# commands will notice tag changes and update flags in filenames +# +[maildir] diff --git a/test/test-lib.el b/test/test-lib.el index ec16c59c..32d53736 100644 --- a/test/test-lib.el +++ b/test/test-lib.el @@ -1,4 +1,4 @@ -;; test-lib.el --- auxiliary stuff for Notmuch Emacs tests. +;;; test-lib.el --- auxiliary stuff for Notmuch Emacs tests ;; ;; Copyright © Carl Worth ;; Copyright © David Edmondson @@ -20,6 +20,8 @@ ;; ;; Authors: Dmitry Kurochkin +;;; Code: + (require 'cl-lib) ;; Ensure that the dynamic variables that are defined by this library @@ -97,6 +99,9 @@ running, quit if it terminated." (add-hook 'notmuch-hello-refresh-hook (lambda () (cl-incf notmuch-hello-refresh-hook-counter))) +(defvar notmuch-test-tag-hook-output nil) +(defun notmuch-test-tag-hook () (push (cons query tag-changes) notmuch-test-tag-hook-output)) + (defun notmuch-test-mark-links () "Enclose links in the current buffer with << and >>." ;; Links are often created by jit-lock functions diff --git a/test/test-lib.sh b/test/test-lib.sh index c23a0d20..862c6bc9 100644 --- a/test/test-lib.sh +++ b/test/test-lib.sh @@ -104,9 +104,15 @@ unset CDPATH unset GREP_OPTIONS +# For lib/open.cc:_load_key_file +unset XDG_CONFIG_HOME + # For emacsclient unset ALTERNATE_EDITOR +# for reproducibility +unset EMAIL + add_gnupg_home () { [ -e "${GNUPGHOME}/gpg.conf" ] && return @@ -130,6 +136,8 @@ add_gnupg_home () add_gpgsm_home () { + test_require_external_prereq openssl + local fpr [ -e "$GNUPGHOME/gpgsm.conf" ] && return _gnupg_exit () { gpgconf --kill all 2>/dev/null || true; } @@ -694,6 +702,22 @@ notmuch_built_with_sanitize () sed 's/^built_with[.]\(.*\)=.*$/built_with.\1=something/' } +notmuch_passwd_sanitize() +{ + ${NOTMUCH_PYTHON} -c' +import os, sys, pwd, socket + +pw = pwd.getpwuid(os.getuid()) +user = pw.pw_name +name = pw.pw_gecos.partition(",")[0] +fqdn = socket.getfqdn() + +for l in sys.stdin: + l = l.replace(user, "USERNAME").replace(fqdn, "FQDN").replace(".(none)","").replace(name, "USER_FULL_NAME") + sys.stdout.write(l) +' +} + notmuch_config_sanitize () { notmuch_dir_sanitize | notmuch_built_with_sanitize @@ -1117,7 +1141,7 @@ notmuch_with_shim () { base_name="$1" shift shim_file="${base_name}.so" - LD_PRELOAD=./${shim_file}${LD_PRELOAD:+:$LD_PRELOAD} notmuch-shared "$@" + LD_PRELOAD=${LD_PRELOAD:+:$LD_PRELOAD}:./${shim_file} notmuch-shared "$@" } # Creates a script that counts how much time it is executed and calls @@ -1270,3 +1294,5 @@ test_declare_external_prereq gpg test_declare_external_prereq openssl test_declare_external_prereq gpgsm test_declare_external_prereq ${NOTMUCH_PYTHON} +test_declare_external_prereq xapian-metadata +test_declare_external_prereq xapian-delve diff --git a/util/Makefile.local b/util/Makefile.local index 7ef029a5..8a0b9bc3 100644 --- a/util/Makefile.local +++ b/util/Makefile.local @@ -6,7 +6,7 @@ extra_cflags += -I$(srcdir)/$(dir) libnotmuch_util_c_srcs := $(dir)/xutil.c $(dir)/error_util.c $(dir)/hex-escape.c \ $(dir)/string-util.c $(dir)/talloc-extra.c $(dir)/zlib-extra.c \ $(dir)/util.c $(dir)/gmime-extra.c $(dir)/crypto.c \ - $(dir)/repair.c \ + $(dir)/repair.c $(dir)/path-util.c \ $(dir)/unicode-util.c libnotmuch_util_modules := $(libnotmuch_util_c_srcs:.c=.o) diff --git a/util/crypto.c b/util/crypto.c index c09f467b..156a6550 100644 --- a/util/crypto.c +++ b/util/crypto.c @@ -48,7 +48,8 @@ _notmuch_crypto_decrypt (bool *attempted, notmuch_message_properties_t *list = NULL; for (list = notmuch_message_get_properties (message, "session-key", TRUE); - notmuch_message_properties_valid (list); notmuch_message_properties_move_to_next (list)) { + notmuch_message_properties_valid (list); notmuch_message_properties_move_to_next ( + list)) { if (err && *err) { g_error_free (*err); *err = NULL; @@ -61,12 +62,13 @@ _notmuch_crypto_decrypt (bool *attempted, notmuch_message_properties_value (list), decrypt_result, err); } else if (GMIME_IS_APPLICATION_PKCS7_MIME (part)) { - GMimeApplicationPkcs7Mime *pkcs7 = GMIME_APPLICATION_PKCS7_MIME (part); + GMimeApplicationPkcs7Mime *pkcs7 = GMIME_APPLICATION_PKCS7_MIME (part); GMimeSecureMimeType type = g_mime_application_pkcs7_mime_get_smime_type (pkcs7); if (type == GMIME_SECURE_MIME_TYPE_ENVELOPED_DATA) { ret = g_mime_application_pkcs7_mime_decrypt (pkcs7, GMIME_DECRYPT_NONE, - notmuch_message_properties_value (list), + notmuch_message_properties_value ( + list), decrypt_result, err); } } @@ -90,6 +92,7 @@ _notmuch_crypto_decrypt (bool *attempted, if (attempted) *attempted = true; GMimeDecryptFlags flags = GMIME_DECRYPT_NONE; + if (decrypt == NOTMUCH_DECRYPT_TRUE && decrypt_result) flags |= GMIME_DECRYPT_EXPORT_SESSION_KEY; if (GMIME_IS_MULTIPART_ENCRYPTED (part)) { @@ -128,7 +131,8 @@ _notmuch_message_crypto_new (void *ctx) } notmuch_status_t -_notmuch_message_crypto_potential_sig_list (_notmuch_message_crypto_t *msg_crypto, GMimeSignatureList *sigs) +_notmuch_message_crypto_potential_sig_list (_notmuch_message_crypto_t *msg_crypto, + GMimeSignatureList *sigs) { if (! msg_crypto) return NOTMUCH_STATUS_NULL_POINTER; @@ -157,7 +161,8 @@ _notmuch_message_crypto_potential_sig_list (_notmuch_message_crypto_t *msg_crypt bool -_notmuch_message_crypto_potential_payload (_notmuch_message_crypto_t *msg_crypto, GMimeObject *part, GMimeObject *parent, int childnum) +_notmuch_message_crypto_potential_payload (_notmuch_message_crypto_t *msg_crypto, GMimeObject *part, + GMimeObject *parent, int childnum) { const char *protected_headers = NULL; const char *forwarded = NULL; @@ -175,7 +180,8 @@ _notmuch_message_crypto_potential_payload (_notmuch_message_crypto_t *msg_crypto * encryption protocol should be "control information" metadata, * not payload. So we skip it. (see * https://tools.ietf.org/html/rfc1847#page-8) */ - if (parent && GMIME_IS_MULTIPART_ENCRYPTED (parent) && childnum == GMIME_MULTIPART_ENCRYPTED_VERSION) { + if (parent && GMIME_IS_MULTIPART_ENCRYPTED (parent) && childnum == + GMIME_MULTIPART_ENCRYPTED_VERSION) { const char *enc_type = g_mime_object_get_content_type_parameter (parent, "protocol"); GMimeContentType *ct = g_mime_object_get_content_type (part); if (ct && enc_type) { diff --git a/util/crypto.h b/util/crypto.h index 4fa5599c..3c5d384b 100644 --- a/util/crypto.h +++ b/util/crypto.h @@ -80,7 +80,8 @@ _notmuch_message_crypto_new (void *ctx); * consider a particular signature as relevant for the message. */ notmuch_status_t -_notmuch_message_crypto_potential_sig_list (_notmuch_message_crypto_t *msg_crypto, GMimeSignatureList *sigs); +_notmuch_message_crypto_potential_sig_list (_notmuch_message_crypto_t *msg_crypto, + GMimeSignatureList *sigs); /* call successful_decryption during a depth-first-search on a message * to indicate that a part was successfully decrypted. @@ -95,7 +96,8 @@ _notmuch_message_crypto_successful_decryption (_notmuch_message_crypto_t *msg_cr * this message. */ bool -_notmuch_message_crypto_potential_payload (_notmuch_message_crypto_t *msg_crypto, GMimeObject *part, GMimeObject *parent, int childnum); +_notmuch_message_crypto_potential_payload (_notmuch_message_crypto_t *msg_crypto, GMimeObject *part, + GMimeObject *parent, int childnum); #ifdef __cplusplus diff --git a/util/gmime-extra.c b/util/gmime-extra.c index 04d8ed3d..81a5b174 100644 --- a/util/gmime-extra.c +++ b/util/gmime-extra.c @@ -101,6 +101,7 @@ g_mime_certificate_get_valid_userid (GMimeCertificate *cert) if (uid == NULL) return uid; GMimeValidity validity = g_mime_certificate_get_id_validity (cert); + if (validity == GMIME_VALIDITY_FULL || validity == GMIME_VALIDITY_ULTIMATE) return uid; return NULL; diff --git a/util/path-util.c b/util/path-util.c new file mode 100644 index 00000000..3267a967 --- /dev/null +++ b/util/path-util.c @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define _GNU_SOURCE + +#include "path-util.h" + +#include +#include + + +char * +notmuch_canonicalize_file_name (const char *path) +{ +#if HAVE_CANONICALIZE_FILE_NAME + return canonicalize_file_name (path); +#elif defined(PATH_MAX) + char *resolved_path = malloc (PATH_MAX + 1); + if (resolved_path == NULL) + return NULL; + + return realpath (path, resolved_path); +#else +#error undefined PATH_MAX _and_ missing canonicalize_file_name not supported +#endif +} diff --git a/util/path-util.h b/util/path-util.h new file mode 100644 index 00000000..ac85f696 --- /dev/null +++ b/util/path-util.h @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#ifndef NOTMUCH_UTIL_PATH_UTIL_H_ +#define NOTMUCH_UTIL_PATH_UTIL_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +char * +notmuch_canonicalize_file_name (const char *path); + +#ifdef __cplusplus +} +#endif + +#endif /* NOTMUCH_UTIL_PATH_UTIL_H_ */ diff --git a/util/repair.c b/util/repair.c index 5a64e001..5b0dfdf4 100644 --- a/util/repair.c +++ b/util/repair.c @@ -32,7 +32,8 @@ _notmuch_crypto_payload_has_legacy_display (GMimeObject *payload) if (! g_mime_content_type_is_type (g_mime_object_get_content_type (payload), "multipart", "mixed")) return false; - protected_header_parameter = g_mime_object_get_content_type_parameter (payload, "protected-headers"); + protected_header_parameter = g_mime_object_get_content_type_parameter (payload, + "protected-headers"); if ((! protected_header_parameter) || strcmp (protected_header_parameter, "v1")) return false; if (! GMIME_IS_MULTIPART (payload)) @@ -44,15 +45,16 @@ _notmuch_crypto_payload_has_legacy_display (GMimeObject *payload) return false; first = g_mime_multipart_get_part (mpayload, 0); /* Early implementations that generated "Legacy Display" parts used - Content-Type: text/rfc822-headers, but text/plain is more widely - rendered, so it is now the standard choice. We accept either as a - Legacy Display part. */ + * Content-Type: text/rfc822-headers, but text/plain is more widely + * rendered, so it is now the standard choice. We accept either as a + * Legacy Display part. */ if (! (g_mime_content_type_is_type (g_mime_object_get_content_type (first), "text", "plain") || g_mime_content_type_is_type (g_mime_object_get_content_type (first), "text", "rfc822-headers"))) return false; - protected_header_parameter = g_mime_object_get_content_type_parameter (first, "protected-headers"); + protected_header_parameter = g_mime_object_get_content_type_parameter (first, + "protected-headers"); if ((! protected_header_parameter) || strcmp (protected_header_parameter, "v1")) return false; if (! GMIME_IS_TEXT_PART (first)) @@ -77,7 +79,7 @@ static bool _notmuch_is_mixed_up_mangled (GMimeObject *part) { GMimeMultipart *mpart = NULL; - GMimeObject *parts[3] = {NULL, NULL, NULL}; + GMimeObject *parts[3] = { NULL, NULL, NULL }; GMimeContentType *type = NULL; char *prelude_string = NULL; bool prelude_is_empty; diff --git a/util/string-util.c b/util/string-util.c index de8430b2..9c46a81a 100644 --- a/util/string-util.c +++ b/util/string-util.c @@ -24,6 +24,7 @@ #include #include +#include char * strtok_len (char *s, const char *delim, size_t *len) @@ -37,6 +38,28 @@ strtok_len (char *s, const char *delim, size_t *len) return *len ? s : NULL; } +const char * +strsplit_len (const char *s, char delim, size_t *len) +{ + bool escaping = false; + size_t count = 0; + + /* Skip initial unescaped delimiters */ + while (*s && *s == delim) + s++; + + while (s[count] && (escaping || s[count] != delim)) { + escaping = (s[count] == '\\'); + count++; + } + + if (count == 0) + return NULL; + + *len = count; + return s; +} + const char * strtok_len_c (const char *s, const char *delim, size_t *len) { @@ -160,6 +183,7 @@ parse_boolean_term (void *ctx, const char *str, /* Parse prefix */ str = skip_space (str); const char *pos = strchr (str, ':'); + if (! pos || pos == str) goto FAIL; *prefix_out = talloc_strndup (ctx, str, pos - str); diff --git a/util/string-util.h b/util/string-util.h index fb95a740..80647c5f 100644 --- a/util/string-util.h +++ b/util/string-util.h @@ -26,6 +26,20 @@ char *strtok_len (char *s, const char *delim, size_t *len); /* Const version of strtok_len. */ const char *strtok_len_c (const char *s, const char *delim, size_t *len); +/* Simplified version of strtok_len, with a single delimiter. + * Handles escaping delimiters with \ + * Usage pattern: + * + * const char *tok = input; + * const char *delim = ';'; + * size_t tok_len = 0; + * + * while ((tok = strsplit_len (tok + tok_len, delim, &tok_len)) != NULL) { + * // do stuff with string tok of length tok_len + * } + */ +const char *strsplit_len (const char *s, char delim, size_t *len); + /* Return a talloced string with str sanitized. * * Whitespace characters (tabs and newlines) are replaced with spaces, diff --git a/util/zlib-extra.c b/util/zlib-extra.c index 3a75e504..1f5f9dbe 100644 --- a/util/zlib-extra.c +++ b/util/zlib-extra.c @@ -87,8 +87,9 @@ gz_error_string (util_status_t status, gzFile file) } const char * -gzerror_str(gzFile file) +gzerror_str (gzFile file) { int dummy; + return gzerror (file, &dummy); } diff --git a/util/zlib-extra.h b/util/zlib-extra.h index e9925c98..7532339b 100644 --- a/util/zlib-extra.h +++ b/util/zlib-extra.h @@ -30,7 +30,7 @@ gz_error_string (util_status_t status, gzFile stream); /* Call gzerror with a dummy errno argument, the docs don't promise to * support the NULL case */ const char * -gzerror_str(gzFile file); +gzerror_str (gzFile file); #ifdef __cplusplus } diff --git a/version.txt b/version.txt index a8a02172..d721c768 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.31.4 +0.32.3 diff --git a/vim/notmuch.vim b/vim/notmuch.vim index ad8b7c80..541698cd 100644 --- a/vim/notmuch.vim +++ b/vim/notmuch.vim @@ -666,7 +666,7 @@ ruby << EOF date = Time.at(e.newest_date).strftime(date_fmt) subject = e.messages.first['subject'] if $mail_installed - subject = Mail::Field.new("Subject: " + subject).to_s + subject = Mail::Field.parse("Subject: " + subject).to_s else subject = subject.force_encoding('utf-8') end